UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

1,487 lines (1,470 loc) 81.7 kB
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: