Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add trace #999

Merged
merged 9 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@latticexyz/solecs": "workspace:*",
"@latticexyz/std-contracts": "workspace:*",
"@latticexyz/store": "workspace:*",
"@latticexyz/utils": "workspace:*",
"@latticexyz/world": "workspace:*",
"@typechain/ethers-v5": "^10.2.0",
"chalk": "^5.0.1",
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 @@ -10,6 +10,7 @@ import deploy from "./deploy";
import worldgen from "./worldgen";
import setVersion from "./set-version";
import test from "./test";
import trace from "./trace";
import devContracts from "./dev-contracts";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
Expand All @@ -24,5 +25,6 @@ export const commands: CommandModule<any, any>[] = [
worldgen,
setVersion,
test,
trace,
devContracts,
];
108 changes: 108 additions & 0 deletions packages/cli/src/commands/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { existsSync, readFileSync } from "fs";
import type { CommandModule } from "yargs";
import { ethers } from "ethers";

import { loadConfig } from "@latticexyz/config/node";
import { MUDError } from "@latticexyz/common/errors";
import { cast, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
import { TableId } from "@latticexyz/utils";
import { StoreConfig } from "@latticexyz/store";
import { resolveWorldConfig, WorldConfig } from "@latticexyz/world";
import { IBaseWorld } from "@latticexyz/world/types/ethers-contracts/IBaseWorld";
import IBaseWorldData from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorld.json" assert { type: "json" };
import { getChainId, getExistingContracts } from "../utils";

const systemsTableId = new TableId("", "Systems");

type Options = {
tx: string;
worldAddress?: string;
configPath?: string;
profile?: string;
srcDir?: string;
rpc?: string;
};

const commandModule: CommandModule<Options, Options> = {
command: "trace",

describe: "Display the trace of a transaction",

builder(yargs) {
return yargs.options({
tx: { type: "string", required: true, description: "Transaction hash to replay" },
worldAddress: {
type: "string",
description: "World contract address. Defaults to the value from worlds.json, based on rpc's chainId",
},
configPath: { type: "string", description: "Path to the config file" },
profile: { type: "string", description: "The foundry profile to use" },
srcDir: { type: "string", description: "Source directory. Defaults to foundry src directory." },
rpc: { type: "string", description: "json rpc endpoint. Defaults to foundry's configured eth_rpc_url" },
});
},

async handler(args) {
args.profile ??= process.env.FOUNDRY_PROFILE;
const { profile } = args;
args.srcDir ??= await getSrcDirectory(profile);
args.rpc ??= await getRpcUrl(profile);
const { tx, configPath, srcDir, rpc } = args;

const existingContracts = getExistingContracts(srcDir);

// Load the config
const mudConfig = (await loadConfig(configPath)) as StoreConfig & WorldConfig;

const resolvedConfig = resolveWorldConfig(
mudConfig,
existingContracts.map(({ basename }) => basename)
);

// Get worldAddress either from args or from worldsFile
const worldAddress = args.worldAddress ?? (await getWorldAddress(mudConfig.worldsFile, rpc));

// Create World contract instance from deployed address
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const WorldContract = new ethers.Contract(worldAddress, IBaseWorldData.abi, provider) as IBaseWorld;

// TODO account for multiple namespaces (https://github.com/latticexyz/mud/issues/994)
const namespace = mudConfig.namespace;
const names = Object.values(resolvedConfig.systems).map(({ name }) => name);

const labels: { name: string; address: string }[] = [];
for (const name of names) {
const systemSelector = new TableId(namespace, name);
// Get the first field of `Systems` table (the table maps system name to its address and other data)
const address = await WorldContract.getField(systemsTableId.toHexString(), [systemSelector.toHexString()], 0);
labels.push({ name, address });
}

const result = await cast([
"run",
"--label",
`${worldAddress}:World`,
...labels.map(({ name, address }) => ["--label", `${address}:${name}`]).flat(),
`${tx}`,
]);
console.log(result);

process.exit(0);
},
};

export default commandModule;

async function getWorldAddress(worldsFile: string, rpc: string) {
if (existsSync(worldsFile)) {
const chainId = await getChainId(rpc);
const deploys = JSON.parse(readFileSync(worldsFile, "utf-8"));

if (!deploys[chainId]) {
throw new MUDError(`chainId ${chainId} is missing in worldsFile "${worldsFile}"`);
}
return deploys[chainId].address as string;
} else {
throw new MUDError("worldAddress is not specified and worldsFile is missing");
}
}
10 changes: 3 additions & 7 deletions packages/cli/src/commands/worldgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { StoreConfig } from "@latticexyz/store";
import { WorldConfig } from "@latticexyz/world";
import { worldgen } from "@latticexyz/world/node";
import { getSrcDirectory } from "@latticexyz/common/foundry";
import glob from "glob";
import path, { basename } from "path";
import path from "path";
import { rmSync } from "fs";
import { getExistingContracts } from "../utils";

type Options = {
configPath?: string;
Expand Down Expand Up @@ -36,11 +36,7 @@ const commandModule: CommandModule<Options, Options> = {
export async function worldgenHandler(args: Options) {
const srcDir = args.srcDir ?? (await getSrcDirectory());

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

// Load the config
const mudConfig = args.config ?? ((await loadConfig(args.configPath)) as StoreConfig & WorldConfig);
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/src/utils/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { StoreConfig } from "@latticexyz/store";
import { resolveAbiOrUserType } from "@latticexyz/store/codegen";
import { WorldConfig, resolveWorldConfig } from "@latticexyz/world";
import { IBaseWorld } from "@latticexyz/world/types/ethers-contracts/IBaseWorld";

import WorldData from "@latticexyz/world/abi/World.sol/World.json" assert { type: "json" };
import IBaseWorldData from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorld.json" assert { type: "json" };
import CoreModuleData from "@latticexyz/world/abi/CoreModule.sol/CoreModule.json" assert { type: "json" };
Expand All @@ -39,10 +38,10 @@ export interface DeploymentInfo {

export async function deploy(
mudConfig: StoreConfig & WorldConfig,
existingContracts: string[],
existingContractNames: string[],
deployConfig: DeployConfig
): Promise<DeploymentInfo> {
const resolvedConfig = resolveWorldConfig(mudConfig, existingContracts);
const resolvedConfig = resolveWorldConfig(mudConfig, existingContractNames);

const startTime = Date.now();
const { worldContractName, namespace, postDeployScript } = mudConfig;
Expand Down
13 changes: 5 additions & 8 deletions packages/cli/src/utils/deployHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import chalk from "chalk";
import glob from "glob";
import path, { basename } from "path";
import path from "path";
import { MUDError } from "@latticexyz/common/errors";
import { loadConfig } from "@latticexyz/config/node";
import { StoreConfig } from "@latticexyz/store";
Expand All @@ -9,6 +8,7 @@ import { deploy } from "../utils/deploy";
import { cast, forge, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { getChainId } from "../utils/getChainId";
import { getExistingContracts } from "./getExistingContracts";

export type DeployOptions = {
configPath?: string;
Expand All @@ -27,7 +27,7 @@ export type DeployOptions = {
};

export async function deployHandler(args: DeployOptions) {
args.profile = args.profile ?? process.env.FOUNDRY_PROFILE;
args.profile ??= process.env.FOUNDRY_PROFILE;
const { configPath, printConfig, profile, clean, skipBuild } = args;

const rpc = args.rpc ?? (await getRpcUrl(profile));
Expand All @@ -44,10 +44,7 @@ export async function deployHandler(args: DeployOptions) {

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

// Load the config
const mudConfig = (await loadConfig(configPath)) as StoreConfig & WorldConfig;
Expand All @@ -61,7 +58,7 @@ export async function deployHandler(args: DeployOptions) {
Run 'echo "PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" > .env'
in your contracts directory to use the default anvil private key.`
);
const deploymentInfo = await deploy(mudConfig, existingContracts, { ...args, rpc, privateKey });
const deploymentInfo = await deploy(mudConfig, existingContractNames, { ...args, rpc, privateKey });

if (args.saveDeployment) {
// Write deployment result to file (latest and timestamp)
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/utils/getExistingContracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import glob from "glob";
import { basename } from "path";

/**
* Get a list of all contract paths/names within the provided src directory
*/
export function getExistingContracts(srcDir: string) {
return glob.sync(`${srcDir}/**/*.sol`).map((path) => ({
path,
basename: basename(path, ".sol"),
}));
}
6 changes: 4 additions & 2 deletions packages/cli/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from "./errors";
export * from "./deploy";
export * from "./deployHandler";
export * from "./worldtypes";
export * from "./errors";
export * from "./getChainId";
export * from "./getExistingContracts";
export * from "./printMUD";
export * from "./worldtypes";
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.