From d7ea2a675db76e96bd2c882c3d35f4c9115c6b43 Mon Sep 17 00:00:00 2001 From: dk1a Date: Wed, 7 Jun 2023 21:59:12 +0300 Subject: [PATCH 1/8] feat(cli): add trace --- packages/cli/package.json | 1 + packages/cli/src/commands/index.ts | 2 + packages/cli/src/commands/trace.ts | 105 ++++++++++++++++++ packages/cli/src/commands/worldgen.ts | 10 +- packages/cli/src/utils/deploy.ts | 4 +- packages/cli/src/utils/deployHandler.ts | 11 +- .../cli/src/utils/getExistingContracts.ts | 12 ++ packages/cli/src/utils/index.ts | 6 +- pnpm-lock.yaml | 3 + 9 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 packages/cli/src/commands/trace.ts create mode 100644 packages/cli/src/utils/getExistingContracts.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 4177e12bf7..6191868e7a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ba044ff721..69177e2863 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -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 @@ -24,5 +25,6 @@ export const commands: CommandModule[] = [ worldgen, setVersion, test, + trace, devContracts, ]; diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts new file mode 100644 index 0000000000..0501683918 --- /dev/null +++ b/packages/cli/src/commands/trace.ts @@ -0,0 +1,105 @@ +import type { CommandModule } from "yargs"; +import { ethers } from "ethers"; +import { loadConfig } from "@latticexyz/config/node"; +import { StoreConfig } from "@latticexyz/store"; +import { cast, getSrcDirectory } from "@latticexyz/common/foundry"; +import { resolveWorldConfig, WorldConfig } from "@latticexyz/world"; +import { TableId } from "@latticexyz/utils"; +import { IBaseWorld } from "@latticexyz/world/types/ethers-contracts/IBaseWorld"; +import { getChainId, getExistingContracts } from "../utils"; + +import IBaseWorldData from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorld.json" assert { type: "json" }; +import { existsSync, readFileSync } from "fs"; +import { MUDError } from "@latticexyz/common/errors"; + +const systemsTableId = new TableId("", "Systems"); + +type Options = { + tx: string; + worldAddress?: string; + configPath?: string; + srcDir?: string; + rpc?: string; +}; + +const commandModule: CommandModule = { + 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 port", + }, + configPath: { type: "string", desc: "Path to the config file" }, + srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." }, + rpc: { type: "string", description: "json rpc endpoint, defaults to http://localhost:8545" }, + }); + }, + + async handler(args) { + const { tx, configPath, rpc } = args; + const srcDir = args.srcDir ?? (await getSrcDirectory()); + + 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 = await getWorldAddress(mudConfig.worldsFile, args.worldAddress, 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 tableId = 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(), [tableId.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, worldAddress?: string, rpc?: string) { + if (worldAddress) { + return worldAddress; + } else if (existsSync(worldsFile)) { + const chainId = rpc ? await getChainId(rpc) : 8545; + const deploys = JSON.parse(readFileSync(worldsFile, "utf-8")); + + if (!deploys[chainId]) { + throw new MUDError(`worldsFile is missing chainId ${chainId}`); + } + return deploys[chainId].address as string; + } else { + throw new MUDError("worldAddress is not specified and worldsFile is missing"); + } +} diff --git a/packages/cli/src/commands/worldgen.ts b/packages/cli/src/commands/worldgen.ts index 49ac781054..7d4212c1b5 100644 --- a/packages/cli/src/commands/worldgen.ts +++ b/packages/cli/src/commands/worldgen.ts @@ -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/getExistingContracts"; type Options = { configPath?: string; @@ -36,11 +36,7 @@ const commandModule: CommandModule = { 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); diff --git a/packages/cli/src/utils/deploy.ts b/packages/cli/src/utils/deploy.ts index e1ea413eef..e5f8d4ae10 100644 --- a/packages/cli/src/utils/deploy.ts +++ b/packages/cli/src/utils/deploy.ts @@ -38,10 +38,10 @@ export interface DeploymentInfo { export async function deploy( mudConfig: StoreConfig & WorldConfig, - existingContracts: string[], + existingContractNames: string[], deployConfig: DeployConfig ): Promise { - const resolvedConfig = resolveWorldConfig(mudConfig, existingContracts); + const resolvedConfig = resolveWorldConfig(mudConfig, existingContractNames); const startTime = Date.now(); const { worldContractName, namespace, postDeployScript } = mudConfig; diff --git a/packages/cli/src/utils/deployHandler.ts b/packages/cli/src/utils/deployHandler.ts index a264c4bdb0..9cfda06bdd 100644 --- a/packages/cli/src/utils/deployHandler.ts +++ b/packages/cli/src/utils/deployHandler.ts @@ -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"; @@ -9,6 +8,7 @@ import { deploy } from "../utils/deploy"; import { 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; @@ -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; @@ -56,7 +53,7 @@ export async function deployHandler(args: DeployOptions) { const privateKey = process.env.PRIVATE_KEY; if (!privateKey) throw new MUDError("Missing PRIVATE_KEY environment variable"); - 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) diff --git a/packages/cli/src/utils/getExistingContracts.ts b/packages/cli/src/utils/getExistingContracts.ts new file mode 100644 index 0000000000..ac3f67d982 --- /dev/null +++ b/packages/cli/src/utils/getExistingContracts.ts @@ -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"), + })); +} diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts index b1824ac827..3ac405bcf3 100644 --- a/packages/cli/src/utils/index.ts +++ b/packages/cli/src/utils/index.ts @@ -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"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8eadc0fca..64175fccee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: '@latticexyz/store': specifier: workspace:* version: link:../store + '@latticexyz/utils': + specifier: workspace:* + version: link:../utils '@latticexyz/world': specifier: workspace:* version: link:../world From 2cecdd35208044661a28fd6f9537abf33ec51533 Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 8 Jun 2023 16:14:39 +0300 Subject: [PATCH 2/8] Update packages/cli/src/commands/worldgen.ts Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> --- packages/cli/src/commands/worldgen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/worldgen.ts b/packages/cli/src/commands/worldgen.ts index 7d4212c1b5..0cee74c92d 100644 --- a/packages/cli/src/commands/worldgen.ts +++ b/packages/cli/src/commands/worldgen.ts @@ -6,7 +6,7 @@ import { worldgen } from "@latticexyz/world/node"; import { getSrcDirectory } from "@latticexyz/common/foundry"; import path from "path"; import { rmSync } from "fs"; -import { getExistingContracts } from "../utils/getExistingContracts"; +import { getExistingContracts } from "../utils"; type Options = { configPath?: string; From 693134e73fc3903e98d99071579c771de33d06c6 Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 8 Jun 2023 16:26:16 +0300 Subject: [PATCH 3/8] refactor(cli): fix spacing --- packages/cli/src/commands/trace.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts index 0501683918..c468238c45 100644 --- a/packages/cli/src/commands/trace.ts +++ b/packages/cli/src/commands/trace.ts @@ -1,16 +1,17 @@ +import { existsSync, readFileSync } from "fs"; import type { CommandModule } from "yargs"; import { ethers } from "ethers"; + import { loadConfig } from "@latticexyz/config/node"; -import { StoreConfig } from "@latticexyz/store"; +import { MUDError } from "@latticexyz/common/errors"; import { cast, getSrcDirectory } from "@latticexyz/common/foundry"; -import { resolveWorldConfig, WorldConfig } from "@latticexyz/world"; 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 { getChainId, getExistingContracts } from "../utils"; import IBaseWorldData from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorld.json" assert { type: "json" }; -import { existsSync, readFileSync } from "fs"; -import { MUDError } from "@latticexyz/common/errors"; const systemsTableId = new TableId("", "Systems"); From 19a9e6002d25e699e28de31207e712993869d415 Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 8 Jun 2023 16:26:27 +0300 Subject: [PATCH 4/8] refactor(cli): improve getWorldAddress --- packages/cli/src/commands/trace.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts index c468238c45..3db93b3e65 100644 --- a/packages/cli/src/commands/trace.ts +++ b/packages/cli/src/commands/trace.ts @@ -56,7 +56,7 @@ const commandModule: CommandModule = { ); // Get worldAddress either from args or from worldsFile - const worldAddress = await getWorldAddress(mudConfig.worldsFile, args.worldAddress, rpc); + const worldAddress = args.worldAddress ?? (await getWorldAddress(mudConfig.worldsFile, rpc)); // Create World contract instance from deployed address const provider = new ethers.providers.StaticJsonRpcProvider(rpc); @@ -89,10 +89,8 @@ const commandModule: CommandModule = { export default commandModule; -async function getWorldAddress(worldsFile: string, worldAddress?: string, rpc?: string) { - if (worldAddress) { - return worldAddress; - } else if (existsSync(worldsFile)) { +async function getWorldAddress(worldsFile: string, rpc?: string) { + if (existsSync(worldsFile)) { const chainId = rpc ? await getChainId(rpc) : 8545; const deploys = JSON.parse(readFileSync(worldsFile, "utf-8")); From d763e3e1ab1ec8149983432acac058f1192f9813 Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 8 Jun 2023 16:26:49 +0300 Subject: [PATCH 5/8] refactor(cli): use foundry config for rpc default --- packages/cli/src/commands/trace.ts | 17 +++++++++++------ packages/cli/src/utils/deployHandler.ts | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts index 3db93b3e65..aa1330fe0a 100644 --- a/packages/cli/src/commands/trace.ts +++ b/packages/cli/src/commands/trace.ts @@ -4,7 +4,7 @@ import { ethers } from "ethers"; import { loadConfig } from "@latticexyz/config/node"; import { MUDError } from "@latticexyz/common/errors"; -import { cast, getSrcDirectory } from "@latticexyz/common/foundry"; +import { cast, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry"; import { TableId } from "@latticexyz/utils"; import { StoreConfig } from "@latticexyz/store"; import { resolveWorldConfig, WorldConfig } from "@latticexyz/world"; @@ -19,6 +19,7 @@ type Options = { tx: string; worldAddress?: string; configPath?: string; + profile?: string; srcDir?: string; rpc?: string; }; @@ -36,14 +37,18 @@ const commandModule: CommandModule = { description: "World contract address. Defaults to the value from worlds.json, based on rpc port", }, configPath: { type: "string", desc: "Path to the config file" }, + profile: { type: "string", desc: "The foundry profile to use" }, srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." }, - rpc: { type: "string", description: "json rpc endpoint, defaults to http://localhost:8545" }, + rpc: { type: "string", description: "json rpc endpoint. Defaults to foundry's configured eth_rpc_url" }, }); }, async handler(args) { - const { tx, configPath, rpc } = args; - const srcDir = args.srcDir ?? (await getSrcDirectory()); + 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); @@ -89,9 +94,9 @@ const commandModule: CommandModule = { export default commandModule; -async function getWorldAddress(worldsFile: string, rpc?: string) { +async function getWorldAddress(worldsFile: string, rpc: string) { if (existsSync(worldsFile)) { - const chainId = rpc ? await getChainId(rpc) : 8545; + const chainId = await getChainId(rpc); const deploys = JSON.parse(readFileSync(worldsFile, "utf-8")); if (!deploys[chainId]) { diff --git a/packages/cli/src/utils/deployHandler.ts b/packages/cli/src/utils/deployHandler.ts index 9cfda06bdd..ce513e688b 100644 --- a/packages/cli/src/utils/deployHandler.ts +++ b/packages/cli/src/utils/deployHandler.ts @@ -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)); From 083f3c5a0d13e7501ff75dde30563a7b5b93b922 Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 8 Jun 2023 16:27:14 +0300 Subject: [PATCH 6/8] fix(cli): fix option descriptions for trace --- packages/cli/src/commands/trace.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts index aa1330fe0a..133993a2c3 100644 --- a/packages/cli/src/commands/trace.ts +++ b/packages/cli/src/commands/trace.ts @@ -34,11 +34,11 @@ const commandModule: CommandModule = { 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 port", + description: "World contract address. Defaults to the value from worlds.json, based on rpc's chainId", }, - configPath: { type: "string", desc: "Path to the config file" }, - profile: { type: "string", desc: "The foundry profile to use" }, - srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." }, + 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" }, }); }, From 72d80658add10ee55d36052f360dfc14f02f1fee Mon Sep 17 00:00:00 2001 From: dk1a Date: Thu, 8 Jun 2023 16:27:26 +0300 Subject: [PATCH 7/8] fix(cli): improve missing chainId error text --- packages/cli/src/commands/trace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts index 133993a2c3..59e3f57d4d 100644 --- a/packages/cli/src/commands/trace.ts +++ b/packages/cli/src/commands/trace.ts @@ -100,7 +100,7 @@ async function getWorldAddress(worldsFile: string, rpc: string) { const deploys = JSON.parse(readFileSync(worldsFile, "utf-8")); if (!deploys[chainId]) { - throw new MUDError(`worldsFile is missing chainId ${chainId}`); + throw new MUDError(`chainId ${chainId} is missing in worldsFile "${worldsFile}"`); } return deploys[chainId].address as string; } else { From 90d8d89f52f168cdb33805d0f4d90c28aaf61470 Mon Sep 17 00:00:00 2001 From: alvrs Date: Fri, 16 Jun 2023 18:23:57 +0200 Subject: [PATCH 8/8] review --- packages/cli/src/commands/trace.ts | 7 +++---- packages/cli/src/utils/deploy.ts | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/trace.ts b/packages/cli/src/commands/trace.ts index 59e3f57d4d..2a95d1b4ba 100644 --- a/packages/cli/src/commands/trace.ts +++ b/packages/cli/src/commands/trace.ts @@ -9,9 +9,8 @@ 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 { getChainId, getExistingContracts } from "../utils"; - import IBaseWorldData from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorld.json" assert { type: "json" }; +import { getChainId, getExistingContracts } from "../utils"; const systemsTableId = new TableId("", "Systems"); @@ -73,9 +72,9 @@ const commandModule: CommandModule = { const labels: { name: string; address: string }[] = []; for (const name of names) { - const tableId = new TableId(namespace, name); + 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(), [tableId.toHexString()], 0); + const address = await WorldContract.getField(systemsTableId.toHexString(), [systemSelector.toHexString()], 0); labels.push({ name, address }); } diff --git a/packages/cli/src/utils/deploy.ts b/packages/cli/src/utils/deploy.ts index d3cae39b8e..018f38effe 100644 --- a/packages/cli/src/utils/deploy.ts +++ b/packages/cli/src/utils/deploy.ts @@ -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" };