Skip to content

Commit

Permalink
feat(cli): add mud test-v2 command (#554)
Browse files Browse the repository at this point in the history
* feat(cli): add test-v2 command scaffold

* feat(cli): test-v2 deploy to internal node

* feat(cli): skip deployment if worldAddress is provided
  • Loading branch information
alvrs authored Mar 31, 2023
1 parent 7d06c1b commit d6be8b0
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 61 deletions.
134 changes: 73 additions & 61 deletions packages/cli/src/commands/deploy-v2.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import chalk from "chalk";
import glob from "glob";
import path, { basename } from "path";
import type { CommandModule } from "yargs";
import type { CommandModule, Options } from "yargs";
import { loadWorldConfig } from "../config/world/index.js";
import { deploy } from "../utils/deploy-v2.js";
import { logError, MUDError } from "../utils/errors.js";
Expand All @@ -10,71 +10,68 @@ import { mkdirSync, writeFileSync } from "fs";
import { loadStoreConfig } from "../config/loadStoreConfig.js";
import { getChainId } from "../utils/getChainId.js";

type Options = {
export type DeployOptions = {
configPath?: string;
printConfig?: boolean;
profile?: string;
privateKey: string;
priorityFeeMultiplier: number;
clean?: boolean;
debug?: boolean;
saveDeployment?: boolean;
rpc?: string;
};

const commandModule: CommandModule<Options, Options> = {
command: "deploy-v2",

describe: "Deploy MUD v2 contracts",

builder(yargs) {
return yargs.options({
configPath: { type: "string", desc: "Path to the config file" },
clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
printConfig: { type: "boolean", desc: "Print the resolved config" },
profile: { type: "string", desc: "The foundry profile to use" },
debug: { type: "boolean", desc: "Print debug logs, like full error messages" },
priorityFeeMultiplier: {
type: "number",
desc: "Multiply the estimated priority fee by the provided factor",
default: 1,
},
});
export const yDeployOptions = {
configPath: { type: "string", desc: "Path to the config file" },
clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
printConfig: { type: "boolean", desc: "Print the resolved config" },
profile: { type: "string", desc: "The foundry profile to use" },
debug: { type: "boolean", desc: "Print debug logs, like full error messages" },
priorityFeeMultiplier: {
type: "number",
desc: "Multiply the estimated priority fee by the provided factor",
default: 1,
},

async handler(args) {
args.profile = args.profile ?? process.env.FOUNDRY_PROFILE;
const { configPath, printConfig, profile, clean } = args;

const rpc = await getRpcUrl(profile);
console.log(
chalk.bgBlue(
chalk.whiteBright(`\n Deploying MUD v2 contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
)
);

if (clean) await forge(["clean"], { profile });

// Run forge build
await forge(["build"], { profile });

// Get a list of all contract names
const srcDir = await getSrcDirectory();
const existingContracts = glob
.sync(`${srcDir}/**/*.sol`)
// Get the basename of the file
.map((path) => basename(path, ".sol"));

// Load and resolve the config
const worldConfig = await loadWorldConfig(configPath, existingContracts);
const storeConfig = await loadStoreConfig(configPath);
const mudConfig = { ...worldConfig, ...storeConfig };

if (printConfig) console.log(chalk.green("\nResolved config:\n"), JSON.stringify(mudConfig, null, 2));

try {
const privateKey = process.env.PRIVATE_KEY;
if (!privateKey) throw new MUDError("Missing PRIVATE_KEY environment variable");
const deploymentInfo = await deploy(mudConfig, { ...args, rpc, privateKey });

saveDeployment: { type: "boolean", desc: "Save the deployment info to a file", default: true },
rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" },
} satisfies Record<keyof DeployOptions, Options>;

export async function deployHandler(args: Parameters<(typeof commandModule)["handler"]>[0]) {
args.profile = args.profile ?? process.env.FOUNDRY_PROFILE;
const { configPath, printConfig, profile, clean } = args;

const rpc = args.rpc ?? (await getRpcUrl(profile));
console.log(
chalk.bgBlue(
chalk.whiteBright(`\n Deploying MUD v2 contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
)
);

if (clean) await forge(["clean"], { profile });

// Run forge build
await forge(["build"], { profile });

// Get a list of all contract names
const srcDir = await getSrcDirectory();
const existingContracts = glob
.sync(`${srcDir}/**/*.sol`)
// Get the basename of the file
.map((path) => basename(path, ".sol"));

// Load and resolve the config
const worldConfig = await loadWorldConfig(configPath, existingContracts);
const storeConfig = await loadStoreConfig(configPath);
const mudConfig = { ...worldConfig, ...storeConfig };

if (printConfig) console.log(chalk.green("\nResolved config:\n"), JSON.stringify(mudConfig, null, 2));

try {
const privateKey = process.env.PRIVATE_KEY;
if (!privateKey) throw new MUDError("Missing PRIVATE_KEY environment variable");
const deploymentInfo = await deploy(mudConfig, { ...args, rpc, privateKey });

if (args.saveDeployment) {
// Write deployment result to file (latest and timestamp)
const chainId = await getChainId(rpc);
const outputDir = path.join(mudConfig.deploysDirectory, chainId.toString());
Expand All @@ -83,12 +80,27 @@ const commandModule: CommandModule<Options, Options> = {
writeFileSync(path.join(outputDir, Date.now() + ".json"), JSON.stringify(deploymentInfo, null, 2));

console.log(chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${outputDir}): \n`)));
console.log(deploymentInfo);
} catch (error: any) {
logError(error);
process.exit(1);
}

console.log(deploymentInfo);
return deploymentInfo;
} catch (error: any) {
logError(error);
process.exit(1);
}
}

const commandModule: CommandModule<DeployOptions, DeployOptions> = {
command: "deploy-v2",

describe: "Deploy MUD v2 contracts",

builder(yargs) {
return yargs.options(yDeployOptions);
},

async handler(args) {
await deployHandler(args);
process.exit(0);
},
};
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import tsgen from "./tsgen.js";
import deployV2 from "./deploy-v2.js";
import worldgen from "./worldgen.js";
import setVersion from "./set-version.js";
import testV2 from "./test-v2.js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
export const commands: CommandModule<any, any>[] = [
Expand All @@ -38,4 +39,5 @@ export const commands: CommandModule<any, any>[] = [
types,
worldgen,
setVersion,
testV2,
];
71 changes: 71 additions & 0 deletions packages/cli/src/commands/test-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { CommandModule } from "yargs";
import { deployHandler, DeployOptions } from "./deploy-v2.js";
import { yDeployOptions } from "./deploy-v2.js";
import { anvil, forge, getRpcUrl, getTestDirectory } from "../utils/foundry.js";
import chalk from "chalk";
import { rmSync, writeFileSync } from "fs";
import path from "path";

type Options = DeployOptions & { port?: number; worldAddress?: string; forgeOptions?: string };

const WORLD_ADDRESS_FILE = ".mudtest";

const commandModule: CommandModule<Options, Options> = {
command: "test-v2",

describe: "Run tests in MUD v2 contracts",

builder(yargs) {
return yargs.options({
...yDeployOptions,
port: { type: "number", description: "Port to run internal node for fork testing on", default: 4242 },
worldAddress: {
type: "string",
description:
"Address of an existing world contract. If provided, deployment is skipped and the RPC provided in the foundry.toml is used for fork testing.",
},
forgeOptions: { type: "string", description: "Options to pass to forge test" },
});
},

async handler(args) {
// Start an internal anvil process if no world address is provided
if (!args.worldAddress) {
const anvilArgs = ["--block-base-fee-per-gas", "0", "--port", String(args.port)];
anvil(anvilArgs);
}

const forkRpc = args.worldAddress ? await getRpcUrl(args.profile) : `http://127.0.0.1:${args.port}`;

const worldAddress =
args.worldAddress ??
(
await deployHandler({
...args,
saveDeployment: false,
rpc: forkRpc,
})
).worldAddress;

console.log(chalk.blue("World address", worldAddress));

// Create a temporary file to pass the world address to the tests
writeFileSync(WORLD_ADDRESS_FILE, worldAddress);

const userOptions = args.forgeOptions?.replaceAll("\\", "").split(" ") ?? [];
try {
const testResult = await forge(["test", "--fork-url", forkRpc, ...userOptions], {
profile: args.profile,
});
console.log(testResult);
} catch (e) {
console.error(e);
}

rmSync(WORLD_ADDRESS_FILE);

process.exit(0);
},
};

export default commandModule;
9 changes: 9 additions & 0 deletions packages/cli/src/utils/foundry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,12 @@ export async function cast(args: string[], options?: { profile?: string }): Prom
env: { FOUNDRY_PROFILE: options?.profile },
});
}

/**
* Start an anvil chain
* @param args The arguments to pass to anvil
* @returns Stdout of the command
*/
export async function anvil(args: string[]): Promise<string> {
return execLog("anvil", args);
}
12 changes: 12 additions & 0 deletions packages/std-contracts/src/test/MudV2Test.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import "forge-std/Test.sol";

contract MudV2Test is Test {
address worldAddress;

function setUp() public virtual {
worldAddress = vm.parseAddress(vm.readFile(".mudtest"));
}
}

0 comments on commit d6be8b0

Please sign in to comment.