@moonwall/cli
Version:
Testing framework for the Moon family of projects
1,487 lines (1,470 loc) • 81.7 kB
JavaScript
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
// src/lib/configReader.ts
import "@moonbeam-network/api-augment";
import { readFile, access } from "fs/promises";
import { readFileSync as readFileSync2, existsSync as existsSync2, constants } from "fs";
import JSONC from "jsonc-parser";
import path2, { extname } from "path";
async function parseConfig(filePath) {
let result;
const file = await readFile(filePath, "utf8");
switch (extname(filePath)) {
case ".json":
result = JSON.parse(file);
break;
case ".config":
result = JSONC.parse(file);
break;
default:
result = void 0;
break;
}
return result;
}
function parseConfigSync(filePath) {
let result;
const file = readFileSync2(filePath, "utf8");
switch (extname(filePath)) {
case ".json":
result = JSON.parse(file);
break;
case ".config":
result = JSONC.parse(file);
break;
default:
result = void 0;
break;
}
return result;
}
function isOptionSet(option) {
const env = getEnvironmentFromConfig();
const optionValue = traverseConfig(env, option);
return optionValue !== void 0;
}
function isEthereumZombieConfig() {
const config = importJsonConfig();
const env = getEnvironmentFromConfig();
return env.foundation.type === "zombie" && !env.foundation.zombieSpec.disableDefaultEthProviders;
}
function isEthereumDevConfig() {
const config = importJsonConfig();
const env = getEnvironmentFromConfig();
return env.foundation.type === "dev" && !env.foundation.launchSpec[0].disableDefaultEthProviders;
}
function getEnvironmentFromConfig() {
const globalConfig = importJsonConfig();
const config = globalConfig.environments.find(({ name }) => name === process.env.MOON_TEST_ENV);
if (!config) {
throw new Error(`Environment ${process.env.MOON_TEST_ENV} not found in config`);
}
return config;
}
function importJsonConfig() {
if (cachedConfig) {
return cachedConfig;
}
const configPath = process.env.MOON_CONFIG_PATH;
if (!configPath) {
throw new Error("No moonwall config path set. This is a defect, please raise it.");
}
const filePath = path2.isAbsolute(configPath) ? configPath : path2.join(process.cwd(), configPath);
try {
const config = parseConfigSync(filePath);
const replacedConfig = replaceEnvVars(config);
cachedConfig = replacedConfig;
return cachedConfig;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
async function importAsyncConfig() {
if (cachedConfig) {
return cachedConfig;
}
const configPath = process.env.MOON_CONFIG_PATH;
if (!configPath) {
throw new Error("No moonwall config path set. This is a defect, please raise it.");
}
const filePath = path2.isAbsolute(configPath) ? configPath : path2.join(process.cwd(), configPath);
try {
const config = await parseConfig(filePath);
const replacedConfig = replaceEnvVars(config);
cachedConfig = replacedConfig;
return cachedConfig;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
function replaceEnvVars(value) {
if (typeof value === "string") {
return value.replace(/\$\{([^}]+)\}/g, (match, group) => {
const envVarValue = process.env[group];
return envVarValue || match;
});
}
if (Array.isArray(value)) {
return value.map(replaceEnvVars);
}
if (typeof value === "object" && value !== null) {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, replaceEnvVars(v)]));
}
return value;
}
function traverseConfig(configObj, option) {
if (typeof configObj !== "object" || !configObj) return void 0;
if (Object.prototype.hasOwnProperty.call(configObj, option)) {
return configObj[option];
}
for (const key in configObj) {
const result = traverseConfig(configObj[key], option);
if (result !== void 0) {
return result;
}
}
return void 0;
}
var cachedConfig;
var init_configReader = __esm({
"src/lib/configReader.ts"() {
"use strict";
}
});
// src/lib/upgradeProcedures.ts
import "@moonbeam-network/api-augment";
import { blake2AsHex as blake2AsHex2 } from "@polkadot/util-crypto";
import chalk from "chalk";
import { sha256 } from "ethers";
import fs2, { existsSync, readFileSync } from "fs";
// src/lib/binariesHelpers.ts
import "@moonbeam-network/api-augment";
import path from "path";
import fs from "fs";
import child_process from "child_process";
import { OVERRIDE_RUNTIME_PATH } from "@moonwall/util";
var BINARY_DIRECTORY = process.env.BINARY_DIRECTORY || "binaries";
var RUNTIME_DIRECTORY = process.env.RUNTIME_DIRECTORY || "runtimes";
var SPECS_DIRECTORY = process.env.SPECS_DIRECTORY || "specs";
async function getRuntimeWasm(runtimeName, runtimeTag, localPath) {
const runtimePath = path.join(RUNTIME_DIRECTORY, `${runtimeName}-${runtimeTag}.wasm`);
if (!fs.existsSync(RUNTIME_DIRECTORY)) {
fs.mkdirSync(RUNTIME_DIRECTORY, { recursive: true });
}
if (runtimeTag === "local") {
const builtRuntimePath = localPath ? localPath : path.join(
OVERRIDE_RUNTIME_PATH || `../target/release/wbuild/${runtimeName}-runtime/`,
`${runtimeName}_runtime.compact.compressed.wasm`
);
const code = fs.readFileSync(builtRuntimePath);
fs.writeFileSync(runtimePath, `0x${code.toString("hex")}`);
} else if (!fs.existsSync(runtimePath)) {
console.log(` Missing ${runtimePath} locally, downloading it...`);
child_process.execSync(
`mkdir -p ${path.dirname(
runtimePath
)} && wget -q https://github.com/PureStake/moonbeam/releases/download/${runtimeTag}/${runtimeName}-${runtimeTag}.wasm -O ${runtimePath}.bin`
);
const code = fs.readFileSync(`${runtimePath}.bin`);
fs.writeFileSync(runtimePath, `0x${code.toString("hex")}`);
console.log(`${runtimePath} downloaded !`);
}
return runtimePath;
}
// src/lib/governanceProcedures.ts
import "@moonbeam-network/api-augment";
import {
GLMR,
alith,
baltathar,
charleth,
dorothy,
ethan,
faith,
filterAndApply,
signAndSend
} from "@moonwall/util";
import { blake2AsHex } from "@polkadot/util-crypto";
var COUNCIL_MEMBERS = [baltathar, charleth, dorothy];
var COUNCIL_THRESHOLD = Math.ceil(COUNCIL_MEMBERS.length * 2 / 3);
var TECHNICAL_COMMITTEE_MEMBERS = [alith, baltathar];
var TECHNICAL_COMMITTEE_THRESHOLD = Math.ceil(
TECHNICAL_COMMITTEE_MEMBERS.length * 2 / 3
);
var OPEN_TECHNICAL_COMMITTEE_MEMBERS = [alith, baltathar];
var OPEN_TECHNICAL_COMMITTEE_THRESHOLD = Math.ceil(
OPEN_TECHNICAL_COMMITTEE_MEMBERS.length * 2 / 3
);
var executeOpenTechCommitteeProposal = async (api, encodedHash) => {
console.log("Executing OpenTechCommittee proposal");
const queryPreimage = await api.query.preimage.requestStatusFor(encodedHash);
if (queryPreimage.isNone) {
throw new Error("Preimage not found");
}
process.stdout.write(`Sending proposal + vote for ${encodedHash}...`);
const proposalLen = queryPreimage.unwrap().asUnrequested.len;
const dispatchCallHex = api.tx.whitelist.dispatchWhitelistedCall(encodedHash, proposalLen, {
refTime: 2e9,
proofSize: 1e5
}).method.toHex();
const dispatchCallPreimageHash = blake2AsHex(dispatchCallHex);
await signAndSend(api.tx.preimage.notePreimage(dispatchCallHex), charleth);
const queryDispatchPreimage = await api.query.preimage.requestStatusFor(dispatchCallPreimageHash);
if (queryDispatchPreimage.isNone) {
throw new Error("Dispatch preimage not found");
}
const dispatchCallPreimageLen = queryDispatchPreimage.unwrap().asUnrequested.len;
await signAndSend(
api.tx.referenda.submit(
{
Origins: { whitelistedcaller: "WhitelistedCaller" }
},
{
Lookup: {
hash: dispatchCallPreimageHash,
len: dispatchCallPreimageLen
}
},
{ After: { After: 0 } }
),
charleth
);
const proposalId = (await api.query.referenda.referendumCount()).toNumber() - 1;
if (proposalId < 0) {
throw new Error("Proposal id not found");
}
await api.tx.referenda.placeDecisionDeposit(proposalId).signAndSend(alith);
process.stdout.write(`Sending proposal to openTechCommittee to whitelist ${encodedHash}...`);
await signAndSend(
api.tx.openTechCommitteeCollective.propose(2, api.tx.whitelist.whitelistCall(encodedHash), 100)
);
const openTechProposal = (await api.query.openTechCommitteeCollective.proposals()).at(-1);
if (!openTechProposal || openTechProposal?.isEmpty) {
throw new Error("OpenTechProposal not found");
}
const index = (await api.query.openTechCommitteeCollective.proposalCount()).toNumber() - 1;
if (index < 0) {
throw new Error("OpenTechProposal index not found");
}
process.stdout.write("\u2705\n");
const baltaNonce = (await api.rpc.system.accountNextIndex(baltathar.address)).toNumber();
process.stdout.write("Voting on openTechCommittee proposal...");
await Promise.all([
signAndSend(api.tx.openTechCommitteeCollective.vote(openTechProposal, index, true)),
signAndSend(
api.tx.openTechCommitteeCollective.vote(openTechProposal, index, true),
baltathar,
baltaNonce
),
signAndSend(
api.tx.openTechCommitteeCollective.close(
openTechProposal,
index,
{
refTime: 2e9,
proofSize: 1e5
},
100
),
baltathar,
baltaNonce + 1
)
]);
process.stdout.write("\u2705\n");
process.stdout.write("Voting on main referendum proposal...");
const bal = (await api.query.system.account(dorothy.address)).data.free.toBigInt();
if (bal <= GLMR) {
throw new Error("Dorothy has no funds to vote with");
}
await signAndSend(
api.tx.convictionVoting.vote(proposalId, {
Standard: {
vote: { aye: true, conviction: "Locked6x" },
balance: bal - GLMR
}
}),
dorothy
);
process.stdout.write("\u2705\n");
process.stdout.write(`Waiting for referendum [${proposalId}] to be no longer ongoing...`);
let referendaInfo;
for (; ; ) {
try {
referendaInfo = (await api.query.referenda.referendumInfoFor(proposalId)).unwrap();
if (!referendaInfo.isOngoing) {
process.stdout.write("\u2705\n");
break;
}
await new Promise((resolve) => setTimeout(resolve, 1e3));
} catch (e) {
console.error(e);
throw new Error(`Error querying referendum info for proposalId: ${proposalId}`);
}
}
process.stdout.write(`${referendaInfo.isApproved ? "\u2705" : "\u274C"}
`);
if (!referendaInfo.isApproved) {
throw new Error("Finished Referendum was not approved");
}
};
var executeProposalWithCouncil = async (api, encodedHash) => {
let nonce = (await api.rpc.system.accountNextIndex(alith.address)).toNumber();
const referendumNextIndex = (await api.query.democracy.referendumCount()).toNumber();
const callData = api.consts.system.version.specVersion.toNumber() >= 2e3 ? { Legacy: encodedHash } : encodedHash;
const external = api.tx.democracy.externalProposeMajority(callData);
const fastTrack = api.tx.democracy.fastTrack(encodedHash, 1, 0);
const voteAmount = 1n * 10n ** BigInt(api.registry.chainDecimals[0]);
process.stdout.write(`Sending motion + fast-track + vote for ${encodedHash}...`);
await Promise.all([
api.tx.councilCollective.propose(1, external, external.length).signAndSend(alith, { nonce: nonce++ }),
api.tx.techCommitteeCollective.propose(1, fastTrack, fastTrack.length).signAndSend(alith, { nonce: nonce++ }),
api.tx.democracy.vote(referendumNextIndex, {
Standard: {
balance: voteAmount,
vote: { aye: true, conviction: 1 }
}
}).signAndSend(alith, { nonce: nonce++ })
]);
process.stdout.write("\u2705\n");
process.stdout.write(`Waiting for referendum [${referendumNextIndex}] to be executed...`);
let referenda;
while (!referenda) {
try {
referenda = ((await api.query.democracy.referendumInfoOf.entries()).find(
(ref) => ref[1].unwrap().isFinished && api.registry.createType("u32", ref[0].toU8a().slice(-4)).toNumber() === referendumNextIndex
)?.[1]).unwrap();
} catch {
await new Promise((resolve) => setTimeout(resolve, 1e3));
}
}
process.stdout.write(`${referenda.asFinished.approved ? "\u2705" : "\u274C"}
`);
if (!referenda.asFinished.approved) {
throw new Error("Finished Referendum was not approved");
}
};
var cancelReferendaWithCouncil = async (api, refIndex) => {
const proposal = api.tx.democracy.cancelReferendum(refIndex);
const encodedProposal = proposal.method.toHex();
const encodedHash = blake2AsHex(encodedProposal);
let nonce = (await api.rpc.system.accountNextIndex(alith.address)).toNumber();
await api.tx.democracy.notePreimage(encodedProposal).signAndSend(alith, { nonce: nonce++ });
await executeProposalWithCouncil(api, encodedHash);
};
// src/lib/upgradeProcedures.ts
async function upgradeRuntime(api, preferences) {
const options = {
waitMigration: true,
upgradeMethod: "Sudo",
...preferences
};
return new Promise(async (resolve, reject) => {
const log = (text) => {
if (options.logger) {
if (typeof options.logger === "function") {
return options.logger(text);
}
if (typeof options.logger.info === "function") {
return options.logger.info(text);
}
}
return;
};
if (!options.runtimeName) {
throw new Error("'runtimeName' is required to upgrade runtime");
}
if (!options.runtimeTag) {
throw new Error("'runtimeTag' is required to upgrade runtime");
}
if (!options.from) {
throw new Error("'from' is required to upgrade runtime");
}
try {
const code = fs2.readFileSync(
await getRuntimeWasm(options.runtimeName, options.runtimeTag, options.localPath)
).toString();
log("Checking if upgrade is needed...");
const existingCode = await api.rpc.state.getStorage(":code");
if (!existingCode) {
throw "No existing runtime code found";
}
if (existingCode.toString() === code) {
reject(
`Runtime upgrade with same code: ${existingCode.toString().slice(0, 20)} vs ${code.toString().slice(0, 20)}`
);
}
let nonce = (await api.rpc.system.accountNextIndex(options.from.address)).toNumber();
switch (options.upgradeMethod) {
case "Sudo": {
log(
`Sending sudo.setCode (${sha256(Buffer.from(code))} [~${Math.floor(
code.length / 1024
)} kb])...`
);
const isWeightV1 = !api.registry.createType("Weight").proofSize;
await api.tx.sudo.sudoUncheckedWeight(
await api.tx.system.setCodeWithoutChecks(code),
isWeightV1 ? "1" : {
proofSize: 1,
refTime: 1
}
).signAndSend(options.from, { nonce: nonce++ });
log("\u2705");
break;
}
case "Governance": {
log("Using governance...");
const proposal = api.consts.system.version.specVersion.toNumber() >= 2400 ? api.tx.parachainSystem.authorizeUpgrade(blake2AsHex2(code), true) : api.tx.parachainSystem.authorizeUpgrade(blake2AsHex2(code));
const encodedProposal = proposal.method.toHex();
const encodedHash = blake2AsHex2(encodedProposal);
log("Checking if preimage already exists...");
const preImageExists = api.query.preimage && await api.query.preimage.statusFor(encodedHash);
const democracyPreImageExists = !api.query.preimage && await api.query.democracy.preimages(encodedHash);
if (api.query.preimage && preImageExists.isSome && preImageExists.unwrap().isRequested) {
log(`Preimage ${encodedHash} already exists !
`);
} else if (!api.query.preimage && democracyPreImageExists) {
log(`Preimage ${encodedHash} already exists !
`);
} else {
log(
`Registering preimage (${sha256(Buffer.from(code))} [~${Math.floor(
code.length / 1024
)} kb])...`
);
if (api.query.preimage) {
await api.tx.preimage.notePreimage(encodedProposal).signAndSend(options.from, { nonce: nonce++ });
} else {
await api.tx.democracy.notePreimage(encodedProposal).signAndSend(options.from, { nonce: nonce++ });
}
log("Complete \u2705");
}
const referendum = await api.query.democracy.referendumInfoOf.entries();
const referendaIndex = api.query.preimage ? referendum.filter(
(ref) => ref[1].unwrap().isOngoing && ref[1].unwrap().asOngoing.proposal.isLookup && ref[1].unwrap().asOngoing.proposal.asLookup.hash.toHex() === encodedHash
).map(
(ref) => api.registry.createType("u32", ref[0].toU8a().slice(-4)).toNumber()
)?.[0] : referendum.filter(
(ref) => ref[1].unwrap().isOngoing && ref[1].unwrap().asOngoing.proposalHash.toHex() === encodedHash
).map(
(ref) => api.registry.createType("u32", ref[0].toU8a().slice(-4)).toNumber()
)?.[0];
if (referendaIndex !== null && referendaIndex !== void 0) {
log("Vote for upgrade already in referendum, cancelling it.");
await cancelReferendaWithCouncil(api, referendaIndex);
}
await executeProposalWithCouncil(api, encodedHash);
nonce = (await api.rpc.system.accountNextIndex(options.from.address)).toNumber();
log("Enacting authorized upgrade...");
await api.tx.parachainSystem.enactAuthorizedUpgrade(code).signAndSend(options.from, { nonce: nonce++ });
log("Complete \u2705");
break;
}
case "WhiteListedCaller": {
log("Using WhiteListed Caller...");
const proposal = api.tx.parachainSystem.authorizeUpgrade(blake2AsHex2(code), true);
const encodedProposal = proposal.method.toHex();
const encodedHash = blake2AsHex2(encodedProposal);
log("Checking if preimage already exists...");
const preImageExists = api.query.preimage && await api.query.preimage.statusFor(encodedHash);
if (preImageExists.isSome && preImageExists.unwrap().isRequested) {
log(`Preimage ${encodedHash} already exists !
`);
} else {
log(
`Registering preimage (${sha256(Buffer.from(code))} [~${Math.floor(
code.length / 1024
)} kb])...`
);
await api.tx.preimage.notePreimage(encodedProposal).signAndSend(options.from, { nonce: nonce++ });
log("Complete \u2705");
}
const referendum = await api.query.referenda.referendumInfoFor.entries();
const referendaIndex = referendum.filter(
(ref) => ref[1].unwrap().isOngoing && ref[1].unwrap().asOngoing.proposal.isLookup && ref[1].unwrap().asOngoing.proposal.asLookup.hash.toHex() === encodedHash
).map(
(ref) => api.registry.createType("u32", ref[0].toU8a().slice(-4)).toNumber()
)?.[0];
await executeOpenTechCommitteeProposal(api, encodedHash);
break;
}
}
log(`Waiting to apply new runtime (${chalk.red("~4min")})...`);
let isInitialVersion = true;
const unsub = await api.rpc.state.subscribeStorage([":code"], async (newCode) => {
if (!isInitialVersion) {
const blockNumber = (await api.rpc.chain.getHeader()).number.toNumber();
log(
`Complete \u2705 [New Code: ${newCode.toString().slice(0, 5)}...${newCode.toString().slice(-4)} , Old Code:${existingCode.toString().slice(0, 5)}...${existingCode.toString().slice(-4)}] [#${blockNumber}]`
);
unsub();
if (newCode.toString() !== code) {
reject(
`Unexpected new code: ${newCode.toString().slice(0, 20)} vs ${code.toString().slice(0, 20)}`
);
}
if (options.waitMigration) {
const blockToWait = (await api.rpc.chain.getHeader()).number.toNumber() + 1;
await new Promise(async (resolve2) => {
const subBlocks = await api.rpc.chain.subscribeNewHeads(async (header) => {
if (header.number.toNumber() === blockToWait) {
subBlocks();
resolve2(blockToWait);
}
});
});
}
resolve(blockNumber);
}
isInitialVersion = false;
});
} catch (e) {
console.error(e);
console.error("Failed to setCode");
reject(e);
}
});
}
// src/lib/globalContext.ts
import "@moonbeam-network/api-augment";
import zombie from "@zombienet/orchestrator";
import { createLogger as createLogger5 } from "@moonwall/util";
import fs12 from "fs";
import net2 from "net";
import readline from "readline";
import { setTimeout as timer3 } from "timers/promises";
import path10 from "path";
// src/internal/commandParsers.ts
import chalk2 from "chalk";
import path3 from "path";
// src/lib/repoDefinitions/moonbeam.ts
var repo = {
name: "moonbeam",
binaries: [
{
name: "moonbeam",
defaultArgs: [
"--no-hardware-benchmarks",
"--no-telemetry",
"--reserved-only",
"--rpc-cors=all",
"--unsafe-rpc-external",
"--unsafe-force-node-key-generation",
"--no-grandpa",
"--sealing=manual",
"--force-authoring",
"--no-prometheus",
"--alice",
"--chain=moonbase-dev",
"--tmp"
]
},
{ name: "moonbase-runtime" },
{ name: "moonbeam-runtime" },
{ name: "moonriver-runtime" }
],
ghAuthor: "moonbeam-foundation",
ghRepo: "moonbeam"
};
var moonbeam_default = repo;
// src/lib/repoDefinitions/polkadot.ts
var repo2 = {
name: "polkadot",
binaries: [
{ name: "polkadot" },
{ name: "polkadot-prepare-worker" },
{ name: "polkadot-execute-worker" }
],
ghAuthor: "paritytech",
ghRepo: "polkadot-sdk"
};
var polkadot_default = repo2;
// src/lib/repoDefinitions/tanssi.ts
var repo3 = {
name: "tanssi",
binaries: [
{
name: "tanssi-node",
defaultArgs: ["--dev", "--sealing=manual", "--no-hardware-benchmarks"]
},
{ name: "container-chain-template-simple-node" },
{ name: "container-chain-template-frontier-node" }
],
ghAuthor: "moondance-labs",
ghRepo: "tanssi"
};
var tanssi_default = repo3;
// src/lib/repoDefinitions/index.ts
init_configReader();
function standardRepos() {
const defaultRepos = [moonbeam_default, polkadot_default, tanssi_default];
return [...defaultRepos];
}
// src/internal/commandParsers.ts
import invariant from "tiny-invariant";
function parseZombieCmd(launchSpec) {
if (launchSpec) {
return { cmd: launchSpec.configPath };
}
throw new Error(
`No ZombieSpec found in config.
Are you sure your ${chalk2.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "configPath" in zombieSpec?`
);
}
function fetchDefaultArgs(binName, additionalRepos = []) {
let defaultArgs;
const repos = [...standardRepos(), ...additionalRepos];
for (const repo4 of repos) {
const foundBin = repo4.binaries.find((bin) => bin.name === binName);
if (foundBin) {
defaultArgs = foundBin.defaultArgs;
break;
}
}
if (!defaultArgs) {
defaultArgs = ["--dev"];
}
return defaultArgs;
}
var LaunchCommandParser = class _LaunchCommandParser {
args;
cmd;
launch;
launchSpec;
additionalRepos;
launchOverrides;
constructor(options) {
const { launchSpec, additionalRepos, launchOverrides } = options;
this.launchSpec = launchSpec;
this.additionalRepos = additionalRepos;
this.launchOverrides = launchOverrides;
this.launch = !launchSpec.running ? true : launchSpec.running;
this.cmd = launchSpec.binPath;
this.args = launchSpec.options ? [...launchSpec.options] : fetchDefaultArgs(path3.basename(launchSpec.binPath), additionalRepos);
}
overrideArg(newArg) {
const newArgKey = newArg.split("=")[0];
const existingIndex = this.args.findIndex((arg) => arg.startsWith(`${newArgKey}=`));
if (existingIndex !== -1) {
this.args[existingIndex] = newArg;
} else {
this.args.push(newArg);
}
}
withPorts() {
if (this.launchSpec.ports) {
const ports = this.launchSpec.ports;
if (ports.p2pPort) {
this.overrideArg(`--port=${ports.p2pPort}`);
}
if (ports.wsPort) {
this.overrideArg(`--ws-port=${ports.wsPort}`);
}
if (ports.rpcPort) {
this.overrideArg(`--rpc-port=${ports.rpcPort}`);
}
} else {
const freePort = getFreePort().toString();
process.env.MOONWALL_RPC_PORT = freePort;
if (this.launchSpec.newRpcBehaviour) {
this.overrideArg(`--rpc-port=${freePort}`);
} else {
this.overrideArg(`--ws-port=${freePort}`);
}
}
return this;
}
withDefaultForkConfig() {
const forkOptions = this.launchSpec.defaultForkConfig;
if (forkOptions) {
this.applyForkOptions(forkOptions);
}
return this;
}
withLaunchOverrides() {
if (this.launchOverrides?.forkConfig) {
this.applyForkOptions(this.launchOverrides.forkConfig);
}
return this;
}
print() {
console.log(chalk2.cyan(`Command to run is: ${chalk2.bold(this.cmd)}`));
console.log(chalk2.cyan(`Arguments are: ${chalk2.bold(this.args.join(" "))}`));
return this;
}
applyForkOptions(forkOptions) {
if (forkOptions.url) {
invariant(forkOptions.url.startsWith("http"), "Fork URL must start with http:// or https://");
this.overrideArg(`--fork-chain-from-rpc=${forkOptions.url}`);
}
if (forkOptions.blockHash) {
this.overrideArg(`--block=${forkOptions.blockHash}`);
}
if (forkOptions.stateOverridePath) {
this.overrideArg(`--fork-state-overrides=${forkOptions.stateOverridePath}`);
}
if (forkOptions.verbose) {
this.overrideArg("-llazy-loading=trace");
}
}
build() {
return {
cmd: this.cmd,
args: this.args,
launch: this.launch
};
}
static create(options) {
const parser = new _LaunchCommandParser(options);
const parsed = parser.withPorts().withDefaultForkConfig().withLaunchOverrides();
if (options.verbose) {
parsed.print();
}
return parsed.build();
}
};
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.cjs",
`--config=${launchSpecs[0].configPath}`,
`--addr=${launchSpecs[0].address ?? "127.0.0.1"}`
// use old behaviour by default
];
const mode = launchSpecs[0].buildBlockMode ? launchSpecs[0].buildBlockMode : "manual";
const num = mode === "batch" ? "Batch" : mode === "instant" ? "Instant" : "Manual";
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}`);
}
if (launchSpecs[0].allowUnresolvedImports) {
chopsticksArgs2.push("--allow-unresolved-imports");
}
return {
cmd: chopsticksCmd2,
args: chopsticksArgs2,
launch
};
}
const chopsticksCmd = "node";
const chopsticksArgs = ["node_modules/@acala-network/chopsticks/chopsticks.cjs", "xcm"];
for (const spec of launchSpecs) {
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
};
}
var getFreePort = () => {
const notionalPort = 1e4 + Number(process.env.VITEST_POOL_ID || 1) * 100;
return notionalPort;
};
// src/internal/foundations/zombieHelpers.ts
import chalk4 from "chalk";
import fs4 from "fs";
import invariant2 from "tiny-invariant";
// src/internal/fileCheckers.ts
import fs3 from "fs";
import { execSync } from "child_process";
import chalk3 from "chalk";
import os from "os";
import path4 from "path";
import { select } from "@inquirer/prompts";
async function checkExists(path11) {
const binPath = path11.split(" ")[0];
const fsResult = fs3.existsSync(binPath);
if (!fsResult) {
throw new Error(
`No binary file found at location: ${binPath}
Are you sure your ${chalk3.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "binPath" in launchSpec?`
);
}
const binArch = await getBinaryArchitecture(binPath);
const currentArch = os.arch();
if (binArch !== currentArch && binArch !== "unknown") {
throw new Error(
`The binary architecture ${chalk3.bgWhiteBright.blackBright(
binArch
)} does not match this system's architecture ${chalk3.bgWhiteBright.blackBright(
currentArch
)}
Download or compile a new binary executable for ${chalk3.bgWhiteBright.blackBright(
currentArch
)} `
);
}
return true;
}
function checkAccess(path11) {
const binPath = path11.split(" ")[0];
try {
fs3.accessSync(binPath, fs3.constants.X_OK);
} catch (err) {
console.error(`The file ${binPath} is not executable`);
throw new Error(`The file at ${binPath} , lacks execute permissions.`);
}
}
async function getBinaryArchitecture(filePath) {
return new Promise((resolve, reject) => {
const architectureMap = {
0: "unknown",
3: "x86",
62: "x64",
183: "arm64"
};
fs3.open(filePath, "r", (err, fd) => {
if (err) {
reject(err);
return;
}
const buffer = Buffer.alloc(20);
fs3.read(fd, buffer, 0, 20, 0, (err2, bytesRead, buffer2) => {
if (err2) {
reject(err2);
return;
}
const e_machine = buffer2.readUInt16LE(18);
const architecture = architectureMap[e_machine] || "unknown";
resolve(architecture);
});
});
});
}
// src/internal/foundations/zombieHelpers.ts
import { setTimeout as timer } from "timers/promises";
import net from "net";
async function checkZombieBins(config) {
const relayBinPath = config.relaychain.default_command;
if (!relayBinPath) {
throw new Error("No relayBinPath '[relaychain.default_command]' specified in zombie config");
}
await checkExists(relayBinPath);
checkAccess(relayBinPath);
if (config.parachains) {
const promises = config.parachains.map((para) => {
if (para.collator) {
if (!para.collator.command) {
throw new Error(
"No command found for collator, please check your zombienet config file for collator command"
);
}
checkExists(para.collator.command);
checkAccess(para.collator.command);
}
if (para.collators) {
for (const coll of para.collators) {
if (!coll.command) {
throw new Error(
"No command found for collators, please check your zombienet config file for collators command"
);
}
checkExists(coll.command);
checkAccess(coll.command);
}
}
});
await Promise.all(promises);
}
}
function getZombieConfig(path11) {
const fsResult = fs4.existsSync(path11);
if (!fsResult) {
throw new Error(
`No ZombieConfig file found at location: ${path11}
Are you sure your ${chalk4.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "configPath" in zombieSpec?`
);
}
const buffer = fs4.readFileSync(path11, "utf-8");
return JSON.parse(buffer);
}
async function sendIpcMessage(message) {
return new Promise(async (resolve, reject) => {
let response;
const ipcPath = process.env.MOON_IPC_SOCKET;
invariant2(ipcPath, "No IPC path found. This is a bug, please report it.");
const client = net.createConnection({ path: ipcPath }, () => {
console.log("\u{1F4E8} Successfully connected to IPC server");
});
client.on("error", (err) => {
console.error("\u{1F4E8} IPC client connection error:", err);
});
client.on("data", async (data) => {
response = JSON.parse(data.toString());
if (response.status === "success") {
client.end();
for (let i = 0; ; i++) {
if (client.closed) {
break;
}
if (i > 100) {
reject(new Error("Closing IPC connection failed"));
}
await timer(200);
}
resolve(response);
}
if (response.status === "failure") {
reject(new Error(JSON.stringify(response)));
}
});
for (let i = 0; ; i++) {
if (!client.connecting) {
break;
}
if (i > 100) {
reject(new Error(`Connection to ${ipcPath} failed`));
}
await timer(200);
}
await new Promise((resolve2) => {
client.write(JSON.stringify(message), () => resolve2("Sent!"));
});
});
}
// src/internal/localNode.ts
import { exec, spawn, spawnSync } from "child_process";
import fs5 from "fs";
import path5 from "path";
import WebSocket from "ws";
import { createLogger } from "@moonwall/util";
import { setTimeout as timer2 } from "timers/promises";
import util from "util";
import Docker from "dockerode";
import invariant3 from "tiny-invariant";
var execAsync = util.promisify(exec);
var logger = createLogger({ name: "localNode" });
var debug = logger.debug.bind(logger);
async function launchDockerContainer(imageName, args, name, dockerConfig) {
const docker = new Docker();
const port = args.find((a) => a.includes("port"))?.split("=")[1];
debug(`\x1B[36mStarting Docker container ${imageName} on port ${port}...\x1B[0m`);
const dirPath = path5.join(process.cwd(), "tmp", "node_logs");
const logLocation = path5.join(dirPath, `${name}_docker_${Date.now()}.log`);
const fsStream = fs5.createWriteStream(logLocation);
process.env.MOON_LOG_LOCATION = logLocation;
const portBindings = dockerConfig?.exposePorts?.reduce(
(acc, { hostPort, internalPort }) => {
acc[`${internalPort}/tcp`] = [{ HostPort: hostPort.toString() }];
return acc;
},
{}
);
const rpcPort = args.find((a) => a.includes("rpc-port"))?.split("=")[1];
invariant3(rpcPort, "RPC port not found, this is a bug");
const containerOptions = {
Image: imageName,
platform: "linux/amd64",
Cmd: args,
name: dockerConfig?.containerName || `moonwall_${name}_${Date.now()}`,
ExposedPorts: {
...Object.fromEntries(
dockerConfig?.exposePorts?.map(({ internalPort }) => [`${internalPort}/tcp`, {}]) || []
),
[`${rpcPort}/tcp`]: {}
},
HostConfig: {
PortBindings: {
...portBindings,
[`${rpcPort}/tcp`]: [{ HostPort: rpcPort }]
}
},
Env: dockerConfig?.runArgs?.filter((arg) => arg.startsWith("env:")).map((arg) => arg.slice(4))
};
try {
await pullImage(imageName, docker);
const container = await docker.createContainer(containerOptions);
await container.start();
const containerInfo = await container.inspect();
if (!containerInfo.State.Running) {
const errorMessage = `Container failed to start: ${containerInfo.State.Error}`;
console.error(errorMessage);
fs5.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
for (let i = 0; i < 300; i++) {
if (await checkWebSocketJSONRPC(Number.parseInt(rpcPort))) {
break;
}
await timer2(100);
}
return { runningNode: container, fsStream };
} catch (error) {
if (error instanceof Error) {
console.error(`Docker container launch failed: ${error.message}`);
fs5.appendFileSync(logLocation, `Docker launch error: ${error.message}
`);
}
throw error;
}
}
async function launchNode(options) {
const { command: cmd, args, name, launchSpec: config } = options;
if (config?.useDocker) {
return launchDockerContainer(cmd, args, name, config.dockerConfig);
}
if (cmd.includes("moonbeam")) {
await checkExists(cmd);
checkAccess(cmd);
}
const port = args.find((a) => a.includes("port"))?.split("=")[1];
debug(`\x1B[36mStarting ${name} node on port ${port}...\x1B[0m`);
const dirPath = path5.join(process.cwd(), "tmp", "node_logs");
const runningNode = spawn(cmd, args);
const logLocation = path5.join(
dirPath,
`${path5.basename(cmd)}_node_${args.find((a) => a.includes("port"))?.split("=")[1]}_${runningNode.pid}.log`
).replaceAll("node_node_undefined", "chopsticks");
process.env.MOON_LOG_LOCATION = logLocation;
const fsStream = fs5.createWriteStream(logLocation);
runningNode.on("error", (err) => {
if (err.errno === "ENOENT") {
console.error(`\x1B[31mMissing Local binary at(${cmd}).
Please compile the project\x1B[0m`);
}
throw new Error(err.message);
});
const logHandler = (chunk) => {
if (fsStream.writable) {
fsStream.write(chunk, (err) => {
if (err) console.error(err);
else fsStream.emit("drain");
});
}
};
runningNode.stderr?.on("data", logHandler);
runningNode.stdout?.on("data", logHandler);
runningNode.once("exit", (code, signal) => {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
let message;
const moonwallNode = runningNode;
if (moonwallNode.isMoonwallTerminating) {
message = `${timestamp} [moonwall] process killed. reason: ${moonwallNode.moonwallTerminationReason || "unknown"}`;
} else if (code !== null) {
message = `${timestamp} [moonwall] process exited with status code ${code}`;
} else if (signal !== null) {
message = `${timestamp} [moonwall] process terminated by signal ${signal}`;
} else {
message = `${timestamp} [moonwall] process terminated unexpectedly`;
}
if (fsStream.writable) {
fsStream.write(`${message}
`, (err) => {
if (err) console.error(`Failed to write exit message to log: ${err}`);
fsStream.end();
});
} else {
try {
fs5.appendFileSync(logLocation, `${message}
`);
} catch (err) {
console.error(`Failed to append exit message to log file: ${err}`);
}
fsStream.end();
}
runningNode.stderr?.removeListener("data", logHandler);
runningNode.stdout?.removeListener("data", logHandler);
});
if (!runningNode.pid) {
const errorMessage = "Failed to start child process";
console.error(errorMessage);
fs5.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
if (runningNode.exitCode !== null) {
const errorMessage = `Child process exited immediately with code ${runningNode.exitCode}`;
console.error(errorMessage);
fs5.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
const isRunning = await isPidRunning(runningNode.pid);
if (!isRunning) {
const errorMessage = `Process with PID ${runningNode.pid} is not running`;
spawnSync(cmd, args, { stdio: "inherit" });
throw new Error(errorMessage);
}
probe: for (let i = 0; ; i++) {
try {
const ports = await findPortsByPid(runningNode.pid);
if (ports) {
for (const port2 of ports) {
try {
await checkWebSocketJSONRPC(port2);
break probe;
} catch {
}
}
}
} catch {
if (i === 300) {
throw new Error("Could not find ports for node after 30 seconds");
}
await timer2(100);
continue;
}
await timer2(100);
}
return { runningNode, fsStream };
}
function isPidRunning(pid) {
return new Promise((resolve) => {
exec(`ps -p ${pid} -o pid=`, (error, stdout, stderr) => {
if (error) {
resolve(false);
} else {
resolve(stdout.trim() !== "");
}
});
});
}
async function checkWebSocketJSONRPC(port) {
try {
const ws = new WebSocket(`ws://localhost:${port}`);
const result = await new Promise((resolve) => {
ws.on("open", () => {
ws.send(
JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "system_chain",
params: []
})
);
});
ws.on("message", (data) => {
try {
const response = JSON.parse(data.toString());
if (response.jsonrpc === "2.0" && response.id === 1) {
resolve(true);
} else {
resolve(false);
}
} catch (e) {
resolve(false);
}
});
ws.on("error", () => {
resolve(false);
});
});
ws?.close();
return result;
} catch {
return false;
}
}
async function findPortsByPid(pid, retryCount = 600, retryDelay = 100) {
for (let i = 0; i < retryCount; i++) {
try {
const { stdout } = await execAsync(`lsof -p ${pid} -n -P | grep LISTEN`);
const ports = [];
const lines = stdout.split("\n");
for (const line of lines) {
const regex = /(?:.+):(\d+)/;
const match = line.match(regex);
if (match) {
ports.push(Number(match[1]));
}
}
if (ports.length) {
return ports;
}
throw new Error("Could not find any ports");
} catch (error) {
if (i === retryCount - 1) {
throw error;
}
}
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
return [];
}
async function pullImage(imageName, docker) {
console.log(`Pulling Docker image: ${imageName}`);
const pullStream = await docker.pull(imageName);
await new Promise((resolve, reject) => {
docker.modem.followProgress(pullStream, (err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
// src/internal/providerFactories.ts
import { ALITH_PRIVATE_KEY, deriveViemChain } from "@moonwall/util";
import { ApiPromise, WsProvider } from "@polkadot/api";
import { Wallet, ethers } from "ethers";
import { createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { Web3 } from "web3";
import { WebSocketProvider } from "web3-providers-ws";
import { createClient } from "polkadot-api";
import { getWsProvider, WsEvent } from "polkadot-api/ws-provider/web";
import { createLogger as createLogger2 } from "@moonwall/util";
var logger2 = createLogger2({ name: "providers" });
var debug2 = logger2.debug.bind(logger2);
var ProviderFactory = class _ProviderFactory {
constructor(providerConfig) {
this.providerConfig = providerConfig;
this.url = providerConfig.endpoints.includes("ENV_VAR") ? process.env.WSS_URL || "error_missing_WSS_URL_env_var" : providerConfig.endpoints[0];
this.privateKey = process.env.MOON_PRIV_KEY || ALITH_PRIVATE_KEY;
}
url;
privateKey;
create() {
switch (this.providerConfig.type) {
case "polkadotJs":
return this.createPolkadotJs();
case "web3":
return this.createWeb3();
case "ethers":
return this.createEthers();
case "viem":
return this.createViem();
case "papi":
return this.createPapi();
default:
return this.createDefault();
}
}
createPolkadotJs() {
debug2(`\u{1F7E2} PolkadotJs provider ${this.providerConfig.name} details prepared`);
return {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: async () => {
process.env.DEFAULT_TIMEOUT_MS = "30000";
const options = {
provider: new WsProvider(this.url),
initWasm: false,
noInitWarn: true,
isPedantic: false,
rpc: this.providerConfig.rpc ? this.providerConfig.rpc : void 0,
typesBundle: this.providerConfig.additionalTypes ? this.providerConfig.additionalTypes : void 0
};
const api = await ApiPromise.create(options);
await api.isReady;
return api;
},
ws: () => new WsProvider(this.url)
};
}
createWeb3() {
debug2(`\u{1F7E2} Web3 provider ${this.providerConfig.name} details prepared`);
return {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: () => {
const provider = new WebSocketProvider(
this.url,
{},
{ delay: 50, autoReconnect: false, maxAttempts: 10 }
);
return new Web3(provider);
}
};
}
createEthers() {
debug2(`\u{1F7E2} Ethers provider ${this.providerConfig.name} details prepared`);
return {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: () => {
const provider = this.url.startsWith("ws") ? new ethers.WebSocketProvider(this.url) : new ethers.JsonRpcProvider(this.url);
return new Wallet(this.privateKey, provider);
}
};
}
createViem() {
debug2(`\u{1F7E2} Viem omni provider ${this.providerConfig.name} details prepared`);
return {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: async () => {
const client = createWalletClient({
chain: await deriveViemChain(this.url),
account: privateKeyToAccount(this.privateKey),
transport: http(this.url.replace("ws", "http"))
}).extend(publicActions);
return client;
}
};
}
createPapi() {
debug2(`\u{1F7E2} Papi provider ${this.providerConfig.name} details prepared`);
return {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: () => {
const provider = getWsProvider(this.url, (status) => {
switch (status.type) {
case WsEvent.CONNECTING:
console.log("Connecting... \u{1F50C}");
break;
case WsEvent.CONNECTED:
console.log("Connected! \u26A1");
break;
case WsEvent.ERROR:
console.log("Errored... \u{1F622}");
break;
case WsEvent.CLOSE:
console.log("Closed \u{1F6AA}");
break;
}
});
return createClient(provider);
}
};
}
createDefault() {
debug2(`\u{1F7E2} Default provider ${this.providerConfig.name} details prepared`);
return {
name: this.providerConfig.name,
type: this.providerConfig.type,
connect: () => {
console.log(`\u{1F6A7} provider ${this.providerConfig.name} not yet implemented`);
return null;
}
};
}
static prepare(providerConfigs) {
return providerConfigs.map((providerConfig) => new _ProviderFactory(providerConfig).create());
}
static prepareDefaultDev() {
return _ProviderFactory.prepare([
{
name: "dev",
type: "polkadotJs",
endpoints: [vitestAutoUrl()]
},
{
name: "w3",
type: "web3",
endpoints: [vitestAutoUrl()]
},
{
name: "eth",
type: "ethers",
endpoints: [vitestAutoUrl()]
},
{
name: "public",
type: "viem",
endpoints: [vitestAutoUrl()]
}
]);
}
static prepareDefaultZombie() {
const MOON_PARA_WSS = process.env.MOON_PARA_WSS || "error";
const MOON_RELAY_WSS = process.env.MOON_RELAY_WSS || "error";
const providers = [
{
name: "w3",
type: "web3",
endpoints: [MOON_PARA_WSS]
},
{
name: "eth",
type: "ethers",
endpoints: [MOON_PARA_WSS]
},
{
name: "viem",
type: "viem",
endpoints: [MOON_PARA_WSS]
},
{
name: "relaychain",
type: "polkadotJs",
endpoints: [MOON_RELAY_WSS]
}
];
if (MOON_PARA_WSS !== "error") {
providers.push({
name: "parachain",
type: "polkadotJs",
endpoints: [MOON_PARA_WSS]
});
}
return _ProviderFactory.prepare(providers);
}
static prepareNoEthDefaultZombie() {
const MOON_PARA_WSS = process.env.MOON_PARA_WSS || "error";
const MOON_RELAY_WSS = process.env.MOON_RELAY_WSS || "error";
const providers = [
{
name: "relaychain",
type: "polkadotJs",
endpoints: [MOON_RELAY_WSS]
}
];
if (MOON_PARA_WSS !== "error") {
providers.push({
name: "parachain",
type: "polkadotJs",
endpoints: [MOON_PARA_WSS]
});
}
return _ProviderFactory.prepare(providers);
}
};
var ProviderInterfaceFactory = class _ProviderInterfaceFactory {
constructor(name, type, connect) {
this.name = name;
this.type = type;
this.connect = connect;
}
async create() {
switch (this.type) {
case "polkadotJs":
return this.createPolkadotJs();
case "web3":
return this.createWeb3();
case "ethers":
return this.createEthers();
case "viem":
return this.createViem();
case "papi":
return this.createPapi();
default:
throw new Error("UNKNOWN TYPE");
}
}
async createPolkadotJs() {
debug2(`\u{1F50C} Connecting PolkadotJs provider: ${this.name}`);
const api = await this.connect();
debug2(`\u2705 PolkadotJs provider ${this.name} connected`);
1;
return {
name: this.name,
api,
type: "polkadotJs",
greet: