UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

1,360 lines (1,331 loc) 155 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 { 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 env = getEnvironmentFromConfig(); return env.foundation.type === "zombie" && !env.foundation.zombieSpec.disableDefaultEthProviders; } function isEthereumDevConfig() { 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.hasOwn(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 { 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 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 { 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 { createLogger as createLogger15 } from "@moonwall/util"; import zombie from "@zombienet/orchestrator"; import Docker4 from "dockerode"; import { ChildProcess, exec as exec3, execSync as execSync4 } from "child_process"; import fs17 from "fs"; import net3 from "net"; import path13 from "path"; import readline from "readline"; import { setTimeout as timer4 } from "timers/promises"; import { promisify as promisify3 } from "util"; import invariant5 from "tiny-invariant"; // src/internal/logging.ts var originalWrite = process.stderr.write.bind(process.stderr); var blockList = [ "has multiple versions, ensure that there is only one installed", "Unable to map [u8; 32] to a lookup index", "Either remove and explicitly install matching versions or dedupe using your package manager." ]; process.stderr.write = (chunk, encodingOrCallback, callback) => { let shouldWrite = true; if (typeof chunk === "string") { shouldWrite = !blockList.some((phrase) => chunk.includes(phrase)); } if (shouldWrite) { if (typeof encodingOrCallback === "function") { return originalWrite.call(process.stderr, chunk, void 0, encodingOrCallback); } return originalWrite.call(process.stderr, chunk, encodingOrCallback, callback); } const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback; if (cb) cb(null); return true; }; // src/internal/cmdFunctions/downloader.ts import { SingleBar, Presets } from "cli-progress"; import fs3 from "fs"; import { Readable } from "stream"; // src/internal/cmdFunctions/fetchArtifact.ts import fs4 from "fs/promises"; import path3 from "path"; import semver from "semver"; import chalk2 from "chalk"; // src/internal/processHelpers.ts import child_process2 from "child_process"; import { promisify } from "util"; import { createLogger } from "@moonwall/util"; var logger = createLogger({ name: "actions:runner" }); var debug = logger.debug.bind(logger); var execAsync = promisify(child_process2.exec); var withTimeout = (promise, ms) => { return Promise.race([ promise, new Promise( (_, reject) => setTimeout(() => reject(new Error("Operation timed out")), ms) ) ]); }; // src/internal/cmdFunctions/fetchArtifact.ts import { minimatch } from "minimatch"; // 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", "--database=paritydb", "--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/cmdFunctions/fetchArtifact.ts init_configReader(); import { execSync } from "child_process"; import { Octokit } from "@octokit/rest"; import { confirm } from "@inquirer/prompts"; var octokit = new Octokit({ baseUrl: "https://api.github.com", log: { debug: () => { }, info: () => { }, warn: console.warn, error: console.error } }); // src/internal/cmdFunctions/initialisation.ts import fs5 from "fs/promises"; import { confirm as confirm2, input, number } from "@inquirer/prompts"; // src/internal/cmdFunctions/tempLogs.ts import path4 from "path"; import fs6 from "fs"; // src/internal/commandParsers.ts import { createLogger as createLogger3 } from "@moonwall/util"; import path5 from "path"; import net from "net"; import { Effect as Effect3 } from "effect"; // src/lib/shardManager.ts var ShardManager = class _ShardManager { static instance = null; _shardInfo = null; constructor() { } static getInstance() { if (!_ShardManager.instance) { _ShardManager.instance = new _ShardManager(); } return _ShardManager.instance; } /** * Initialize shard configuration from command line argument or environment * @param shardArg Optional shard argument from CLI (format: "current/total") */ initializeSharding(shardArg) { if (shardArg) { this._shardInfo = this.parseShardString(shardArg); process.env.MOONWALL_TEST_SHARD = shardArg; } else if (process.env.MOONWALL_TEST_SHARD) { this._shardInfo = this.parseShardString(process.env.MOONWALL_TEST_SHARD); } else { this._shardInfo = { current: 1, total: 1, isSharded: false }; } } /** * Get current shard information */ getShardInfo() { if (!this._shardInfo) { this.initializeSharding(); } return this._shardInfo; } /** * Get shard index (0-based) for port calculations */ getShardIndex() { return this.getShardInfo().current - 1; } /** * Get total number of shards */ getTotalShards() { return this.getShardInfo().total; } /** * Check if sharding is enabled */ isSharded() { return this.getShardInfo().isSharded; } /** * Reset shard configuration (mainly for testing) */ reset() { this._shardInfo = null; delete process.env.MOONWALL_TEST_SHARD; } parseShardString(shardString) { if (!shardString.includes("/")) { throw new Error( `Invalid shard format: "${shardString}". Expected format: "current/total" (e.g., "1/3")` ); } const [currentStr, totalStr] = shardString.split("/"); const current = parseInt(currentStr, 10); const total = parseInt(totalStr, 10); if (Number.isNaN(current) || Number.isNaN(total) || current < 1 || total < 1) { throw new Error( `Invalid shard numbers in "${shardString}". Both current and total must be positive integers.` ); } if (current > total) { throw new Error( `Invalid shard configuration: current shard ${current} cannot be greater than total ${total}` ); } const isSharded = total > 1; return { current, total, isSharded }; } }; var shardManager = ShardManager.getInstance(); // src/internal/commandParsers.ts import invariant from "tiny-invariant"; // src/internal/effect/StartupCacheService.ts import { Command, FileSystem as FileSystem2, Path } from "@effect/platform"; import { NodeContext } from "@effect/platform-node"; import { createLogger as createLogger2 } from "@moonwall/util"; import { Context, Duration as Duration2, Effect as Effect2, Layer, Option, Stream } from "effect"; import * as crypto from "crypto"; // src/internal/effect/errors.ts import { Data } from "effect"; var PortDiscoveryError = class extends Data.TaggedError("PortDiscoveryError") { }; var NodeLaunchError = class extends Data.TaggedError("NodeLaunchError") { }; var NodeReadinessError = class extends Data.TaggedError("NodeReadinessError") { }; var ProcessError = class extends Data.TaggedError("ProcessError") { }; var StartupCacheError = class extends Data.TaggedError("StartupCacheError") { }; var FileLockError = class extends Data.TaggedError("FileLockError") { }; // src/internal/effect/FileLock.ts import { FileSystem } from "@effect/platform"; import { Duration, Effect, Schedule } from "effect"; import * as os from "os"; var LOCK_MAX_AGE = Duration.minutes(2); var LOCK_POLL_INTERVAL = Duration.millis(500); var isProcessAlive = (pid) => Effect.try(() => { process.kill(pid, 0); return true; }).pipe(Effect.orElseSucceed(() => false)); var isLockStale = (info) => Effect.gen(function* () { const isTimedOut = Date.now() - info.timestamp > Duration.toMillis(LOCK_MAX_AGE); if (isTimedOut) return true; const isSameHost = info.hostname === os.hostname(); if (!isSameHost) return false; const alive = yield* isProcessAlive(info.pid); return !alive; }); var cleanupStaleLock = (lockPath) => Effect.gen(function* () { const fs18 = yield* FileSystem.FileSystem; const infoPath = `${lockPath}/lock.json`; const exists = yield* fs18.exists(infoPath).pipe(Effect.orElseSucceed(() => false)); if (!exists) return; const content = yield* fs18.readFileString(infoPath).pipe(Effect.orElseSucceed(() => "")); const info = yield* Effect.try(() => JSON.parse(content)).pipe( Effect.orElseSucceed(() => null) ); if (!info) return; const stale = yield* isLockStale(info); if (stale) { yield* fs18.remove(lockPath, { recursive: true }).pipe(Effect.ignore); } }); var writeLockInfo = (lockPath) => Effect.gen(function* () { const fs18 = yield* FileSystem.FileSystem; const info = { pid: process.pid, timestamp: Date.now(), hostname: os.hostname() }; yield* fs18.writeFileString(`${lockPath}/lock.json`, JSON.stringify(info)).pipe(Effect.ignore); }); var tryAcquireLock = (lockPath) => Effect.gen(function* () { const fs18 = yield* FileSystem.FileSystem; yield* cleanupStaleLock(lockPath); yield* fs18.makeDirectory(lockPath).pipe(Effect.mapError(() => new FileLockError({ reason: "acquisition_failed", lockPath }))); yield* writeLockInfo(lockPath); }); var acquireFileLock = (lockPath, timeout = Duration.minutes(2)) => tryAcquireLock(lockPath).pipe( Effect.retry(Schedule.fixed(LOCK_POLL_INTERVAL).pipe(Schedule.upTo(timeout))), Effect.catchAll(() => Effect.fail(new FileLockError({ reason: "timeout", lockPath }))) ); var releaseFileLock = (lockPath) => Effect.gen(function* () { const fs18 = yield* FileSystem.FileSystem; yield* fs18.remove(lockPath, { recursive: true }).pipe(Effect.ignore); }); var withFileLock = (lockPath, effect, timeout = Duration.minutes(2)) => Effect.acquireUseRelease( acquireFileLock(lockPath, timeout), () => effect, () => releaseFileLock(lockPath) ); // src/internal/effect/StartupCacheService.ts var logger2 = createLogger2({ name: "StartupCacheService" }); var StartupCacheService = class extends Context.Tag("StartupCacheService")() { }; var hashFile = (filePath) => Effect2.gen(function* () { const fs18 = yield* FileSystem2.FileSystem; const hash = crypto.createHash("sha256"); yield* fs18.stream(filePath).pipe(Stream.runForEach((chunk) => Effect2.sync(() => hash.update(chunk)))); return hash.digest("hex"); }).pipe(Effect2.mapError((cause) => new StartupCacheError({ cause, operation: "hash" }))); var findPrecompiledWasm = (dir) => Effect2.gen(function* () { const fs18 = yield* FileSystem2.FileSystem; const pathService = yield* Path.Path; const exists = yield* fs18.exists(dir); if (!exists) return Option.none(); const files = yield* fs18.readDirectory(dir).pipe(Effect2.orElseSucceed(() => [])); const wasmFile = files.find( (f) => f.startsWith("precompiled_wasm_") || f.endsWith(".cwasm") || f.endsWith(".wasm") ); return wasmFile ? Option.some(pathService.join(dir, wasmFile)) : Option.none(); }).pipe(Effect2.catchAll(() => Effect2.succeed(Option.none()))); var checkCache = (cacheDir, hashPath, expectedHash) => Effect2.gen(function* () { const fs18 = yield* FileSystem2.FileSystem; const savedHash = yield* fs18.readFileString(hashPath).pipe(Effect2.orElseSucceed(() => "")); if (savedHash.trim() !== expectedHash) return Option.none(); const wasmPath = yield* findPrecompiledWasm(cacheDir); if (Option.isNone(wasmPath)) return Option.none(); const accessible = yield* fs18.access(wasmPath.value).pipe( Effect2.as(true), Effect2.orElseSucceed(() => false) ); return accessible ? wasmPath : Option.none(); }); var runPrecompile = (binPath, chainArg, outputDir) => Effect2.gen(function* () { const fs18 = yield* FileSystem2.FileSystem; const pathService = yield* Path.Path; const args = chainArg ? ["precompile-wasm", chainArg, outputDir] : ["precompile-wasm", outputDir]; logger2.debug(`Precompiling: ${binPath} ${args.join(" ")}`); const startTime = Date.now(); const exitCode = yield* Command.exitCode(Command.make(binPath, ...args)).pipe( Effect2.mapError( (e) => new StartupCacheError({ cause: e, operation: "precompile" }) ) ); const files = yield* fs18.readDirectory(outputDir).pipe(Effect2.mapError((e) => new StartupCacheError({ cause: e, operation: "precompile" }))); const wasmFile = files.find( (f) => f.startsWith("precompiled_wasm_") || f.endsWith(".cwasm") || f.endsWith(".wasm") ); if (!wasmFile) { return yield* Effect2.fail( new StartupCacheError({ cause: `precompile-wasm failed (code ${exitCode}): no WASM file generated`, operation: "precompile" }) ); } const wasmPath = pathService.join(outputDir, wasmFile); logger2.debug(`Precompiled in ${Date.now() - startTime}ms: ${wasmPath}`); return wasmPath; }); var generateRawChainSpec = (binPath, chainName, outputPath) => Effect2.gen(function* () { const fs18 = yield* FileSystem2.FileSystem; const args = chainName === "dev" || chainName === "default" ? ["build-spec", "--dev", "--raw"] : ["build-spec", `--chain=${chainName}`, "--raw"]; logger2.debug(`Generating raw chain spec: ${binPath} ${args.join(" ")}`); const stdout = yield* Command.string(Command.make(binPath, ...args)).pipe( Effect2.mapError( (e) => new StartupCacheError({ cause: e, operation: "chainspec" }) ) ); if (!stdout.length) { return yield* Effect2.fail( new StartupCacheError({ cause: "build-spec produced no output", operation: "chainspec" }) ); } yield* fs18.writeFileString(outputPath, stdout).pipe(Effect2.mapError((e) => new StartupCacheError({ cause: e, operation: "chainspec" }))); return outputPath; }); var maybeGetRawChainSpec = (binPath, chainName, cacheSubDir, shouldGenerate) => Effect2.gen(function* () { if (!shouldGenerate) return Option.none(); const fs18 = yield* FileSystem2.FileSystem; const pathService = yield* Path.Path; const rawSpecPath = pathService.join(cacheSubDir, `${chainName}-raw.json`); const exists = yield* fs18.exists(rawSpecPath).pipe(Effect2.orElseSucceed(() => false)); if (exists) return Option.some(rawSpecPath); return yield* generateRawChainSpec(binPath, chainName, rawSpecPath).pipe( Effect2.map(Option.some), Effect2.catchAll(() => Effect2.succeed(Option.none())) ); }); var getCachedArtifactsImpl = (config) => Effect2.gen(function* () { const fs18 = yield* FileSystem2.FileSystem; const pathService = yield* Path.Path; const binaryHash = yield* hashFile(config.binPath); const shortHash = binaryHash.substring(0, 12); const chainName = config.isDevMode ? "dev" : config.chainArg?.match(/--chain[=\s]?(\S+)/)?.[1] || "default"; const binName = pathService.basename(config.binPath); const cacheSubDir = pathService.join(config.cacheDir, `${binName}-${chainName}-${shortHash}`); const hashPath = pathService.join(cacheSubDir, "binary.hash"); const lockPath = pathService.join(config.cacheDir, `${binName}-${chainName}.lock`); yield* fs18.makeDirectory(cacheSubDir, { recursive: true }).pipe(Effect2.mapError((e) => new StartupCacheError({ cause: e, operation: "cache" }))); const cached = yield* checkCache(cacheSubDir, hashPath, binaryHash); if (Option.isSome(cached)) { logger2.debug(`Using cached precompiled WASM: ${cached.value}`); const rawChainSpecPath = yield* maybeGetRawChainSpec( config.binPath, chainName, cacheSubDir, config.generateRawChainSpec ?? false ); return { precompiledPath: cached.value, fromCache: true, rawChainSpecPath: Option.getOrUndefined(rawChainSpecPath) }; } return yield* withFileLock( lockPath, Effect2.gen(function* () { const nowCached = yield* checkCache(cacheSubDir, hashPath, binaryHash); if (Option.isSome(nowCached)) { logger2.debug( `Using cached precompiled WASM (created by another process): ${nowCached.value}` ); const rawChainSpecPath2 = yield* maybeGetRawChainSpec( config.binPath, chainName, cacheSubDir, config.generateRawChainSpec ?? false ); return { precompiledPath: nowCached.value, fromCache: true, rawChainSpecPath: Option.getOrUndefined(rawChainSpecPath2) }; } logger2.debug("Precompiling WASM (this may take a moment)..."); const wasmPath = yield* runPrecompile(config.binPath, config.chainArg, cacheSubDir); yield* fs18.writeFileString(hashPath, binaryHash).pipe(Effect2.mapError((e) => new StartupCacheError({ cause: e, operation: "cache" }))); const rawChainSpecPath = yield* maybeGetRawChainSpec( config.binPath, chainName, cacheSubDir, config.generateRawChainSpec ?? false ); return { precompiledPath: wasmPath, fromCache: false, rawChainSpecPath: Option.getOrUndefined(rawChainSpecPath) }; }), Duration2.minutes(2) ); }); var StartupCacheServiceLive = Layer.succeed(StartupCacheService, { getCachedArtifacts: (config) => getCachedArtifactsImpl(config).pipe( Effect2.mapError( (e) => e._tag === "FileLockError" ? new StartupCacheError({ cause: e, operation: "lock" }) : e ), Effect2.provide(NodeContext.layer) ) }); var StartupCacheServiceTestable = Layer.succeed(StartupCacheService, { getCachedArtifacts: (config) => getCachedArtifactsImpl(config).pipe( Effect2.mapError( (e) => e._tag === "FileLockError" ? new StartupCacheError({ cause: e, operation: "lock" }) : e ) ) }); // src/internal/commandParsers.ts var logger3 = createLogger3({ name: "commandParsers" }); function parseZombieCmd(launchSpec) { if (launchSpec) { return { cmd: launchSpec.configPath }; } throw new Error( "No ZombieSpec found in config. Are you sure your 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; launchOverrides; constructor(options) { const { launchSpec, additionalRepos, launchOverrides } = options; this.launchSpec = launchSpec; this.launchOverrides = launchOverrides; this.launch = !launchSpec.running ? true : launchSpec.running; this.cmd = launchSpec.binPath; this.args = launchSpec.options ? [...launchSpec.options] : fetchDefaultArgs(path5.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); } } async withPorts() { if (process.env.MOON_RECYCLE === "true") { const existingPort = process.env.MOONWALL_RPC_PORT; if (existingPort) { this.overrideArg(`--rpc-port=${existingPort}`); } return this; } 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 = (await getFreePort()).toString(); process.env.MOONWALL_RPC_PORT = freePort; this.overrideArg(`--rpc-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() { logger3.debug(`Command to run: ${this.cmd}`); logger3.debug(`Arguments: ${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"); } } /** * Cache startup artifacts if enabled in launchSpec. * This uses an Effect-based service that caches artifacts by binary hash. * * When cacheStartupArtifacts is enabled, this generates: * 1. Precompiled WASM for the runtime * 2. Raw chain spec to skip genesis WASM compilation * * This reduces startup from ~3s to ~200ms (~10x improvement). */ async withStartupCache() { if (!this.launchSpec.cacheStartupArtifacts) { return this; } if (this.launchSpec.useDocker) { logger3.warn("Startup caching is not supported for Docker images, skipping"); return this; } const chainArg = this.args.find((arg) => arg.startsWith("--chain")); const hasDevFlag = this.args.includes("--dev"); const existingChainName = chainArg?.match(/--chain[=\s]?(\S+)/)?.[1]; const canGenerateRawSpec = hasDevFlag || !!existingChainName; const cacheDir = this.launchSpec.startupCacheDir || path5.join(process.cwd(), "tmp", "startup-cache"); const program = StartupCacheService.pipe( Effect3.flatMap( (service) => service.getCachedArtifacts({ binPath: this.launchSpec.binPath, chainArg, cacheDir, // Generate raw chain spec for faster startup (works for both --dev and --chain=XXX) generateRawChainSpec: canGenerateRawSpec, // Pass dev mode flag for proper chain name detection isDevMode: hasDevFlag }) ), Effect3.provide(StartupCacheServiceLive) ); try { const result = await Effect3.runPromise(program); const precompiledDir = path5.dirname(result.precompiledPath); this.overrideArg(`--wasmtime-precompiled=${precompiledDir}`); if (result.rawChainSpecPath) { if (hasDevFlag) { this.args = this.args.filter((arg) => arg !== "--dev"); this.overrideArg(`--chain=${result.rawChainSpecPath}`); this.overrideArg("--alice"); this.overrideArg("--force-authoring"); this.overrideArg("--rpc-cors=all"); this.overrideArg( "--node-key=0000000000000000000000000000000000000000000000000000000000000001" ); } else if (existingChainName) { this.overrideArg(`--chain=${result.rawChainSpecPath}`); } logger3.debug(`Using raw chain spec for ~10x faster startup: ${result.rawChainSpecPath}`); } process.env.MOONWALL_CACHE_DIR = precompiledDir; logger3.debug( result.fromCache ? `Using cached precompiled WASM: ${result.precompiledPath}` : `Precompiled WASM created: ${result.precompiledPath}` ); } catch (error) { logger3.warn(`WASM precompilation failed, continuing without: ${error}`); } return this; } build() { return { cmd: this.cmd, args: this.args, launch: this.launch }; } static async create(options) { const parser = new _LaunchCommandParser(options); const parsed = await parser.withPorts().then((p) => p.withDefaultForkConfig().withLaunchOverrides()).then((p) => p.withStartupCache()); 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}`, `--host=${launchSpecs[0].address ?? "127.0.0.1"}` ]; 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 isPortAvailable = async (port) => { return new Promise((resolve) => { const server = net.createServer(); server.listen(port, () => { server.once("close", () => resolve(true)); server.close(); }); server.on("error", () => resolve(false)); }); }; var getNextAvailablePort = async (startPort) => { let port = startPort; while (port <= 65535) { if (await isPortAvailable(port)) { return port; } port++; } throw new Error(`No available ports found starting from ${startPort}`); }; var getFreePort = async () => { const shardIndex = shardManager.getShardIndex(); const totalShards = shardManager.getTotalShards(); const poolId = parseInt(process.env.VITEST_POOL_ID || "0", 10); const basePort = 1e4; const shardOffset = shardIndex * 1e3; const poolOffset = poolId * 100; const processOffset = process.pid % 50; const calculatedPort = basePort + shardOffset + poolOffset + processOffset; const startPort = Math.min(calculatedPort, 6e4 + shardIndex * 100 + poolId); logger3.debug( `Port calculation: shard=${shardIndex + 1}/${totalShards}, pool=${poolId}, final=${startPort}` ); return getNextAvailablePort(startPort); }; // src/internal/deriveTestIds.ts import chalk3 from "chalk"; import fs7 from "fs"; import { confirm as confirm3 } from "@inquirer/prompts"; import path6 from "path"; // src/internal/testIdParser.ts import { Lang, parse, findInFiles } from "@ast-grep/napi"; // src/internal/fileCheckers.ts import fs8 from "fs"; import { execSync as execSync2 } from "child_process"; import chalk4 from "chalk"; import os2 from "os"; import path7 from "path"; import { select } from "@inquirer/prompts"; async function checkExists(path14) { const binPath = path14.split(" ")[0]; const fsResult = fs8.existsSync(binPath); if (!fsResult) { throw new Error( `No binary file found at location: ${binPath} Are you sure your ${chalk4.bgWhiteBright.blackBright( "moonwall.config.json" )} file has the correct "binPath" in launchSpec?` ); } const binArch = await getBinaryArchitecture(binPath); const currentArch = os2.arch(); if (binArch !== currentArch && binArch !== "unknown") { throw new Error( `The binary architecture ${chalk4.bgWhiteBright.blackBright( binArch )} does not match this system's architecture ${chalk4.bgWhiteBright.blackBright( currentArch )} Download or compile a new binary executable for ${chalk4.bgWhiteBright.blackBright( currentArch )} ` ); } return true; } function checkAccess(path14) { const binPath = path14.split(" ")[0]; try { fs8.accessSync(binPath, fs8.constants.X_OK); } catch (_err) { console.error(`The file ${binPath} is not executable`); throw new Error(`The file at ${binPath} , lacks execute