From c79242582a9e0c2ce085880dd75cf5917515d9fa Mon Sep 17 00:00:00 2001 From: Tim B <79199034+timbrinded@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:05:26 +0000 Subject: [PATCH] Feat/fork test (#443) * feat: :sparkles: Added default fork config * feat: :recycle: Overhaul command parsing * refactor: :recycle: Refactor to use invariant * test: :white_check_mark: Added fork test to CI * test: :white_check_mark: add fork test * refactor: :recycle: Fix block arg --- .changeset/lucky-boats-cheat.md | 11 ++ .github/workflows/main.yml | 2 +- docs/guide/intro/foundations.md | 7 +- docs/guide/intro/getting-started.md | 1 - packages/cli/src/cmds/runTests.ts | 3 +- .../internal/cmdFunctions/initialisation.ts | 2 +- packages/cli/src/internal/commandParsers.ts | 151 ++++++++++++++---- packages/cli/src/lib/globalContext.ts | 131 +++++++-------- packages/cli/src/lib/runnerContext.ts | 7 +- packages/types/config_schema.json | 35 ++-- packages/types/src/config.ts | 38 +++-- packages/types/src/runner.ts | 21 ++- test/configs/mbStateOverride.json | 6 + test/moonwall.config.json | 35 ++++ .../chopsticks/test-chopsticks-state.ts | 2 +- .../chopsticks/test-chopsticks-state2.ts | 2 +- test/suites/dev_tests/test1/test_dev.ts | 4 +- test/suites/dev_tests/test2/test_dev2.ts | 6 +- .../dummy-smoke/test_block_finalized.ts | 2 +- test/suites/dummy-smoke/test_conditional.ts | 2 +- test/suites/dummy-smoke/test_conditional2.ts | 2 +- .../eth_test/test-ethers_test.spec copy 2.ts | 2 +- .../eth_test/test-ethers_test.spec copy.ts | 2 +- test/suites/eth_test/test-ethers_test.spec.ts | 2 +- .../suites/eth_test2/test-ethers_test.spec.ts | 2 +- test/suites/fork_test/test_fork.ts | 62 +++++++ test/suites/multizombie/test_basic.ts | 2 +- test/suites/read_only/test-readonly.spec.ts | 2 +- test/suites/run_error/test-run_errors.ts | 2 +- .../test_separation2 copy 2.ts | 2 +- .../test_separation/test_separation2 copy.ts | 2 +- .../test_separation/test_separation2.ts | 2 +- .../test_separation3 copy 2.ts | 2 +- .../test_separation/test_separation3 copy.ts | 2 +- .../test_separation/test_separation3.ts | 2 +- .../test_separation4 copy 2.ts | 2 +- .../test_separation/test_separation4 copy.ts | 2 +- .../test_separation/test_separation4.ts | 2 +- .../test_separation5 copy 2.ts | 2 +- .../test_separation/test_separation5 copy.ts | 2 +- .../test_separation/test_separation5.ts | 2 +- test/suites/viem/test_viem_test.ts | 2 +- test/suites/web3_test/test_web3.ts | 2 +- 43 files changed, 404 insertions(+), 170 deletions(-) create mode 100644 .changeset/lucky-boats-cheat.md create mode 100644 test/configs/mbStateOverride.json create mode 100644 test/suites/fork_test/test_fork.ts diff --git a/.changeset/lucky-boats-cheat.md b/.changeset/lucky-boats-cheat.md new file mode 100644 index 00000000..5cbcd008 --- /dev/null +++ b/.changeset/lucky-boats-cheat.md @@ -0,0 +1,11 @@ +--- +"@moonwall/types": major +"@moonwall/cli": major +"@moonwall/tests": major +"@moonwall/docs": major +"@moonwall/util": major +--- + +Added Fork config to dev foundations + +- BETA: Added ability to use moonbeam's fork API both via moonwall.config and also in a test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a1448030..1ddedcc4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,7 +114,7 @@ jobs: strategy: fail-fast: false matrix: - suite: ["dev_test", "dev_multi", "dev_seq", "dev_smoke", "papi_dev"] + suite: ["dev_test", "dev_multi", "dev_seq", "dev_smoke", "papi_dev", "fork_test"] shard: [1, 2, 3, 4] steps: - uses: actions/checkout@v4 diff --git a/docs/guide/intro/foundations.md b/docs/guide/intro/foundations.md index 932f9393..1e5e1f04 100644 --- a/docs/guide/intro/foundations.md +++ b/docs/guide/intro/foundations.md @@ -48,9 +48,4 @@ Use Read Only if Moonwall doesn't need to start any networks - you've already go ::: tip Zombie is the ideal Foundation for testing cross chain interactions including XCM. -::: - - -### Fork: - -- 🚧 Not yet implemented! Will be part of a new way of forking the network with a real client. +::: \ No newline at end of file diff --git a/docs/guide/intro/getting-started.md b/docs/guide/intro/getting-started.md index 481ad17c..5931517d 100644 --- a/docs/guide/intro/getting-started.md +++ b/docs/guide/intro/getting-started.md @@ -87,7 +87,6 @@ This isn't too important as you will always be able to create new environment sp - `chopsticks` : Using Acala Foundation's Chopsticks to start a lazily-forked network. - `read_only`: Not starting a network but instead connecting to one that already exists. - `zombie`: Using ParityTech's ZombieNetwork framework to run a multi-node network -- `fork` : 🚧 Not yet implemented! Will be part of a new way of forking the network with a real client ::: tip This is the very brief rundown of foundations. For their specific information please visit the relevant sections in [Config](/guide/intro/foundations). diff --git a/packages/cli/src/cmds/runTests.ts b/packages/cli/src/cmds/runTests.ts index 36f21044..c5e9023c 100644 --- a/packages/cli/src/cmds/runTests.ts +++ b/packages/cli/src/cmds/runTests.ts @@ -120,8 +120,7 @@ export async function executeTests(env: Environment, testRunArgs?: testRunArgs) const testFileDir = additionalArgs?.subDirectory !== undefined ? env.testFileDir.map((folder) => - // @ts-expect-error - bug in tsc - path.join(folder, additionalArgs.subDirectory) + path.join(folder, additionalArgs.subDirectory || "error") ) : env.testFileDir; diff --git a/packages/cli/src/internal/cmdFunctions/initialisation.ts b/packages/cli/src/internal/cmdFunctions/initialisation.ts index e4f6fbd7..e565a891 100644 --- a/packages/cli/src/internal/cmdFunctions/initialisation.ts +++ b/packages/cli/src/internal/cmdFunctions/initialisation.ts @@ -123,7 +123,7 @@ export function createConfig(options: { testFileDir: [options.testDir], foundation: { type: options.foundation as any, - }, + } as any, }, ], }; diff --git a/packages/cli/src/internal/commandParsers.ts b/packages/cli/src/internal/commandParsers.ts index e4c87359..1a6e10b3 100644 --- a/packages/cli/src/internal/commandParsers.ts +++ b/packages/cli/src/internal/commandParsers.ts @@ -3,10 +3,13 @@ import type { DevLaunchSpec, RepoSpec, ZombieLaunchSpec, + LaunchOverrides, + ForkConfig, } from "@moonwall/types"; import chalk from "chalk"; import path from "node:path"; import { standardRepos } from "../lib/repoDefinitions"; +import invariant from "tiny-invariant"; export function parseZombieCmd(launchSpec: ZombieLaunchSpec) { if (launchSpec) { @@ -38,42 +41,127 @@ function fetchDefaultArgs(binName: string, additionalRepos: RepoSpec[] = []): st return defaultArgs; } -export async function parseRunCmd(launchSpec: DevLaunchSpec, additionalRepos?: RepoSpec[]) { - const launch = !launchSpec.running ? true : launchSpec.running; - const cmd = launchSpec.binPath; - const args = launchSpec.options - ? [...launchSpec.options] - : fetchDefaultArgs(path.basename(launchSpec.binPath), additionalRepos); - - if (launchSpec.ports) { - const ports = launchSpec.ports; - if (ports.p2pPort) { - args.push(`--port=${ports.p2pPort}`); +export class LaunchCommandParser { + private args: string[]; + private cmd: string; + private launch: boolean; + private launchSpec: DevLaunchSpec; + private additionalRepos?: RepoSpec[]; + private launchOverrides?: LaunchOverrides; + + constructor(options: { + launchSpec: DevLaunchSpec; + additionalRepos?: RepoSpec[]; + launchOverrides?: LaunchOverrides; + }) { + 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(path.basename(launchSpec.binPath), additionalRepos); + } + + private overrideArg(newArg: string): void { + 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); } - if (ports.wsPort) { - args.push(`--ws-port=${ports.wsPort}`); + } + + 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}`); + } } - if (ports.rpcPort) { - args.push(`--rpc-port=${ports.rpcPort}`); + return this; + } + + withDefaultForkConfig(): LaunchCommandParser { + const forkOptions = this.launchSpec.defaultForkConfig; + if (forkOptions) { + this.applyForkOptions(forkOptions); } - } else { - const freePort = (await getFreePort()).toString(); - process.env.MOONWALL_RPC_PORT = freePort; + return this; + } - if (launchSpec.newRpcBehaviour) { - args.push(`--rpc-port=${freePort}`); - } else { - args.push(`--ws-port=${freePort}`); + withLaunchOverrides(): LaunchCommandParser { + if (this.launchOverrides?.forkConfig) { + this.applyForkOptions(this.launchOverrides.forkConfig); } + return this; } - return { cmd, args, launch }; -} -export const getFreePort = async () => { - const notionalPort = 10000 + Number(process.env.VITEST_POOL_ID || 1) * 100; - // return getPort({ port: notionalPort }); - return notionalPort; -}; + private print() { + console.log(chalk.cyan(`Command to run is: ${chalk.bold(this.cmd)}`)); + console.log(chalk.cyan(`Arguments are: ${chalk.bold(this.args.join(" "))}`)); + return this; + } + + private applyForkOptions(forkOptions: ForkConfig): void { + 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(): { cmd: string; args: string[]; launch: boolean } { + return { + cmd: this.cmd, + args: this.args, + launch: this.launch, + }; + } + + static create(options: { + launchSpec: DevLaunchSpec; + additionalRepos?: RepoSpec[]; + launchOverrides?: LaunchOverrides; + verbose?: boolean; + }) { + const parser = new LaunchCommandParser(options); + const parsed = parser.withPorts().withDefaultForkConfig().withLaunchOverrides(); + + if (options.verbose) { + parsed.print(); + } + + return parsed.build(); + } +} export function parseChopsticksRunCmd(launchSpecs: ChopsticksLaunchSpec[]): { cmd: string; @@ -132,3 +220,8 @@ export function parseChopsticksRunCmd(launchSpecs: ChopsticksLaunchSpec[]): { launch, }; } + +export const getFreePort = () => { + const notionalPort = 10000 + Number(process.env.VITEST_POOL_ID || 1) * 100; + return notionalPort; +}; diff --git a/packages/cli/src/lib/globalContext.ts b/packages/cli/src/lib/globalContext.ts index aeb3111b..915dc65e 100644 --- a/packages/cli/src/lib/globalContext.ts +++ b/packages/cli/src/lib/globalContext.ts @@ -3,6 +3,7 @@ import type { ConnectedProvider, Environment, FoundationType, + LaunchOverrides, MoonwallConfig, MoonwallEnvironment, MoonwallProvider, @@ -16,7 +17,11 @@ import net from "node:net"; import readline from "node:readline"; import { setTimeout as timer } from "node:timers/promises"; import path from "node:path"; -import { parseChopsticksRunCmd, parseRunCmd, parseZombieCmd } from "../internal/commandParsers"; +import { + LaunchCommandParser, + parseChopsticksRunCmd, + parseZombieCmd, +} from "../internal/commandParsers"; import { type IPCRequestMessage, type IPCResponseMessage, @@ -39,6 +44,7 @@ import { import { type ChildProcess, exec, execSync } from "node:child_process"; import { promisify } from "node:util"; import { withTimeout } from "../internal"; +import invariant from "tiny-invariant"; const debugSetup = Debug("global:context"); export class MoonwallContext { @@ -51,26 +57,23 @@ export class MoonwallContext { zombieNetwork?: Network; rtUpgradePath?: string; ipcServer?: net.Server; + injectedOptions?: LaunchOverrides; - constructor(config: MoonwallConfig) { + constructor(config: MoonwallConfig, options?: LaunchOverrides) { const env = config.environments.find(({ name }) => name === process.env.MOON_TEST_ENV); - - if (!env) { - throw new Error(`Environment ${process.env.MOON_TEST_ENV} not found in config`); - } + invariant(env, `Environment ${process.env.MOON_TEST_ENV} not found in config`); this.providers = []; this.nodes = []; this.foundation = env.foundation.type; + this.injectedOptions = options; } public async setupFoundation() { const config = await importAsyncConfig(); const env = config.environments.find(({ name }) => name === process.env.MOON_TEST_ENV); - if (!env) { - throw new Error(`Environment ${process.env.MOON_TEST_ENV} not found in config`); - } + invariant(env, `Environment ${process.env.MOON_TEST_ENV} not found in config`); const foundationHandlers: Record< FoundationType, @@ -80,7 +83,6 @@ export class MoonwallContext { chopsticks: this.handleChopsticks, dev: this.handleDev, zombie: this.handleZombie, - fork: this.handleReadOnly, // TODO: Implement fork }; const foundationHandler = foundationHandlers[env.foundation.type]; @@ -93,9 +95,7 @@ export class MoonwallContext { } private async handleZombie(env: Environment) { - if (env.foundation.type !== "zombie") { - throw new Error(`Foundation type must be 'zombie'`); - } + invariant(env.foundation.type === "zombie", "Foundation type must be 'zombie'"); const { cmd: zombieConfig } = await parseZombieCmd(env.foundation.zombieSpec); this.rtUpgradePath = env.foundation.rtUpgradePath; @@ -114,14 +114,14 @@ export class MoonwallContext { } private async handleDev(env: Environment, config: MoonwallConfig) { - if (env.foundation.type !== "dev") { - throw new Error(`Foundation type must be 'dev'`); - } + invariant(env.foundation.type === "dev", "Foundation type must be 'dev'"); - const { cmd, args, launch } = await parseRunCmd( - env.foundation.launchSpec[0], - config.additionalRepos - ); + const { cmd, args, launch } = LaunchCommandParser.create({ + launchSpec: env.foundation.launchSpec[0], + additionalRepos: config.additionalRepos, + launchOverrides: this.injectedOptions, + verbose: false, + }); return { name: env.name, @@ -149,15 +149,13 @@ export class MoonwallContext { } private async handleReadOnly(env: Environment) { - if (env.foundation.type !== "read_only") { - throw new Error(`Foundation type must be 'read_only'`); - } + invariant(env.foundation.type === "read_only", "Foundation type must be 'read_only'"); + + invariant( + env.connections, + `${env.name} env config is missing connections specification, required by foundation READ_ONLY` + ); - if (!env.connections) { - throw new Error( - `${env.name} env config is missing connections specification, required by foundation READ_ONLY` - ); - } return { name: env.name, foundationType: "read_only", @@ -166,15 +164,11 @@ export class MoonwallContext { } private async handleChopsticks(env: Environment) { - if (env.foundation.type !== "chopsticks") { - throw new Error(`Foundation type must be 'chopsticks'`); - } - - if (!env.connections || env.connections.length === 0) { - throw new Error( - `${env.name} env config is missing connections specification, required by foundation CHOPSTICKS` - ); - } + invariant(env.foundation.type === "chopsticks", "Foundation type must be 'chopsticks'"); + invariant( + env.connections && env.connections.length > 0, + `${env.name} env config is missing connections specification, required by foundation CHOPSTICKS` + ); this.rtUpgradePath = env.foundation.rtUpgradePath; return { @@ -187,9 +181,10 @@ export class MoonwallContext { private async startZombieNetwork() { const env = getEnvironmentFromConfig(); - if (env.foundation.type !== "zombie") { - throw new Error(`Foundation type must be 'zombie', something has gone very wrong.`); - } + invariant( + env.foundation.type === "zombie", + "Foundation type must be 'zombie', something has gone very wrong." + ); console.log("🧟 Spawning zombie nodes ..."); const nodes = this.environment.nodes; @@ -219,9 +214,7 @@ export class MoonwallContext { const onProcessExit = () => { try { - if (!this.zombieNetwork) { - throw "Zombie network not found to kill"; - } + invariant(this.zombieNetwork, "Zombie network not found to kill"); const processIds = Object.values((this.zombieNetwork.client as any).processMap) .filter((item: any) => item.pid) @@ -261,13 +254,10 @@ export class MoonwallContext { try { const message: IPCRequestMessage = JSON.parse(data.toString()); + invariant(message.nodeName, "nodeName not provided in message"); const zombieClient = network.client; - if (!message.nodeName) { - throw new Error("nodeName not provided in message"); - } - switch (message.cmd) { case "networkmap": { const result = Object.keys(network.nodesByName); @@ -371,7 +361,7 @@ export class MoonwallContext { } default: - throw new Error(`Invalid command received: ${message.cmd}`); + invariant(false, `Invalid command received: ${message.cmd}`); } } catch (e: any) { logIpc("📨 Error processing message from client"); @@ -542,9 +532,7 @@ export class MoonwallContext { const envVar = process.env.MOON_ZOMBIE_NODES; - if (!envVar) { - throw new Error("MOON_ZOMBIE_NODES not set, this is an error please raise."); - } + invariant(envVar, "MOON_ZOMBIE_NODES not set, this is an error please raise."); const zombieNodeLogs = envVar .split("|") @@ -599,10 +587,7 @@ export class MoonwallContext { public async disconnect(providerName?: string) { if (providerName) { const prov = this.providers.find(({ name }) => name === providerName); - - if (!prov) { - throw new Error(`Provider ${providerName} not found`); - } + invariant(prov, `Provider ${providerName} not found`); try { await prov.disconnect(); @@ -625,12 +610,20 @@ export class MoonwallContext { } } - public static async getContext(config?: MoonwallConfig, force = false): Promise { + public static async getContext( + config?: MoonwallConfig, + options?: LaunchOverrides, + force = false + ): Promise { + invariant( + !(options && MoonwallContext.instance), + "Attempting to open a new context with overrides when context already exists" + ); + if (!MoonwallContext.instance?.configured || force) { - if (!config) { - throw new Error("❌ Config must be provided on Global Context instantiation"); - } - MoonwallContext.instance = new MoonwallContext(config); + invariant(config, "Config must be provided on Global Context instantiation"); + + MoonwallContext.instance = new MoonwallContext(config, options); await MoonwallContext.instance.setupFoundation(); debugSetup(`🟢 Moonwall context "${config.label}" created`); } @@ -640,9 +633,7 @@ export class MoonwallContext { public static async destroy() { const ctx = MoonwallContext.instance; - if (!ctx) { - throw new Error("❌ No context to destroy"); - } + invariant(ctx, "No context to destroy"); try { await ctx.disconnect(); @@ -652,16 +643,10 @@ export class MoonwallContext { while (ctx.nodes.length > 0) { const node = ctx.nodes.pop(); - - if (!node) { - throw new Error("❌ No nodes to destroy"); - } + invariant(node, "No node to destroy"); const pid = node.pid; - - if (!pid) { - throw new Error("❌ No pid to destroy"); - } + invariant(pid, "No pid to destroy"); node.kill("SIGINT"); for (;;) { @@ -698,9 +683,9 @@ export class MoonwallContext { } } -export const contextCreator = async () => { +export const contextCreator = async (options?: LaunchOverrides) => { const config = await importAsyncConfig(); - const ctx = await MoonwallContext.getContext(config); + const ctx = await MoonwallContext.getContext(config, options); await runNetworkOnly(); await ctx.connectEnvironment(); return ctx; diff --git a/packages/cli/src/lib/runnerContext.ts b/packages/cli/src/lib/runnerContext.ts index b660bfed..eee0077a 100644 --- a/packages/cli/src/lib/runnerContext.ts +++ b/packages/cli/src/lib/runnerContext.ts @@ -70,6 +70,7 @@ export function describeSuite({ minRtVersion, chainType, notChainType, + options, }: ITestSuiteType): void { if ( (minRtVersion && minRtVersion > RT_VERSION) || @@ -83,6 +84,11 @@ export function describeSuite({ beforeAll(async () => { const env = getEnvironmentFromConfig(); + if (env.foundation.type === "dev") { + // Pass options to contextCreator if they exist + ctx = await contextCreator(options); + } + ctx = await contextCreator(); if (env.foundation.type === "read_only") { const settings = loadParams(env.foundation.launchSpec); @@ -159,7 +165,6 @@ export function describeSuite({ chopsticks: chopsticksHandler, zombie: zombieHandler, read_only: readOnlyHandler, - fork: readOnlyHandler, }; const handler = foundationHandlers[foundationMethods]; diff --git a/packages/types/config_schema.json b/packages/types/config_schema.json index 80a10fde..ac1e4d7c 100644 --- a/packages/types/config_schema.json +++ b/packages/types/config_schema.json @@ -69,9 +69,31 @@ "description": "A launch specification object for the \"dev\" foundation type.", "properties": { "binPath": { - "description": "The path to the binary file.", + "description": "The path to the binary to execute", "type": "string" }, + "defaultForkConfig": { + "description": "BETA: Default Fork options for the node (overriden by per-test fork options)", + "properties": { + "blockHash": { + "description": "The block hash to fork from", + "type": "string" + }, + "stateOverridePath": { + "description": "The state override path (optional)", + "type": "string" + }, + "url": { + "description": "The URL to fork from", + "type": "string" + }, + "verbose": { + "description": "Turns on trace logging for LazyLoading service (optional)", + "type": "boolean" + } + }, + "type": "object" + }, "disableDefaultEthProviders": { "description": "Determines if the default Ethereum provider connections should be disabled.\nWhen set to true, the framework will not automatically connect the Ethereum providers.\nDefault behavior (when unset or set to false) is to connect with Ethers, Viem & Web3 frameworks.\n\nNote: This also acts as a feature gate for context methods like createTxn and readPrecompile.", "type": "boolean" @@ -92,7 +114,7 @@ "type": "array" }, "ports": { - "description": "An optional object with p2pPort, wsPort, and rpcPort.", + "description": "Port configuration", "properties": { "p2pPort": { "description": "The port for peer-to-peer (P2P) communication.", @@ -181,15 +203,6 @@ } }, "type": "object" - }, - { - "properties": { - "type": { - "const": "fork", - "type": "string" - } - }, - "type": "object" } ], "description": "The foundation configuration for the environment. It can be of several types including \"dev\", \"chopsticks\", \"zombie\", \"read_only\", or \"fork\"." diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 28dc78df..9266b0af 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -173,10 +173,6 @@ export type IFoundation = | { type: "read_only"; launchSpec: ReadOnlyLaunchSpec; - } - | { - type: "fork"; - // launchSpec: ForkLaunchSpec; }; /** @@ -231,12 +227,6 @@ export interface ReadOnlyLaunchSpec extends GenericLaunchSpec { disableRuntimeVersionCheck?: boolean; } -/** - * A launch specification object for the "fork" foundation type. - * @extends GenericLaunchSpec - */ -export interface ForkLaunchSpec extends GenericLaunchSpec {} - /** * A launch specification object for the "zombie" foundation type. * @extends GenericLaunchSpec @@ -328,7 +318,7 @@ export interface ChopsticksLaunchSpec extends GenericLaunchSpec { */ export interface DevLaunchSpec extends GenericLaunchSpec { /** - * The path to the binary file. + * The path to the binary to execute */ binPath: string; @@ -347,7 +337,12 @@ export interface DevLaunchSpec extends GenericLaunchSpec { newRpcBehaviour?: boolean; /** - * An optional object with p2pPort, wsPort, and rpcPort. + * BETA: Default Fork options for the node (overriden by per-test fork options) + */ + defaultForkConfig?: ForkConfig; + + /** + * Port configuration */ ports?: { /** @@ -542,3 +537,22 @@ export type Bin = { name: string; defaultArgs?: string[]; }; + +export type ForkConfig = { + /** + * The URL to fork from + */ + url: string; + /** + * The block hash to fork from + */ + blockHash?: string; + /** + * The state override path (optional) + */ + stateOverridePath?: string; + /** + * Turns on trace logging for LazyLoading service (optional) + */ + verbose?: boolean; +}; diff --git a/packages/types/src/runner.ts b/packages/types/src/runner.ts index 8cb93761..1cca1d84 100644 --- a/packages/types/src/runner.ts +++ b/packages/types/src/runner.ts @@ -14,7 +14,7 @@ import type { } from "viem"; import type { Chain } from "viem/chains"; import type { Web3 } from "web3"; -import type { FoundationType } from "./config"; +import type { ForkConfig, FoundationType } from "./config"; import type { BlockCreation, BlockCreationResponse, ChopsticksBlockCreation } from "./context"; import type { ContractDeploymentOptions } from "./contracts"; import type { TransactionType } from "./eth"; @@ -86,6 +86,14 @@ export interface ITestCase { timeout?: number; } +export type TestSuiteConfig = { + id: string; + title: string; + foundationMethods: T; + description?: string; + testCases: TestCasesFn; +}; + export type FoundationHandler = (params: { testCases: TestCasesFn; context: GenericContext; @@ -94,12 +102,21 @@ export type FoundationHandler = (params: { ctx?: any; }) => void; +/** + * BETA: Represents overrides for launching a test environment. + * @property forkConfig - Optional configuration for forking a network. + */ +export type LaunchOverrides = { + forkConfig?: ForkConfig; +}; + export type ITestSuiteType = { id: string; title: string; testCases: (TestContext: TestContextMap[T]) => void; foundationMethods: T; - options?: object; + // TODO: Make this foundation dependent + options?: LaunchOverrides; minRtVersion?: number; chainType?: ChainType; notChainType?: ChainType; diff --git a/test/configs/mbStateOverride.json b/test/configs/mbStateOverride.json new file mode 100644 index 00000000..a675a1cf --- /dev/null +++ b/test/configs/mbStateOverride.json @@ -0,0 +1,6 @@ +[ + { + "key": "0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da90b65f88190770e2d4021d80a480ecf9c8300db2442725604b8f5eb172692bb15078205c2", + "value": "0x000000000000000001000000000000000000443945309a7a4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080" + } +] diff --git a/test/moonwall.config.json b/test/moonwall.config.json index 1f461c3e..c803b39d 100644 --- a/test/moonwall.config.json +++ b/test/moonwall.config.json @@ -949,6 +949,41 @@ "endpoints": ["ws://127.0.0.1:9955"] } ] + }, + { + "name": "fork_test", + "testFileDir": ["suites/fork_test"], + + "foundation": { + "type": "dev", + "launchSpec": [ + { + "name": "moonbeam", + "binPath": "tmp/moonbeam", + "newRpcBehaviour": true, + "disableDefaultEthProviders": true, + "options": [ + "--ethapi=txpool", + "--no-hardware-benchmarks", + "--no-telemetry", + "--unsafe-force-node-key-generation", + "--reserved-only", + "--no-grandpa", + "--no-prometheus", + "--force-authoring", + "--rpc-cors=all", + "--alice", + "--sealing=manual", + "--tmp" + ], + "defaultForkConfig": { + "url": "https://moonbeam.unitedbloc.com", + "stateOverridePath": "./configs/mbStateOverride.json", + "verbose": true + } + } + ] + } } ] } diff --git a/test/suites/chopsticks/test-chopsticks-state.ts b/test/suites/chopsticks/test-chopsticks-state.ts index 55467f8c..4345b204 100644 --- a/test/suites/chopsticks/test-chopsticks-state.ts +++ b/test/suites/chopsticks/test-chopsticks-state.ts @@ -2,7 +2,7 @@ import "@moonbeam-network/api-augment"; import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "S1", diff --git a/test/suites/chopsticks/test-chopsticks-state2.ts b/test/suites/chopsticks/test-chopsticks-state2.ts index 929fc7ae..31984d0a 100644 --- a/test/suites/chopsticks/test-chopsticks-state2.ts +++ b/test/suites/chopsticks/test-chopsticks-state2.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "S1", diff --git a/test/suites/dev_tests/test1/test_dev.ts b/test/suites/dev_tests/test1/test_dev.ts index 5c90ff99..71a0110d 100644 --- a/test/suites/dev_tests/test1/test_dev.ts +++ b/test/suites/dev_tests/test1/test_dev.ts @@ -21,9 +21,9 @@ import { deployViemContract, } from "@moonwall/util"; import { BN } from "@polkadot/util"; -import { Wallet, parseEther } from "ethers"; +import { type Wallet, parseEther } from "ethers"; import { - Abi, + type Abi, createWalletClient, decodeErrorResult, decodeEventLog, diff --git a/test/suites/dev_tests/test2/test_dev2.ts b/test/suites/dev_tests/test2/test_dev2.ts index 522d67ad..a78871e8 100644 --- a/test/suites/dev_tests/test2/test_dev2.ts +++ b/test/suites/dev_tests/test2/test_dev2.ts @@ -1,10 +1,10 @@ import "@moonbeam-network/api-augment"; import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { CHARLETH_ADDRESS, BALTATHAR_ADDRESS, alith } from "@moonwall/util"; -import { parseEther, Wallet } from "ethers"; +import { parseEther, type Wallet } from "ethers"; import { BN } from "@polkadot/util"; -import { ApiPromise } from "@polkadot/api"; -import Web3 from "web3"; +import type { ApiPromise } from "@polkadot/api"; +import type Web3 from "web3"; describeSuite({ id: "D02", title: "Dev test suite2", diff --git a/test/suites/dummy-smoke/test_block_finalized.ts b/test/suites/dummy-smoke/test_block_finalized.ts index 6649e416..5d813c0f 100644 --- a/test/suites/dummy-smoke/test_block_finalized.ts +++ b/test/suites/dummy-smoke/test_block_finalized.ts @@ -1,6 +1,6 @@ import Bottleneck from "bottleneck"; import semverLt from "semver/functions/lt"; -import { expect, describeSuite, ApiPromise, beforeAll, Web3 } from "@moonwall/cli"; +import { expect, describeSuite, type ApiPromise, beforeAll, type Web3 } from "@moonwall/cli"; import { checkBlockFinalized, fetchHistoricBlockNum, getBlockTime } from "@moonwall/util"; import Debug from "debug"; const debug = Debug("smoke:block-finalized"); diff --git a/test/suites/dummy-smoke/test_conditional.ts b/test/suites/dummy-smoke/test_conditional.ts index b85e2222..bfa3e963 100644 --- a/test/suites/dummy-smoke/test_conditional.ts +++ b/test/suites/dummy-smoke/test_conditional.ts @@ -1,5 +1,5 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "R01", diff --git a/test/suites/dummy-smoke/test_conditional2.ts b/test/suites/dummy-smoke/test_conditional2.ts index 187b1efc..f64f6270 100644 --- a/test/suites/dummy-smoke/test_conditional2.ts +++ b/test/suites/dummy-smoke/test_conditional2.ts @@ -1,5 +1,5 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "R01", diff --git a/test/suites/eth_test/test-ethers_test.spec copy 2.ts b/test/suites/eth_test/test-ethers_test.spec copy 2.ts index 3e6e6959..c4c0c6da 100644 --- a/test/suites/eth_test/test-ethers_test.spec copy 2.ts +++ b/test/suites/eth_test/test-ethers_test.spec copy 2.ts @@ -1,6 +1,6 @@ import { describeSuite, expect, beforeAll, MoonwallContext } from "@moonwall/cli"; import { xcAssetAbi } from "@moonwall/util"; -import { Wallet, ethers } from "ethers"; +import { type Wallet, ethers } from "ethers"; describeSuite({ id: "S01", diff --git a/test/suites/eth_test/test-ethers_test.spec copy.ts b/test/suites/eth_test/test-ethers_test.spec copy.ts index e1660e71..90b5e3c0 100644 --- a/test/suites/eth_test/test-ethers_test.spec copy.ts +++ b/test/suites/eth_test/test-ethers_test.spec copy.ts @@ -1,6 +1,6 @@ import { describeSuite, expect, beforeAll, MoonwallContext } from "@moonwall/cli"; import { xcAssetAbi } from "@moonwall/util"; -import { Wallet, ethers } from "ethers"; +import { type Wallet, ethers } from "ethers"; describeSuite({ id: "S01", diff --git a/test/suites/eth_test/test-ethers_test.spec.ts b/test/suites/eth_test/test-ethers_test.spec.ts index bf339465..c007f148 100644 --- a/test/suites/eth_test/test-ethers_test.spec.ts +++ b/test/suites/eth_test/test-ethers_test.spec.ts @@ -1,6 +1,6 @@ import { describeSuite, expect, beforeAll, MoonwallContext } from "@moonwall/cli"; import { xcAssetAbi } from "@moonwall/util"; -import { Wallet, ethers } from "ethers"; +import { type Wallet, ethers } from "ethers"; describeSuite({ id: "S01", diff --git a/test/suites/eth_test2/test-ethers_test.spec.ts b/test/suites/eth_test2/test-ethers_test.spec.ts index bf339465..c007f148 100644 --- a/test/suites/eth_test2/test-ethers_test.spec.ts +++ b/test/suites/eth_test2/test-ethers_test.spec.ts @@ -1,6 +1,6 @@ import { describeSuite, expect, beforeAll, MoonwallContext } from "@moonwall/cli"; import { xcAssetAbi } from "@moonwall/util"; -import { Wallet, ethers } from "ethers"; +import { type Wallet, ethers } from "ethers"; describeSuite({ id: "S01", diff --git a/test/suites/fork_test/test_fork.ts b/test/suites/fork_test/test_fork.ts new file mode 100644 index 00000000..09442d48 --- /dev/null +++ b/test/suites/fork_test/test_fork.ts @@ -0,0 +1,62 @@ +import "@moonbeam-network/api-augment"; +import { describeSuite, expect, beforeAll } from "@moonwall/cli"; +import type { ApiPromise } from "@polkadot/api"; + +describeSuite({ + id: "F01", + title: "Fork test", + foundationMethods: "dev", + options: { + forkConfig: { + url: "https://moonbeam.unitedbloc.com", + verbose: true, + blockHash: "0xffe39256c17cc4523a07c907bcf1aeeef4db217cd57cfcfb95d56088e0bb9f2d" + }, + }, + testCases: ({ it, context, log }) => { + let polkadotJs: ApiPromise; + + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + }); + + it({ + id: "T01", + title: "Checking that launched node can create blocks", + test: async () => { + const block = (await polkadotJs.rpc.chain.getBlock()).block.header.number.toNumber(); + await context.createBlock([], { finalize: false }); + const block2 = (await polkadotJs.rpc.chain.getBlock()).block.header.number.toNumber(); + expect(block2).to.be.greaterThan(block); + }, + }); + + it({ + id: "T02", + title: "Check that state overrides work", + test: async () => { + const testAccount = "0x8300db2442725604b8f5Eb172692Bb15078205c2"; + log(`Address: ${testAccount}`); + + const { + data: { free }, + } = await polkadotJs.query.system.account(testAccount); + expect(free.toBigInt(), "Free balance should be 1337000").toBe(1337000000000000000000n); + }, + }); + + it({ + id: "T03", + title: "Check that forking works at a particular height", + test: async () => { + const testAccount = "0x0f300B667c55B28f4609EecE5628Cc27445A10cC"; + log(`Address: ${testAccount}`); + + const { + data: { free }, + } = await polkadotJs.query.system.account(testAccount); + expect(free.toBigInt(), `Free balance should match what account ${testAccount} has at block #8508372`).toBe(76562560590695097485140n); + }, + }); + }, +}); diff --git a/test/suites/multizombie/test_basic.ts b/test/suites/multizombie/test_basic.ts index 7eeb3e57..a3edfc40 100644 --- a/test/suites/multizombie/test_basic.ts +++ b/test/suites/multizombie/test_basic.ts @@ -1,6 +1,6 @@ import "@moonbeam-network/api-augment"; import { expect, describeSuite, beforeAll } from "@moonwall/cli"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "Z1", diff --git a/test/suites/read_only/test-readonly.spec.ts b/test/suites/read_only/test-readonly.spec.ts index d9b6f75e..8a619041 100644 --- a/test/suites/read_only/test-readonly.spec.ts +++ b/test/suites/read_only/test-readonly.spec.ts @@ -1,7 +1,7 @@ import "@moonbeam-network/api-augment"; import { describeSuite } from "@moonwall/cli"; import { checkBlockFinalized } from "@moonwall/util"; -import { Wallet } from "ethers"; +import type { Wallet } from "ethers"; describeSuite({ id: "S01", diff --git a/test/suites/run_error/test-run_errors.ts b/test/suites/run_error/test-run_errors.ts index c0a464b1..a54b98aa 100644 --- a/test/suites/run_error/test-run_errors.ts +++ b/test/suites/run_error/test-run_errors.ts @@ -1,4 +1,4 @@ -import { describeSuite, expect, beforeAll, ApiPromise, Web3 } from "@moonwall/cli"; +import { describeSuite, expect, beforeAll, type ApiPromise, type Web3 } from "@moonwall/cli"; import { ALITH_ADDRESS } from "@moonwall/util"; describeSuite({ diff --git a/test/suites/test_separation/test_separation2 copy 2.ts b/test/suites/test_separation/test_separation2 copy 2.ts index 597a50ac..e92b320c 100644 --- a/test/suites/test_separation/test_separation2 copy 2.ts +++ b/test/suites/test_separation/test_separation2 copy 2.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll, beforeEach } from "@moonwall/cli"; import { CHARLETH_ADDRESS, ETHAN_ADDRESS, alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D21", diff --git a/test/suites/test_separation/test_separation2 copy.ts b/test/suites/test_separation/test_separation2 copy.ts index 597a50ac..e92b320c 100644 --- a/test/suites/test_separation/test_separation2 copy.ts +++ b/test/suites/test_separation/test_separation2 copy.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll, beforeEach } from "@moonwall/cli"; import { CHARLETH_ADDRESS, ETHAN_ADDRESS, alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D21", diff --git a/test/suites/test_separation/test_separation2.ts b/test/suites/test_separation/test_separation2.ts index 597a50ac..e92b320c 100644 --- a/test/suites/test_separation/test_separation2.ts +++ b/test/suites/test_separation/test_separation2.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll, beforeEach } from "@moonwall/cli"; import { CHARLETH_ADDRESS, ETHAN_ADDRESS, alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D21", diff --git a/test/suites/test_separation/test_separation3 copy 2.ts b/test/suites/test_separation/test_separation3 copy 2.ts index 22ff2c93..d3a5c39f 100644 --- a/test/suites/test_separation/test_separation3 copy 2.ts +++ b/test/suites/test_separation/test_separation3 copy 2.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D31", diff --git a/test/suites/test_separation/test_separation3 copy.ts b/test/suites/test_separation/test_separation3 copy.ts index 22ff2c93..d3a5c39f 100644 --- a/test/suites/test_separation/test_separation3 copy.ts +++ b/test/suites/test_separation/test_separation3 copy.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D31", diff --git a/test/suites/test_separation/test_separation3.ts b/test/suites/test_separation/test_separation3.ts index 22ff2c93..d3a5c39f 100644 --- a/test/suites/test_separation/test_separation3.ts +++ b/test/suites/test_separation/test_separation3.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D31", diff --git a/test/suites/test_separation/test_separation4 copy 2.ts b/test/suites/test_separation/test_separation4 copy 2.ts index 77b159a7..baee6f91 100644 --- a/test/suites/test_separation/test_separation4 copy 2.ts +++ b/test/suites/test_separation/test_separation4 copy 2.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D41", diff --git a/test/suites/test_separation/test_separation4 copy.ts b/test/suites/test_separation/test_separation4 copy.ts index 77b159a7..baee6f91 100644 --- a/test/suites/test_separation/test_separation4 copy.ts +++ b/test/suites/test_separation/test_separation4 copy.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D41", diff --git a/test/suites/test_separation/test_separation4.ts b/test/suites/test_separation/test_separation4.ts index 77b159a7..baee6f91 100644 --- a/test/suites/test_separation/test_separation4.ts +++ b/test/suites/test_separation/test_separation4.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D41", diff --git a/test/suites/test_separation/test_separation5 copy 2.ts b/test/suites/test_separation/test_separation5 copy 2.ts index 904381cd..c9ecae01 100644 --- a/test/suites/test_separation/test_separation5 copy 2.ts +++ b/test/suites/test_separation/test_separation5 copy 2.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D51", diff --git a/test/suites/test_separation/test_separation5 copy.ts b/test/suites/test_separation/test_separation5 copy.ts index 904381cd..c9ecae01 100644 --- a/test/suites/test_separation/test_separation5 copy.ts +++ b/test/suites/test_separation/test_separation5 copy.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D51", diff --git a/test/suites/test_separation/test_separation5.ts b/test/suites/test_separation/test_separation5.ts index 904381cd..c9ecae01 100644 --- a/test/suites/test_separation/test_separation5.ts +++ b/test/suites/test_separation/test_separation5.ts @@ -1,7 +1,7 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { alith } from "@moonwall/util"; import { parseEther } from "ethers"; -import { ApiPromise } from "@polkadot/api"; +import type { ApiPromise } from "@polkadot/api"; describeSuite({ id: "D51", diff --git a/test/suites/viem/test_viem_test.ts b/test/suites/viem/test_viem_test.ts index a6fb2f3e..31eda0b2 100644 --- a/test/suites/viem/test_viem_test.ts +++ b/test/suites/viem/test_viem_test.ts @@ -1,4 +1,4 @@ -import { ViemClient, beforeAll, describeSuite, expect } from "@moonwall/cli"; +import { type ViemClient, beforeAll, describeSuite, expect } from "@moonwall/cli"; import { xcAssetAbi } from "@moonwall/util"; import { formatEther, formatUnits, getContract } from "viem"; diff --git a/test/suites/web3_test/test_web3.ts b/test/suites/web3_test/test_web3.ts index ccefd05e..03fef3d3 100644 --- a/test/suites/web3_test/test_web3.ts +++ b/test/suites/web3_test/test_web3.ts @@ -1,6 +1,6 @@ import { describeSuite, expect, beforeAll } from "@moonwall/cli"; import { xcAssetAbi } from "@moonwall/util"; -import Web3 from "web3"; +import type Web3 from "web3"; describeSuite({ id: "W3",