From dc9e1cb1f0f486f89f95067f5b03df1e8dbfc1fb Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 11 Sep 2024 15:30:04 +0100 Subject: [PATCH 01/14] feat(cli): mud pull --- packages/cli/package.json | 1 + packages/cli/src/commands/index.ts | 2 + packages/cli/src/commands/pull.ts | 48 ++++++++++++++++ packages/cli/src/pull/debug.ts | 10 ++++ packages/cli/src/pull/pull.ts | 89 ++++++++++++++++++++++++++++++ pnpm-lock.yaml | 47 ++++++++-------- 6 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 packages/cli/src/commands/pull.ts create mode 100644 packages/cli/src/pull/debug.ts create mode 100644 packages/cli/src/pull/pull.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 84a1941283..2160df8550 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -64,6 +64,7 @@ "p-queue": "^7.4.1", "p-retry": "^5.1.2", "path": "^0.12.7", + "prettier": "3.2.5", "rxjs": "7.5.5", "throttle-debounce": "^5.0.0", "toposort": "^2.0.2", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index e70e0a89c3..84ffef5bb6 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -14,6 +14,7 @@ import test from "./test"; import trace from "./trace"; import devContracts from "./dev-contracts"; import verify from "./verify"; +import pull from "./pull"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options export const commands: CommandModule[] = [ @@ -30,4 +31,5 @@ export const commands: CommandModule[] = [ devContracts, abiTs, verify, + pull, ]; diff --git a/packages/cli/src/commands/pull.ts b/packages/cli/src/commands/pull.ts new file mode 100644 index 0000000000..d2d69bdbca --- /dev/null +++ b/packages/cli/src/commands/pull.ts @@ -0,0 +1,48 @@ +import type { CommandModule, InferredOptionTypes } from "yargs"; +import { getRpcUrl } from "@latticexyz/common/foundry"; +import { Address, createClient, http } from "viem"; +import chalk from "chalk"; +import { pull } from "../pull/pull"; + +const options = { + worldAddress: { type: "string", required: true, desc: "World address" }, + profile: { type: "string", desc: "The foundry profile to use" }, + rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" }, + rpcBatch: { + type: "boolean", + desc: "Enable batch processing of RPC requests in viem client (defaults to batch size of 100 and wait of 1s)", + }, +} as const; + +type Options = InferredOptionTypes; + +const commandModule: CommandModule = { + command: "pull", + + describe: "Pull mud.config.ts and interfaces from an existing world.", + + builder(yargs) { + return yargs.options(options); + }, + + async handler(opts) { + const profile = opts.profile ?? process.env.FOUNDRY_PROFILE; + const rpc = opts.rpc ?? (await getRpcUrl(profile)); + const client = createClient({ + transport: http(rpc, { + batch: opts.rpcBatch + ? { + batchSize: 100, + wait: 1000, + } + : undefined, + }), + }); + + console.log(chalk.bgBlue(chalk.whiteBright(`\n Pulling MUD config from world at ${opts.worldAddress} \n`))); + + await pull({ rootDir: process.cwd(), client, worldAddress: opts.worldAddress as Address }); + }, +}; + +export default commandModule; diff --git a/packages/cli/src/pull/debug.ts b/packages/cli/src/pull/debug.ts new file mode 100644 index 0000000000..9fb89d2ba5 --- /dev/null +++ b/packages/cli/src/pull/debug.ts @@ -0,0 +1,10 @@ +import { debug as parentDebug } from "../debug"; + +export const debug = parentDebug.extend("pull"); +export const error = parentDebug.extend("pull"); + +// Pipe debug output to stdout instead of stderr +debug.log = console.debug.bind(console); + +// Pipe error output to stderr +error.log = console.error.bind(console); diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts new file mode 100644 index 0000000000..f0c6c7b9ea --- /dev/null +++ b/packages/cli/src/pull/pull.ts @@ -0,0 +1,89 @@ +import { Address, Client, hexToString, stringToHex } from "viem"; +import { getTables } from "../deploy/getTables"; +import { getWorldDeploy } from "../deploy/getWorldDeploy"; +import { unique } from "@latticexyz/common/utils"; +import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; +import prettier from "prettier"; +import { resourceToHex } from "@latticexyz/common"; +import metadataConfig from "@latticexyz/world-module-metadata/mud.config"; +import { getRecord } from "../deploy/getRecord"; +import path from "node:path"; +import fs from "node:fs/promises"; + +export type PullOptions = { + rootDir: string; + client: Client; + worldAddress: Address; +}; + +export async function pull({ rootDir, client, worldAddress }: PullOptions) { + const worldDeploy = await getWorldDeploy(client, worldAddress); + const tables = await getTables({ client, worldDeploy }); + + const ignoredNamespaces = new Set(["store", "world"]); + const namespaces = unique(tables.map((table) => table.namespace)) + .filter((namespace) => !ignoredNamespaces.has(namespace)) + .map((namespace) => ({ namespace, namespaceId: resourceToHex({ type: "namespace", namespace, name: "" }) })); + + const resourceIds = [ + ...namespaces.map((namespace) => namespace.namespaceId), + ...tables.map((table) => table.tableId), + ]; + + const labels = Object.fromEntries( + await Promise.all( + resourceIds.map(async (resourceId) => { + const { value: bytesValue } = await getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: resourceId, tag: stringToHex("label", { size: 32 }) }, + }); + const value = hexToString(bytesValue); + return [resourceId, value === "" ? null : value]; + }), + ), + ); + + const config = { + namespaces: Object.fromEntries( + namespaces.map(({ namespace, namespaceId }) => { + const namespaceLabel = labels[namespaceId] ?? namespace; + return [ + namespaceLabel, + { + ...(namespaceLabel !== namespace ? { namespace } : null), + tables: Object.fromEntries( + tables + .filter((table) => table.namespace === namespace) + .map((table) => { + const tableLabel = labels[table.tableId] ?? table.name; + return [ + tableLabel, + { + ...(tableLabel !== table.name ? { name: table.name } : null), + ...(table.type !== "table" ? { type: table.type } : null), + schema: getSchemaTypes(table.schema), + key: table.key, + }, + ]; + }), + ), + }, + ]; + }), + ), + }; + + const configSource = ` + import { defineWorld } from "@latticexyz/world"; + + export default defineWorld(${JSON.stringify(config)}); + `; + + const formattedConfig = await prettier.format(configSource, { + parser: "typescript", + }); + + await fs.writeFile(path.join(rootDir, "mud.config.ts"), formattedConfig); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d349fb7f4..bd9b3388f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,7 +110,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/block-logs-stream: dependencies: @@ -138,7 +138,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/cli: dependencies: @@ -217,6 +217,9 @@ importers: path: specifier: ^0.12.7 version: 0.12.7 + prettier: + specifier: 3.2.5 + version: 3.2.5 rxjs: specifier: 7.5.5 version: 7.5.5 @@ -277,7 +280,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/common: dependencies: @@ -329,7 +332,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/config: dependencies: @@ -443,7 +446,7 @@ importers: version: 6.7.0(postcss@8.4.23)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/explorer: dependencies: @@ -640,7 +643,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/gas-report: dependencies: @@ -686,7 +689,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/protocol-parser: dependencies: @@ -711,7 +714,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/react: dependencies: @@ -763,7 +766,7 @@ importers: version: 4.3.6(@types/node@20.12.12)(terser@5.31.6) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/recs: dependencies: @@ -788,10 +791,10 @@ importers: version: 8.3.4 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@18.15.11) + version: 29.5.0(@types/node@20.12.12) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@18.15.11))(typescript@5.4.2) + version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@20.12.12))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) @@ -822,7 +825,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/solhint-config-mud: devDependencies: @@ -887,7 +890,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/store: dependencies: @@ -948,7 +951,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/store-indexer: dependencies: @@ -1063,7 +1066,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/store-sync: dependencies: @@ -1157,7 +1160,7 @@ importers: version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/utils: dependencies: @@ -1176,10 +1179,10 @@ importers: version: 27.4.1 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@20.12.12) + version: 29.5.0(@types/node@18.15.11) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@20.12.12))(typescript@5.4.2) + version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@18.15.11))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) @@ -1258,7 +1261,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/world-module-metadata: dependencies: @@ -1298,7 +1301,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) packages/world-modules: dependencies: @@ -1359,7 +1362,7 @@ importers: version: 3.12.6 vitest: specifier: 0.34.6 - version: 0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6) + version: 0.34.6(jsdom@22.1.0)(terser@5.31.6) test/mock-game-contracts: devDependencies: @@ -22419,7 +22422,7 @@ snapshots: fsevents: 2.3.3 terser: 5.31.6 - vitest@0.34.6(jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.6): + vitest@0.34.6(jsdom@22.1.0)(terser@5.31.6): dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 From 02bfe910844683915ee9324663959756cae67ea7 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 11 Sep 2024 15:34:31 +0100 Subject: [PATCH 02/14] disable deploy on tables --- packages/cli/src/pull/pull.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index f0c6c7b9ea..2b12e9087d 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -65,6 +65,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ...(table.type !== "table" ? { type: table.type } : null), schema: getSchemaTypes(table.schema), key: table.key, + deploy: { disabled: true }, }, ]; }), From 8769f60a9facd76c49343cbd1c0f6832d794c90b Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 10:25:05 +0100 Subject: [PATCH 03/14] first pass at interfaces --- packages/cli/package.json | 1 - packages/cli/src/pull/pull.ts | 117 +++++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 2160df8550..84a1941283 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -64,7 +64,6 @@ "p-queue": "^7.4.1", "p-retry": "^5.1.2", "path": "^0.12.7", - "prettier": "3.2.5", "rxjs": "7.5.5", "throttle-debounce": "^5.0.0", "toposort": "^2.0.2", diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 2b12e9087d..8fee9405b0 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -1,14 +1,18 @@ -import { Address, Client, hexToString, stringToHex } from "viem"; +import { Address, Client, hexToString, parseAbi, stringToHex } from "viem"; import { getTables } from "../deploy/getTables"; import { getWorldDeploy } from "../deploy/getWorldDeploy"; -import { unique } from "@latticexyz/common/utils"; import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; -import prettier from "prettier"; -import { resourceToHex } from "@latticexyz/common"; +import { hexToResource, resourceToHex } from "@latticexyz/common"; import metadataConfig from "@latticexyz/world-module-metadata/mud.config"; import { getRecord } from "../deploy/getRecord"; import path from "node:path"; import fs from "node:fs/promises"; +import { getResourceIds } from "../deploy/getResourceIds"; +import { getFunctions } from "@latticexyz/world/internal"; +import { formatTypescript } from "@latticexyz/common/codegen"; +import { execa } from "execa"; + +const ignoredNamespaces = new Set(["store", "world", "metadata"]); export type PullOptions = { rootDir: string; @@ -18,18 +22,10 @@ export type PullOptions = { export async function pull({ rootDir, client, worldAddress }: PullOptions) { const worldDeploy = await getWorldDeploy(client, worldAddress); + const resourceIds = await getResourceIds({ client, worldDeploy }); + const resources = resourceIds.map(hexToResource).filter((resource) => !ignoredNamespaces.has(resource.namespace)); const tables = await getTables({ client, worldDeploy }); - const ignoredNamespaces = new Set(["store", "world"]); - const namespaces = unique(tables.map((table) => table.namespace)) - .filter((namespace) => !ignoredNamespaces.has(namespace)) - .map((namespace) => ({ namespace, namespaceId: resourceToHex({ type: "namespace", namespace, name: "" }) })); - - const resourceIds = [ - ...namespaces.map((namespace) => namespace.namespaceId), - ...tables.map((table) => table.tableId), - ]; - const labels = Object.fromEntries( await Promise.all( resourceIds.map(async (resourceId) => { @@ -45,9 +41,59 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ), ); + const namespaces = resources.filter((resource) => resource.type === "namespace"); + + const worldFunctions = await getFunctions({ + client, + worldAddress: worldDeploy.address, + fromBlock: worldDeploy.deployBlock, + toBlock: worldDeploy.stateBlock, + }); + + const systems = await Promise.all( + resources + .filter((resource) => resource.type === "system") + .map(async ({ namespace, name, resourceId: systemId }) => { + const [abi, worldAbi] = await Promise.all([ + getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: systemId, tag: stringToHex("abi", { size: 32 }) }, + }) + .then((record) => hexToString(record.value)) + .then((value) => (value !== "" ? value.split("\n") : [])), + getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: systemId, tag: stringToHex("worldAbi", { size: 32 }) }, + }) + .then((record) => hexToString(record.value)) + .then((value) => (value !== "" ? value.split("\n") : [])), + ]); + + const functions = worldFunctions.filter((func) => func.systemId === systemId); + + const namespaceId = resourceToHex({ type: "namespace", namespace, name }); + return { + namespaceId, + namespaceLabel: labels[namespaceId] ?? namespace, + label: labels[systemId] ?? name, + systemId, + namespace, + name, + // If empty or unset ABI in metadata table, backfill with world functions. + // These don't have parameter names or return values, but better than nothing? + abi: parseAbi(abi.length ? abi : functions.map((func) => `function ${func.systemFunctionSignature}`)), + worldAbi: parseAbi(worldAbi.length ? worldAbi : functions.map((func) => `function ${func.signature}`)), + }; + }), + ); + const config = { namespaces: Object.fromEntries( - namespaces.map(({ namespace, namespaceId }) => { + namespaces.map(({ namespace, resourceId: namespaceId }) => { const namespaceLabel = labels[namespaceId] ?? namespace; return [ namespaceLabel, @@ -76,15 +122,40 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ), }; - const configSource = ` - import { defineWorld } from "@latticexyz/world"; + await writeFile( + path.join(rootDir, "tmp/mud.config.ts"), + await formatTypescript(` + import { defineWorld } from "@latticexyz/world"; - export default defineWorld(${JSON.stringify(config)}); - `; + export default defineWorld(${JSON.stringify(config)}); + `), + ); - const formattedConfig = await prettier.format(configSource, { - parser: "typescript", - }); + await Promise.all( + systems + .filter((system) => system.abi.length) + .map(async (system) => { + const interfaceName = `I${system.label}`; + const systemAbi = path.join(rootDir, `.mud/systems/${system.systemId}.abi.json`); + const systemInterface = path.join(rootDir, `tmp/namespaces/${system.namespaceLabel}/${interfaceName}.sol`); + + await writeFile(systemAbi, JSON.stringify(system.abi)); + + await execa("cast", [ + "interface", + "--name", + interfaceName, + "--pragma", + ">=0.8.24", + "-o", + systemInterface, + systemAbi, + ]); + }), + ); +} - await fs.writeFile(path.join(rootDir, "mud.config.ts"), formattedConfig); +async function writeFile(filename: string, contents: string) { + await fs.mkdir(path.dirname(filename), { recursive: true }); + await fs.writeFile(filename, contents); } From 58cb6224006e5002da3629690ebce0938d169f27 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 11:23:28 +0100 Subject: [PATCH 04/14] filter out tuples for now --- packages/cli/src/pull/pull.ts | 47 ++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 8fee9405b0..96936b128f 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -1,4 +1,4 @@ -import { Address, Client, hexToString, parseAbi, stringToHex } from "viem"; +import { AbiItem, Address, Client, hexToString, parseAbi, stringToHex } from "viem"; import { getTables } from "../deploy/getTables"; import { getWorldDeploy } from "../deploy/getWorldDeploy"; import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; @@ -11,9 +11,20 @@ import { getResourceIds } from "../deploy/getResourceIds"; import { getFunctions } from "@latticexyz/world/internal"; import { formatTypescript } from "@latticexyz/common/codegen"; import { execa } from "execa"; +import { debug } from "./debug"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); +function excludeFunctionsWithTuples(item: AbiItem) { + if ( + item.type === "function" && + [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") + ) { + return false; + } + return true; +} + export type PullOptions = { rootDir: string; client: Client; @@ -54,7 +65,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { resources .filter((resource) => resource.type === "system") .map(async ({ namespace, name, resourceId: systemId }) => { - const [abi, worldAbi] = await Promise.all([ + const [metadataAbi, metadataWorldAbi] = await Promise.all([ getRecord({ client, worldDeploy, @@ -75,6 +86,30 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const functions = worldFunctions.filter((func) => func.systemId === systemId); + console.log( + "system", + `${namespace}:${name}`, + JSON.stringify(parseAbi(functions.map((func) => `function ${func.signature}`)), null, 2), + ); + + // If empty or unset ABI in metadata table, backfill with world functions. + // These don't have parameter names or return values, but better than nothing? + const abi = parseAbi( + metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), + ) + // skip functions that use tuples for now, because they can't compile unless they're represented as structs + // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct + // of the same shape + .filter(excludeFunctionsWithTuples); + + const worldAbi = parseAbi( + metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), + ) + // skip functions that use tuples for now, because they can't compile unless they're represented as structs + // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct + // of the same shape + .filter(excludeFunctionsWithTuples); + const namespaceId = resourceToHex({ type: "namespace", namespace, name }); return { namespaceId, @@ -83,10 +118,8 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { systemId, namespace, name, - // If empty or unset ABI in metadata table, backfill with world functions. - // These don't have parameter names or return values, but better than nothing? - abi: parseAbi(abi.length ? abi : functions.map((func) => `function ${func.systemFunctionSignature}`)), - worldAbi: parseAbi(worldAbi.length ? worldAbi : functions.map((func) => `function ${func.signature}`)), + abi, + worldAbi, }; }), ); @@ -139,8 +172,10 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const systemAbi = path.join(rootDir, `.mud/systems/${system.systemId}.abi.json`); const systemInterface = path.join(rootDir, `tmp/namespaces/${system.namespaceLabel}/${interfaceName}.sol`); + debug("writing system ABI for", interfaceName, "to", systemAbi); await writeFile(systemAbi, JSON.stringify(system.abi)); + debug("generating system interface", interfaceName, "to", systemInterface); await execa("cast", [ "interface", "--name", From 2c96a8404f5e2ca00fd4a7b5de216d75ae0826b1 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 15:49:34 +0100 Subject: [PATCH 05/14] seeing if they compile --- packages/cli/src/pull/pull.ts | 93 ++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 96936b128f..7521b63a1a 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -12,17 +12,12 @@ import { getFunctions } from "@latticexyz/world/internal"; import { formatTypescript } from "@latticexyz/common/codegen"; import { execa } from "execa"; import { debug } from "./debug"; +import { formatAbiItem } from "abitype"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); -function excludeFunctionsWithTuples(item: AbiItem) { - if ( - item.type === "function" && - [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") - ) { - return false; - } - return true; +function namespaceToHex(namespace: string) { + return resourceToHex({ type: "namespace", namespace, name: "" }); } export type PullOptions = { @@ -38,20 +33,25 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const tables = await getTables({ client, worldDeploy }); const labels = Object.fromEntries( - await Promise.all( - resourceIds.map(async (resourceId) => { - const { value: bytesValue } = await getRecord({ - client, - worldDeploy, - table: metadataConfig.tables.metadata__ResourceTag, - key: { resource: resourceId, tag: stringToHex("label", { size: 32 }) }, - }); - const value = hexToString(bytesValue); - return [resourceId, value === "" ? null : value]; - }), - ), + ( + await Promise.all( + resourceIds.map(async (resourceId) => { + const { value: bytesValue } = await getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: resourceId, tag: stringToHex("label", { size: 32 }) }, + }); + const value = hexToString(bytesValue); + return [resourceId, value === "" ? null : value]; + }), + ) + ).filter(([, label]) => label != null), ); + // ensure we always have a root namespace label + labels[namespaceToHex("")] ??= "root"; + console.log(labels); const namespaces = resources.filter((resource) => resource.type === "namespace"); const worldFunctions = await getFunctions({ @@ -65,6 +65,12 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { resources .filter((resource) => resource.type === "system") .map(async ({ namespace, name, resourceId: systemId }) => { + const namespaceId = namespaceToHex(namespace); + // the system name from the system ID can be potentially truncated, so we'll strip off + // any partial "System" suffix and replace it with a full "System" suffix so that it + // matches our criteria for system names + const label = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); + const [metadataAbi, metadataWorldAbi] = await Promise.all([ getRecord({ client, @@ -86,35 +92,36 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const functions = worldFunctions.filter((func) => func.systemId === systemId); - console.log( - "system", - `${namespace}:${name}`, - JSON.stringify(parseAbi(functions.map((func) => `function ${func.signature}`)), null, 2), - ); + // skip functions that use tuples for now, because they can't compile unless they're represented as structs + // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct + // of the same shape + function excludeFunctionsWithTuples(item: AbiItem) { + if ( + item.type === "function" && + [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") + ) { + debug( + `System function \`${label}.${item.name}\` is using a tuple argument or return type, which is not yet supported by interface generation. Skipping...`, + ); + return false; + } + return true; + } // If empty or unset ABI in metadata table, backfill with world functions. // These don't have parameter names or return values, but better than nothing? const abi = parseAbi( metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), - ) - // skip functions that use tuples for now, because they can't compile unless they're represented as structs - // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct - // of the same shape - .filter(excludeFunctionsWithTuples); + ).filter(excludeFunctionsWithTuples); const worldAbi = parseAbi( metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), - ) - // skip functions that use tuples for now, because they can't compile unless they're represented as structs - // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct - // of the same shape - .filter(excludeFunctionsWithTuples); + ).filter(excludeFunctionsWithTuples); - const namespaceId = resourceToHex({ type: "namespace", namespace, name }); return { namespaceId, namespaceLabel: labels[namespaceId] ?? namespace, - label: labels[systemId] ?? name, + label, systemId, namespace, name, @@ -156,7 +163,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { }; await writeFile( - path.join(rootDir, "tmp/mud.config.ts"), + path.join(rootDir, "mud.config.ts"), await formatTypescript(` import { defineWorld } from "@latticexyz/world"; @@ -170,12 +177,12 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { .map(async (system) => { const interfaceName = `I${system.label}`; const systemAbi = path.join(rootDir, `.mud/systems/${system.systemId}.abi.json`); - const systemInterface = path.join(rootDir, `tmp/namespaces/${system.namespaceLabel}/${interfaceName}.sol`); + const systemInterface = path.join(rootDir, `src/namespaces/${system.namespaceLabel}/${interfaceName}.sol`); - debug("writing system ABI for", interfaceName, "to", systemAbi); + debug("writing system ABI for", interfaceName, "to", path.relative(rootDir, systemAbi)); await writeFile(systemAbi, JSON.stringify(system.abi)); - debug("generating system interface", interfaceName, "to", systemInterface); + debug("generating system interface", interfaceName, "to", path.relative(rootDir, systemInterface)); await execa("cast", [ "interface", "--name", @@ -188,6 +195,10 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ]); }), ); + + // const abi = systems.flatMap((system) => system.worldAbi); + // const worldAbi = path.join(rootDir, `.mud/systems/world.abi.json`); + // const worldInterface = path.join(rootDir, `tmp/`) } async function writeFile(filename: string, contents: string) { From 3c54a91b842824730c57a8db47ebbfff3c66a833 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Sep 2024 17:24:06 +0100 Subject: [PATCH 06/14] world systems interface --- packages/cli/src/pull/pull.ts | 89 +++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 7521b63a1a..544d511cde 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -1,4 +1,4 @@ -import { AbiItem, Address, Client, hexToString, parseAbi, stringToHex } from "viem"; +import { Address, Client, hexToString, parseAbi, stringToHex } from "viem"; import { getTables } from "../deploy/getTables"; import { getWorldDeploy } from "../deploy/getWorldDeploy"; import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; @@ -12,7 +12,6 @@ import { getFunctions } from "@latticexyz/world/internal"; import { formatTypescript } from "@latticexyz/common/codegen"; import { execa } from "execa"; import { debug } from "./debug"; -import { formatAbiItem } from "abitype"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); @@ -69,7 +68,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { // the system name from the system ID can be potentially truncated, so we'll strip off // any partial "System" suffix and replace it with a full "System" suffix so that it // matches our criteria for system names - const label = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); + const systemLabel = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); const [metadataAbi, metadataWorldAbi] = await Promise.all([ getRecord({ @@ -92,36 +91,19 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const functions = worldFunctions.filter((func) => func.systemId === systemId); - // skip functions that use tuples for now, because they can't compile unless they're represented as structs - // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct - // of the same shape - function excludeFunctionsWithTuples(item: AbiItem) { - if ( - item.type === "function" && - [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") - ) { - debug( - `System function \`${label}.${item.name}\` is using a tuple argument or return type, which is not yet supported by interface generation. Skipping...`, - ); - return false; - } - return true; - } - // If empty or unset ABI in metadata table, backfill with world functions. // These don't have parameter names or return values, but better than nothing? const abi = parseAbi( metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), - ).filter(excludeFunctionsWithTuples); - + ); const worldAbi = parseAbi( metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), - ).filter(excludeFunctionsWithTuples); + ); return { namespaceId, namespaceLabel: labels[namespaceId] ?? namespace, - label, + label: systemLabel, systemId, namespace, name, @@ -173,6 +155,24 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { await Promise.all( systems + .map((system) => ({ + ...system, + // skip functions that use tuples for now, because they can't compile unless they're represented as structs + // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct + // of the same shape + abi: system.abi.filter((item) => { + if ( + item.type === "function" && + [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") + ) { + debug( + `System function \`${system.label}.${item.name}\` is using a tuple argument or return type, which is not yet supported by interface generation. Skipping...`, + ); + return false; + } + return true; + }), + })) .filter((system) => system.abi.length) .map(async (system) => { const interfaceName = `I${system.label}`; @@ -196,9 +196,46 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { }), ); - // const abi = systems.flatMap((system) => system.worldAbi); - // const worldAbi = path.join(rootDir, `.mud/systems/world.abi.json`); - // const worldInterface = path.join(rootDir, `tmp/`) + const abi = systems + .map((system) => ({ + ...system, + // skip functions that use tuples for now, because they can't compile unless they're represented as structs + // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct + // of the same shape + worldAbi: system.worldAbi.filter((item) => { + if ( + item.type === "function" && + [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") + ) { + debug( + `World function \`${item.name}\` (from ${system.label}) is using a tuple argument or return type, which is not yet supported by interface generation. Skipping...`, + ); + return false; + } + return true; + }), + })) + .flatMap((system) => system.worldAbi); + + if (abi.length) { + const worldAbi = path.join(rootDir, `.mud/systems/world.abi.json`); + const worldInterface = path.join(rootDir, `src/IWorldSystems.sol`); + + debug("writing world systems ABI to", path.relative(rootDir, worldAbi)); + await writeFile(worldAbi, JSON.stringify(abi)); + + debug("generating world systems interface to", path.relative(rootDir, worldInterface)); + await execa("cast", [ + "interface", + "--name", + "IWorldSystems", + "--pragma", + ">=0.8.24", + "-o", + worldInterface, + worldAbi, + ]); + } } async function writeFile(filename: string, contents: string) { From 0f23d237d36ea6deafc6ab008d95b30fef8ff438 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 10:41:56 +0100 Subject: [PATCH 07/14] render interfaces ourselves so we can do more with them --- packages/cli/src/pull/pull.ts | 104 +++--------------- packages/common/package.json | 1 + .../codegen/render-solidity/abiToInterface.ts | 59 ++++++++++ .../src/codegen/render-solidity/index.ts | 1 + pnpm-lock.yaml | 14 +-- 5 files changed, 86 insertions(+), 93 deletions(-) create mode 100644 packages/common/src/codegen/render-solidity/abiToInterface.ts diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 544d511cde..474c0b1a2f 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -9,8 +9,7 @@ import path from "node:path"; import fs from "node:fs/promises"; import { getResourceIds } from "../deploy/getResourceIds"; import { getFunctions } from "@latticexyz/world/internal"; -import { formatTypescript } from "@latticexyz/common/codegen"; -import { execa } from "execa"; +import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/common/codegen"; import { debug } from "./debug"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); @@ -50,9 +49,6 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { // ensure we always have a root namespace label labels[namespaceToHex("")] ??= "root"; - console.log(labels); - const namespaces = resources.filter((resource) => resource.type === "namespace"); - const worldFunctions = await getFunctions({ client, worldAddress: worldDeploy.address, @@ -60,6 +56,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { toBlock: worldDeploy.stateBlock, }); + const namespaces = resources.filter((resource) => resource.type === "namespace"); const systems = await Promise.all( resources .filter((resource) => resource.type === "system") @@ -153,88 +150,23 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { `), ); - await Promise.all( - systems - .map((system) => ({ - ...system, - // skip functions that use tuples for now, because they can't compile unless they're represented as structs - // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct - // of the same shape - abi: system.abi.filter((item) => { - if ( - item.type === "function" && - [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") - ) { - debug( - `System function \`${system.label}.${item.name}\` is using a tuple argument or return type, which is not yet supported by interface generation. Skipping...`, - ); - return false; - } - return true; - }), - })) - .filter((system) => system.abi.length) - .map(async (system) => { - const interfaceName = `I${system.label}`; - const systemAbi = path.join(rootDir, `.mud/systems/${system.systemId}.abi.json`); - const systemInterface = path.join(rootDir, `src/namespaces/${system.namespaceLabel}/${interfaceName}.sol`); - - debug("writing system ABI for", interfaceName, "to", path.relative(rootDir, systemAbi)); - await writeFile(systemAbi, JSON.stringify(system.abi)); - - debug("generating system interface", interfaceName, "to", path.relative(rootDir, systemInterface)); - await execa("cast", [ - "interface", - "--name", - interfaceName, - "--pragma", - ">=0.8.24", - "-o", - systemInterface, - systemAbi, - ]); - }), - ); + for (const system of systems.filter((system) => system.abi.length)) { + const interfaceName = `I${system.label}`; + const interfaceFile = `src/namespaces/${system.namespaceLabel}/${interfaceName}.sol`; + const source = abiToInterface({ name: interfaceName, systemId: system.systemId, abi: system.abi }); - const abi = systems - .map((system) => ({ - ...system, - // skip functions that use tuples for now, because they can't compile unless they're represented as structs - // but if we hoist tuples into structs, you have to use the interface's specific struct rather any struct - // of the same shape - worldAbi: system.worldAbi.filter((item) => { - if ( - item.type === "function" && - [...item.inputs, ...item.outputs].some((param) => param.type === "tuple" || param.type === "tuple[]") - ) { - debug( - `World function \`${item.name}\` (from ${system.label}) is using a tuple argument or return type, which is not yet supported by interface generation. Skipping...`, - ); - return false; - } - return true; - }), - })) - .flatMap((system) => system.worldAbi); - - if (abi.length) { - const worldAbi = path.join(rootDir, `.mud/systems/world.abi.json`); - const worldInterface = path.join(rootDir, `src/IWorldSystems.sol`); - - debug("writing world systems ABI to", path.relative(rootDir, worldAbi)); - await writeFile(worldAbi, JSON.stringify(abi)); - - debug("generating world systems interface to", path.relative(rootDir, worldInterface)); - await execa("cast", [ - "interface", - "--name", - "IWorldSystems", - "--pragma", - ">=0.8.24", - "-o", - worldInterface, - worldAbi, - ]); + debug("generating system interface", interfaceName, "to", interfaceFile); + await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); + } + + const worldAbi = systems.flatMap((system) => system.worldAbi); + if (worldAbi.length) { + const interfaceName = "IWorldSystems"; + const interfaceFile = `src/${interfaceName}.sol`; + const source = abiToInterface({ name: interfaceName, abi: worldAbi }); + + debug("generating world systems interface to", interfaceFile); + await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); } } diff --git a/packages/common/package.json b/packages/common/package.json index 3adf5cb9f9..91082d40ee 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -68,6 +68,7 @@ "dependencies": { "@latticexyz/schema-type": "workspace:*", "@solidity-parser/parser": "^0.16.0", + "abitype": "catalog:", "debug": "^4.3.4", "execa": "^7.0.0", "p-queue": "^7.4.1", diff --git a/packages/common/src/codegen/render-solidity/abiToInterface.ts b/packages/common/src/codegen/render-solidity/abiToInterface.ts new file mode 100644 index 0000000000..ab81d96db7 --- /dev/null +++ b/packages/common/src/codegen/render-solidity/abiToInterface.ts @@ -0,0 +1,59 @@ +import { AbiParameter, Hex } from "viem"; +import { Abi, AbiError, AbiFunction, formatAbiItem, formatAbiParameter } from "abitype"; +import { renderedSolidityHeader } from "./common"; +import { hexToResource } from "../../hexToResource"; + +function formatParam(param: AbiParameter): string { + return param.type === "string" || param.type === "bytes" || param.type === "tuple" || param.type.endsWith("]") + ? `${formatAbiParameter(param)} memory` + : formatAbiParameter(param); +} + +function formatFunction(item: AbiFunction): string { + const params = item.inputs.map(formatParam).join(", "); + const returns = item.outputs.map(formatParam).join(", "); + return `function ${item.name}(${params}) external${returns.length ? ` returns (${returns})` : ""}`; +} + +function formatSystemId(systemId: Hex): string { + const resource = hexToResource(systemId); + return ` + // equivalent to \`WorldResourceIdLib.encode({ namespace: ${JSON.stringify( + resource.namespace, + )}, name: ${JSON.stringify(resource.name)}, typeId: RESOURCE_SYSTEM });\` + ResourceId constant systemId = ResourceId.wrap(${systemId}); + `; +} + +export type AbiToInterfaceOptions = { + name: string; + systemId?: Hex; + abi: Abi; +}; + +export function abiToInterface({ name, systemId, abi }: AbiToInterfaceOptions): string { + const imports = systemId ? [`{ ResourceId } from "@latticexyz/store/src/ResourceId.sol"`] : []; + const errors = abi.filter((item): item is AbiError => item.type === "error"); + const functions = abi.filter((item): item is AbiFunction => item.type === "function"); + + return ` + ${renderedSolidityHeader} + + ${imports.map((item) => `import ${item};`).join("\n")} + + ${systemId ? formatSystemId(systemId) : ""} + + interface ${name} { + ${errors.map((item) => `${formatAbiItem(item)};`).join("\n")} + + ${functions + .map((item) => { + if ([...item.inputs, ...item.outputs].some((param) => param.type.startsWith("tuple"))) { + return `// TODO: replace tuple with struct\n// ${formatFunction(item)};`; + } + return `${formatFunction(item)};`; + }) + .join("\n")} + } + `; +} diff --git a/packages/common/src/codegen/render-solidity/index.ts b/packages/common/src/codegen/render-solidity/index.ts index 1a9c63f824..931750d7db 100644 --- a/packages/common/src/codegen/render-solidity/index.ts +++ b/packages/common/src/codegen/render-solidity/index.ts @@ -1,3 +1,4 @@ +export * from "./abiToInterface"; export * from "./common"; export * from "./renderEnums"; export * from "./renderImportPath"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9176131208..a7c47d3cda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,9 +223,6 @@ importers: path: specifier: ^0.12.7 version: 0.12.7 - prettier: - specifier: 3.2.5 - version: 3.2.5 rxjs: specifier: 7.5.5 version: 7.5.5 @@ -299,6 +296,9 @@ importers: '@solidity-parser/parser': specifier: ^0.16.0 version: 0.16.0 + abitype: + specifier: 'catalog:' + version: 1.0.6(typescript@5.4.2)(zod@3.23.8) asn1.js: specifier: 5.x version: 5.4.1 @@ -803,10 +803,10 @@ importers: version: 8.3.4 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@20.12.12) + version: 29.5.0(@types/node@18.15.11) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@20.12.12))(typescript@5.4.2) + version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@18.15.11))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) @@ -1191,10 +1191,10 @@ importers: version: 27.4.1 jest: specifier: ^29.3.1 - version: 29.5.0(@types/node@18.15.11) + version: 29.5.0(@types/node@20.12.12) ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@18.15.11))(typescript@5.4.2) + version: 29.0.5(@babel/core@7.21.4)(@jest/types@29.6.3)(babel-jest@29.5.0(@babel/core@7.21.4))(jest@29.5.0(@types/node@20.12.12))(typescript@5.4.2) tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.31)(typescript@5.4.2) From 30a115b44f01fc1612a6e542c6e738ec2dfa699b Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 11:02:20 +0100 Subject: [PATCH 08/14] validate config, use config to generate paths --- packages/cli/src/pull/pull.ts | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 474c0b1a2f..f368de5c58 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -11,6 +11,7 @@ import { getResourceIds } from "../deploy/getResourceIds"; import { getFunctions } from "@latticexyz/world/internal"; import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/common/codegen"; import { debug } from "./debug"; +import { defineWorld } from "@latticexyz/world"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); @@ -110,7 +111,8 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { }), ); - const config = { + debug("generating config"); + const configInput = { namespaces: Object.fromEntries( namespaces.map(({ namespace, resourceId: namespaceId }) => { const namespaceLabel = labels[namespaceId] ?? namespace; @@ -141,31 +143,42 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ), }; + // use the config before writing it so we make sure its valid + // and because we'll use the default paths to write interfaces + debug("validating config"); + const config = defineWorld(configInput); + + debug("writing config"); await writeFile( path.join(rootDir, "mud.config.ts"), await formatTypescript(` import { defineWorld } from "@latticexyz/world"; - export default defineWorld(${JSON.stringify(config)}); + export default defineWorld(${JSON.stringify(configInput)}); `), ); for (const system of systems.filter((system) => system.abi.length)) { const interfaceName = `I${system.label}`; - const interfaceFile = `src/namespaces/${system.namespaceLabel}/${interfaceName}.sol`; + const interfaceFile = path.join( + config.sourceDirectory, + "namespaces", + system.namespaceLabel, + `${interfaceName}.sol`, + ); + + debug("writing system interface", interfaceName, "to", interfaceFile); const source = abiToInterface({ name: interfaceName, systemId: system.systemId, abi: system.abi }); - - debug("generating system interface", interfaceName, "to", interfaceFile); await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); } const worldAbi = systems.flatMap((system) => system.worldAbi); if (worldAbi.length) { const interfaceName = "IWorldSystems"; - const interfaceFile = `src/${interfaceName}.sol`; - const source = abiToInterface({ name: interfaceName, abi: worldAbi }); + const interfaceFile = path.join(config.sourceDirectory, `${interfaceName}.sol`); - debug("generating world systems interface to", interfaceFile); + debug("writing world systems interface to", interfaceFile); + const source = abiToInterface({ name: interfaceName, abi: worldAbi }); await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); } } From d172a284dd091bf77fa0eaa5532e9bbdff7fd8df Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 14:24:55 +0100 Subject: [PATCH 09/14] use interfaces in IWorld --- packages/cli/src/pull/pull.ts | 135 +++++++++--------- .../src/codegen/utils/contractToInterface.ts | 2 +- .../world/ts/node/buildSystemsManifest.ts | 16 ++- packages/world/ts/node/getSystemContracts.ts | 8 +- .../world/ts/node/render-solidity/worldgen.ts | 53 ++++--- packages/world/ts/node/resolveSystems.ts | 7 +- 6 files changed, 123 insertions(+), 98 deletions(-) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index f368de5c58..fe722eed42 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -1,4 +1,4 @@ -import { Address, Client, hexToString, parseAbi, stringToHex } from "viem"; +import { Address, Client, hexToString, parseAbi, stringToHex, zeroAddress } from "viem"; import { getTables } from "../deploy/getTables"; import { getWorldDeploy } from "../deploy/getWorldDeploy"; import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; @@ -12,8 +12,10 @@ import { getFunctions } from "@latticexyz/world/internal"; import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/common/codegen"; import { debug } from "./debug"; import { defineWorld } from "@latticexyz/world"; +import { getWorldContracts } from "../deploy/getWorldContracts"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); +const ignoredSystems = new Set(Object.keys(getWorldContracts(zeroAddress)).filter((name) => name.endsWith("System"))); function namespaceToHex(namespace: string) { return resourceToHex({ type: "namespace", namespace, name: "" }); @@ -58,58 +60,62 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { }); const namespaces = resources.filter((resource) => resource.type === "namespace"); - const systems = await Promise.all( - resources - .filter((resource) => resource.type === "system") - .map(async ({ namespace, name, resourceId: systemId }) => { - const namespaceId = namespaceToHex(namespace); - // the system name from the system ID can be potentially truncated, so we'll strip off - // any partial "System" suffix and replace it with a full "System" suffix so that it - // matches our criteria for system names - const systemLabel = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); - - const [metadataAbi, metadataWorldAbi] = await Promise.all([ - getRecord({ - client, - worldDeploy, - table: metadataConfig.tables.metadata__ResourceTag, - key: { resource: systemId, tag: stringToHex("abi", { size: 32 }) }, - }) - .then((record) => hexToString(record.value)) - .then((value) => (value !== "" ? value.split("\n") : [])), - getRecord({ - client, - worldDeploy, - table: metadataConfig.tables.metadata__ResourceTag, - key: { resource: systemId, tag: stringToHex("worldAbi", { size: 32 }) }, - }) - .then((record) => hexToString(record.value)) - .then((value) => (value !== "" ? value.split("\n") : [])), - ]); - - const functions = worldFunctions.filter((func) => func.systemId === systemId); - - // If empty or unset ABI in metadata table, backfill with world functions. - // These don't have parameter names or return values, but better than nothing? - const abi = parseAbi( - metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), - ); - const worldAbi = parseAbi( - metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), - ); - - return { - namespaceId, - namespaceLabel: labels[namespaceId] ?? namespace, - label: systemLabel, - systemId, - namespace, - name, - abi, - worldAbi, - }; - }), - ); + const systems = ( + await Promise.all( + resources + .filter((resource) => resource.type === "system") + .map(async ({ namespace, name, resourceId: systemId }) => { + const namespaceId = namespaceToHex(namespace); + // the system name from the system ID can be potentially truncated, so we'll strip off + // any partial "System" suffix and replace it with a full "System" suffix so that it + // matches our criteria for system names + const systemLabel = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); + + const [metadataAbi, metadataWorldAbi] = await Promise.all([ + getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: systemId, tag: stringToHex("abi", { size: 32 }) }, + }) + .then((record) => hexToString(record.value)) + .then((value) => (value !== "" ? value.split("\n") : [])), + getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: systemId, tag: stringToHex("worldAbi", { size: 32 }) }, + }) + .then((record) => hexToString(record.value)) + .then((value) => (value !== "" ? value.split("\n") : [])), + ]); + + const functions = worldFunctions.filter((func) => func.systemId === systemId); + + // If empty or unset ABI in metadata table, backfill with world functions. + // These don't have parameter names or return values, but better than nothing? + const abi = parseAbi( + metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), + ); + const worldAbi = parseAbi( + metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), + ); + + return { + namespaceId, + namespaceLabel: labels[namespaceId] ?? namespace, + label: systemLabel, + systemId, + namespace, + name, + abi, + worldAbi, + }; + }), + ) + ) + .filter((system) => !ignoredSystems.has(system.label)) + .filter((system) => system.abi.length); debug("generating config"); const configInput = { @@ -137,6 +143,17 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ]; }), ), + systems: Object.fromEntries( + systems + .filter((system) => system.namespace === namespace) + .map((system) => [ + system.label, + { + ...(system.label !== system.name ? { name: system.name } : null), + deploy: { disabled: true }, + }, + ]), + ), }, ]; }), @@ -158,7 +175,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { `), ); - for (const system of systems.filter((system) => system.abi.length)) { + for (const system of systems) { const interfaceName = `I${system.label}`; const interfaceFile = path.join( config.sourceDirectory, @@ -171,16 +188,6 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const source = abiToInterface({ name: interfaceName, systemId: system.systemId, abi: system.abi }); await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); } - - const worldAbi = systems.flatMap((system) => system.worldAbi); - if (worldAbi.length) { - const interfaceName = "IWorldSystems"; - const interfaceFile = path.join(config.sourceDirectory, `${interfaceName}.sol`); - - debug("writing world systems interface to", interfaceFile); - const source = abiToInterface({ name: interfaceName, abi: worldAbi }); - await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); - } } async function writeFile(filename: string, contents: string) { diff --git a/packages/common/src/codegen/utils/contractToInterface.ts b/packages/common/src/codegen/utils/contractToInterface.ts index a896404fac..a163e89d2c 100644 --- a/packages/common/src/codegen/utils/contractToInterface.ts +++ b/packages/common/src/codegen/utils/contractToInterface.ts @@ -111,7 +111,7 @@ export function findContractNode(ast: SourceUnit, contractName: string): Contrac visit(ast, { ContractDefinition(node) { - if (node.name === contractName) { + if (node.name === contractName || node.name === `I${contractName}`) { contract = node; } }, diff --git a/packages/world/ts/node/buildSystemsManifest.ts b/packages/world/ts/node/buildSystemsManifest.ts index 6bc40771e3..82f21dbbb6 100644 --- a/packages/world/ts/node/buildSystemsManifest.ts +++ b/packages/world/ts/node/buildSystemsManifest.ts @@ -45,10 +45,10 @@ export async function buildSystemsManifest(opts: { rootDir: string; config: Worl const contractArtifacts = await findContractArtifacts({ forgeOutDir }); function getSystemArtifact(system: ResolvedSystem): ContractArtifact { - const artifact = contractArtifacts.find((a) => a.sourcePath === system.sourcePath && a.name === system.label); + const artifact = contractArtifacts.find((a) => a.sourcePath === system.sourcePath && a.name === system.sourceName); if (!artifact) { throw new Error( - `Could not find build artifact for system \`${system.label}\` at \`${system.sourcePath}\`. Did \`forge build\` run successfully?`, + `Could not find build artifact for system \`${system.label}\` at \`${system.sourcePath}:${system.sourceName}\`. Did \`forge build\` run successfully?`, ); } return artifact; @@ -58,9 +58,15 @@ export async function buildSystemsManifest(opts: { rootDir: string; config: Worl systems: systems.map((system): (typeof SystemsManifest)["infer"]["systems"][number] => { const artifact = getSystemArtifact(system); const abi = artifact.abi.filter((item) => !excludedAbi.includes(formatAbiItem(item))); - const worldAbi = system.deploy.registerWorldFunctions - ? abi.map((item) => (item.type === "function" ? { ...item, name: `${system.namespace}__${item.name}` } : item)) - : []; + const worldAbi = + // TODO: normalize this so `disabled: true` sets `registerWorldFunctions: false` + !system.deploy.disabled && system.deploy.registerWorldFunctions + ? abi.map((item) => + item.type === "function" + ? { ...item, name: system.namespace === "" ? item.name : `${system.namespace}__${item.name}` } + : item, + ) + : []; return { // labels namespaceLabel: system.namespaceLabel, diff --git a/packages/world/ts/node/getSystemContracts.ts b/packages/world/ts/node/getSystemContracts.ts index 76aff24a4d..9eecd5f9cd 100644 --- a/packages/world/ts/node/getSystemContracts.ts +++ b/packages/world/ts/node/getSystemContracts.ts @@ -4,6 +4,7 @@ import { findSolidityFiles } from "./findSolidityFiles"; export type SystemContract = { readonly sourcePath: string; + readonly sourceName: string; readonly namespaceLabel: string; readonly systemLabel: string; }; @@ -27,9 +28,7 @@ export async function getSystemContracts({ (file) => file.basename.endsWith("System") && // exclude the base System contract - file.basename !== "System" && - // exclude interfaces - !/^I[A-Z]/.test(file.basename), + file.basename !== "System", ) .map((file) => { const namespaceLabel = (() => { @@ -50,8 +49,9 @@ export async function getSystemContracts({ return { sourcePath: file.filename, + sourceName: file.basename, namespaceLabel, - systemLabel: file.basename, + systemLabel: file.basename.replace(/^I/, ""), }; }); } diff --git a/packages/world/ts/node/render-solidity/worldgen.ts b/packages/world/ts/node/render-solidity/worldgen.ts index fcb62bf6c4..93fa78c377 100644 --- a/packages/world/ts/node/render-solidity/worldgen.ts +++ b/packages/world/ts/node/render-solidity/worldgen.ts @@ -28,15 +28,20 @@ export async function worldgen({ const outputPath = path.join(outDir, config.codegen.worldInterfaceName + ".sol"); + // TODO: if already an interface, reuse it + const systems = (await resolveSystems({ rootDir, config })) // TODO: move to codegen option or generate "system manifest" and codegen from that .filter((system) => system.deploy.registerWorldFunctions) .map((system) => { const interfaceName = `I${system.label}`; + const interfaceFilename = `${interfaceName}.sol`; + const isInterface = path.basename(system.sourcePath) === interfaceFilename; return { ...system, + createInterface: !isInterface, interfaceName, - interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`), + interfacePath: isInterface ? system.sourcePath : path.join(path.dirname(outputPath), interfaceFilename), }; }); @@ -48,28 +53,30 @@ export async function worldgen({ ); await Promise.all( - systems.map(async (system) => { - const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8"); - // get external functions from a contract - const { functions, errors, symbolImports } = contractToInterface(source, system.label); - const imports = symbolImports.map( - ({ symbol, path: importPath }): ImportDatum => ({ - symbol, - path: importPath.startsWith(".") - ? "./" + path.relative(outDir, path.join(rootDir, path.dirname(system.sourcePath), importPath)) - : importPath, - }), - ); - const output = renderSystemInterface({ - name: system.interfaceName, - functionPrefix: system.namespace === "" ? "" : `${system.namespace}__`, - functions, - errors, - imports, - }); - // write to file - await formatAndWriteSolidity(output, system.interfacePath, "Generated system interface"); - }), + systems + .filter((system) => system.createInterface) + .map(async (system) => { + const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8"); + // get external functions from a contract + const { functions, errors, symbolImports } = contractToInterface(source, system.label); + const imports = symbolImports.map( + ({ symbol, path: importPath }): ImportDatum => ({ + symbol, + path: importPath.startsWith(".") + ? "./" + path.relative(outDir, path.join(rootDir, path.dirname(system.sourcePath), importPath)) + : importPath, + }), + ); + const output = renderSystemInterface({ + name: system.interfaceName, + functionPrefix: system.namespace === "" ? "" : `${system.namespace}__`, + functions, + errors, + imports, + }); + // write to file + await formatAndWriteSolidity(output, system.interfacePath, "Generated system interface"); + }), ); // render IWorld diff --git a/packages/world/ts/node/resolveSystems.ts b/packages/world/ts/node/resolveSystems.ts index 152c1bb93a..5fae9fb3e0 100644 --- a/packages/world/ts/node/resolveSystems.ts +++ b/packages/world/ts/node/resolveSystems.ts @@ -6,6 +6,7 @@ import { resourceToLabel } from "@latticexyz/common"; export type ResolvedSystem = System & { readonly sourcePath: string; + readonly sourceName: string; }; export async function resolveSystems({ @@ -16,11 +17,14 @@ export async function resolveSystems({ config: World; }): Promise { const systemContracts = await getSystemContracts({ rootDir, config }); + console.log("system contracts", systemContracts); // validate every system in config refers to an existing system contract const configSystems = Object.values(config.namespaces).flatMap((namespace) => Object.values(namespace.systems)); + console.log("config sysmtes", configSystems); const missingSystems = configSystems.filter( - (system) => !systemContracts.some((s) => s.namespaceLabel === system.namespace && s.systemLabel === system.label), + (system) => + !systemContracts.some((s) => s.namespaceLabel === system.namespaceLabel && s.systemLabel === system.label), ); if (missingSystems.length > 0) { throw new Error( @@ -41,6 +45,7 @@ export async function resolveSystems({ return { ...systemConfig, sourcePath: contract.sourcePath, + sourceName: contract.sourceName, }; }) // TODO: replace `excludeSystems` with `deploy.disabled` or `codegen.disabled` From a98e5655008839bf28980ac9d38f391bc2250770 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 14:24:58 +0100 Subject: [PATCH 10/14] Revert "use interfaces in IWorld" This reverts commit d172a284dd091bf77fa0eaa5532e9bbdff7fd8df. --- packages/cli/src/pull/pull.ts | 135 +++++++++--------- .../src/codegen/utils/contractToInterface.ts | 2 +- .../world/ts/node/buildSystemsManifest.ts | 16 +-- packages/world/ts/node/getSystemContracts.ts | 8 +- .../world/ts/node/render-solidity/worldgen.ts | 53 +++---- packages/world/ts/node/resolveSystems.ts | 7 +- 6 files changed, 98 insertions(+), 123 deletions(-) diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index fe722eed42..f368de5c58 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -1,4 +1,4 @@ -import { Address, Client, hexToString, parseAbi, stringToHex, zeroAddress } from "viem"; +import { Address, Client, hexToString, parseAbi, stringToHex } from "viem"; import { getTables } from "../deploy/getTables"; import { getWorldDeploy } from "../deploy/getWorldDeploy"; import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; @@ -12,10 +12,8 @@ import { getFunctions } from "@latticexyz/world/internal"; import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/common/codegen"; import { debug } from "./debug"; import { defineWorld } from "@latticexyz/world"; -import { getWorldContracts } from "../deploy/getWorldContracts"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); -const ignoredSystems = new Set(Object.keys(getWorldContracts(zeroAddress)).filter((name) => name.endsWith("System"))); function namespaceToHex(namespace: string) { return resourceToHex({ type: "namespace", namespace, name: "" }); @@ -60,62 +58,58 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { }); const namespaces = resources.filter((resource) => resource.type === "namespace"); - const systems = ( - await Promise.all( - resources - .filter((resource) => resource.type === "system") - .map(async ({ namespace, name, resourceId: systemId }) => { - const namespaceId = namespaceToHex(namespace); - // the system name from the system ID can be potentially truncated, so we'll strip off - // any partial "System" suffix and replace it with a full "System" suffix so that it - // matches our criteria for system names - const systemLabel = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); - - const [metadataAbi, metadataWorldAbi] = await Promise.all([ - getRecord({ - client, - worldDeploy, - table: metadataConfig.tables.metadata__ResourceTag, - key: { resource: systemId, tag: stringToHex("abi", { size: 32 }) }, - }) - .then((record) => hexToString(record.value)) - .then((value) => (value !== "" ? value.split("\n") : [])), - getRecord({ - client, - worldDeploy, - table: metadataConfig.tables.metadata__ResourceTag, - key: { resource: systemId, tag: stringToHex("worldAbi", { size: 32 }) }, - }) - .then((record) => hexToString(record.value)) - .then((value) => (value !== "" ? value.split("\n") : [])), - ]); - - const functions = worldFunctions.filter((func) => func.systemId === systemId); - - // If empty or unset ABI in metadata table, backfill with world functions. - // These don't have parameter names or return values, but better than nothing? - const abi = parseAbi( - metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), - ); - const worldAbi = parseAbi( - metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), - ); - - return { - namespaceId, - namespaceLabel: labels[namespaceId] ?? namespace, - label: systemLabel, - systemId, - namespace, - name, - abi, - worldAbi, - }; - }), - ) - ) - .filter((system) => !ignoredSystems.has(system.label)) - .filter((system) => system.abi.length); + const systems = await Promise.all( + resources + .filter((resource) => resource.type === "system") + .map(async ({ namespace, name, resourceId: systemId }) => { + const namespaceId = namespaceToHex(namespace); + // the system name from the system ID can be potentially truncated, so we'll strip off + // any partial "System" suffix and replace it with a full "System" suffix so that it + // matches our criteria for system names + const systemLabel = labels[systemId] ?? name.replace(/(S(y(s(t(e(m)?)?)?)?)?)?$/, "System"); + + const [metadataAbi, metadataWorldAbi] = await Promise.all([ + getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: systemId, tag: stringToHex("abi", { size: 32 }) }, + }) + .then((record) => hexToString(record.value)) + .then((value) => (value !== "" ? value.split("\n") : [])), + getRecord({ + client, + worldDeploy, + table: metadataConfig.tables.metadata__ResourceTag, + key: { resource: systemId, tag: stringToHex("worldAbi", { size: 32 }) }, + }) + .then((record) => hexToString(record.value)) + .then((value) => (value !== "" ? value.split("\n") : [])), + ]); + + const functions = worldFunctions.filter((func) => func.systemId === systemId); + + // If empty or unset ABI in metadata table, backfill with world functions. + // These don't have parameter names or return values, but better than nothing? + const abi = parseAbi( + metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), + ); + const worldAbi = parseAbi( + metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), + ); + + return { + namespaceId, + namespaceLabel: labels[namespaceId] ?? namespace, + label: systemLabel, + systemId, + namespace, + name, + abi, + worldAbi, + }; + }), + ); debug("generating config"); const configInput = { @@ -143,17 +137,6 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { ]; }), ), - systems: Object.fromEntries( - systems - .filter((system) => system.namespace === namespace) - .map((system) => [ - system.label, - { - ...(system.label !== system.name ? { name: system.name } : null), - deploy: { disabled: true }, - }, - ]), - ), }, ]; }), @@ -175,7 +158,7 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { `), ); - for (const system of systems) { + for (const system of systems.filter((system) => system.abi.length)) { const interfaceName = `I${system.label}`; const interfaceFile = path.join( config.sourceDirectory, @@ -188,6 +171,16 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { const source = abiToInterface({ name: interfaceName, systemId: system.systemId, abi: system.abi }); await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); } + + const worldAbi = systems.flatMap((system) => system.worldAbi); + if (worldAbi.length) { + const interfaceName = "IWorldSystems"; + const interfaceFile = path.join(config.sourceDirectory, `${interfaceName}.sol`); + + debug("writing world systems interface to", interfaceFile); + const source = abiToInterface({ name: interfaceName, abi: worldAbi }); + await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); + } } async function writeFile(filename: string, contents: string) { diff --git a/packages/common/src/codegen/utils/contractToInterface.ts b/packages/common/src/codegen/utils/contractToInterface.ts index a163e89d2c..a896404fac 100644 --- a/packages/common/src/codegen/utils/contractToInterface.ts +++ b/packages/common/src/codegen/utils/contractToInterface.ts @@ -111,7 +111,7 @@ export function findContractNode(ast: SourceUnit, contractName: string): Contrac visit(ast, { ContractDefinition(node) { - if (node.name === contractName || node.name === `I${contractName}`) { + if (node.name === contractName) { contract = node; } }, diff --git a/packages/world/ts/node/buildSystemsManifest.ts b/packages/world/ts/node/buildSystemsManifest.ts index 82f21dbbb6..6bc40771e3 100644 --- a/packages/world/ts/node/buildSystemsManifest.ts +++ b/packages/world/ts/node/buildSystemsManifest.ts @@ -45,10 +45,10 @@ export async function buildSystemsManifest(opts: { rootDir: string; config: Worl const contractArtifacts = await findContractArtifacts({ forgeOutDir }); function getSystemArtifact(system: ResolvedSystem): ContractArtifact { - const artifact = contractArtifacts.find((a) => a.sourcePath === system.sourcePath && a.name === system.sourceName); + const artifact = contractArtifacts.find((a) => a.sourcePath === system.sourcePath && a.name === system.label); if (!artifact) { throw new Error( - `Could not find build artifact for system \`${system.label}\` at \`${system.sourcePath}:${system.sourceName}\`. Did \`forge build\` run successfully?`, + `Could not find build artifact for system \`${system.label}\` at \`${system.sourcePath}\`. Did \`forge build\` run successfully?`, ); } return artifact; @@ -58,15 +58,9 @@ export async function buildSystemsManifest(opts: { rootDir: string; config: Worl systems: systems.map((system): (typeof SystemsManifest)["infer"]["systems"][number] => { const artifact = getSystemArtifact(system); const abi = artifact.abi.filter((item) => !excludedAbi.includes(formatAbiItem(item))); - const worldAbi = - // TODO: normalize this so `disabled: true` sets `registerWorldFunctions: false` - !system.deploy.disabled && system.deploy.registerWorldFunctions - ? abi.map((item) => - item.type === "function" - ? { ...item, name: system.namespace === "" ? item.name : `${system.namespace}__${item.name}` } - : item, - ) - : []; + const worldAbi = system.deploy.registerWorldFunctions + ? abi.map((item) => (item.type === "function" ? { ...item, name: `${system.namespace}__${item.name}` } : item)) + : []; return { // labels namespaceLabel: system.namespaceLabel, diff --git a/packages/world/ts/node/getSystemContracts.ts b/packages/world/ts/node/getSystemContracts.ts index 9eecd5f9cd..76aff24a4d 100644 --- a/packages/world/ts/node/getSystemContracts.ts +++ b/packages/world/ts/node/getSystemContracts.ts @@ -4,7 +4,6 @@ import { findSolidityFiles } from "./findSolidityFiles"; export type SystemContract = { readonly sourcePath: string; - readonly sourceName: string; readonly namespaceLabel: string; readonly systemLabel: string; }; @@ -28,7 +27,9 @@ export async function getSystemContracts({ (file) => file.basename.endsWith("System") && // exclude the base System contract - file.basename !== "System", + file.basename !== "System" && + // exclude interfaces + !/^I[A-Z]/.test(file.basename), ) .map((file) => { const namespaceLabel = (() => { @@ -49,9 +50,8 @@ export async function getSystemContracts({ return { sourcePath: file.filename, - sourceName: file.basename, namespaceLabel, - systemLabel: file.basename.replace(/^I/, ""), + systemLabel: file.basename, }; }); } diff --git a/packages/world/ts/node/render-solidity/worldgen.ts b/packages/world/ts/node/render-solidity/worldgen.ts index 93fa78c377..fcb62bf6c4 100644 --- a/packages/world/ts/node/render-solidity/worldgen.ts +++ b/packages/world/ts/node/render-solidity/worldgen.ts @@ -28,20 +28,15 @@ export async function worldgen({ const outputPath = path.join(outDir, config.codegen.worldInterfaceName + ".sol"); - // TODO: if already an interface, reuse it - const systems = (await resolveSystems({ rootDir, config })) // TODO: move to codegen option or generate "system manifest" and codegen from that .filter((system) => system.deploy.registerWorldFunctions) .map((system) => { const interfaceName = `I${system.label}`; - const interfaceFilename = `${interfaceName}.sol`; - const isInterface = path.basename(system.sourcePath) === interfaceFilename; return { ...system, - createInterface: !isInterface, interfaceName, - interfacePath: isInterface ? system.sourcePath : path.join(path.dirname(outputPath), interfaceFilename), + interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`), }; }); @@ -53,30 +48,28 @@ export async function worldgen({ ); await Promise.all( - systems - .filter((system) => system.createInterface) - .map(async (system) => { - const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8"); - // get external functions from a contract - const { functions, errors, symbolImports } = contractToInterface(source, system.label); - const imports = symbolImports.map( - ({ symbol, path: importPath }): ImportDatum => ({ - symbol, - path: importPath.startsWith(".") - ? "./" + path.relative(outDir, path.join(rootDir, path.dirname(system.sourcePath), importPath)) - : importPath, - }), - ); - const output = renderSystemInterface({ - name: system.interfaceName, - functionPrefix: system.namespace === "" ? "" : `${system.namespace}__`, - functions, - errors, - imports, - }); - // write to file - await formatAndWriteSolidity(output, system.interfacePath, "Generated system interface"); - }), + systems.map(async (system) => { + const source = await fs.readFile(path.join(rootDir, system.sourcePath), "utf8"); + // get external functions from a contract + const { functions, errors, symbolImports } = contractToInterface(source, system.label); + const imports = symbolImports.map( + ({ symbol, path: importPath }): ImportDatum => ({ + symbol, + path: importPath.startsWith(".") + ? "./" + path.relative(outDir, path.join(rootDir, path.dirname(system.sourcePath), importPath)) + : importPath, + }), + ); + const output = renderSystemInterface({ + name: system.interfaceName, + functionPrefix: system.namespace === "" ? "" : `${system.namespace}__`, + functions, + errors, + imports, + }); + // write to file + await formatAndWriteSolidity(output, system.interfacePath, "Generated system interface"); + }), ); // render IWorld diff --git a/packages/world/ts/node/resolveSystems.ts b/packages/world/ts/node/resolveSystems.ts index 5fae9fb3e0..152c1bb93a 100644 --- a/packages/world/ts/node/resolveSystems.ts +++ b/packages/world/ts/node/resolveSystems.ts @@ -6,7 +6,6 @@ import { resourceToLabel } from "@latticexyz/common"; export type ResolvedSystem = System & { readonly sourcePath: string; - readonly sourceName: string; }; export async function resolveSystems({ @@ -17,14 +16,11 @@ export async function resolveSystems({ config: World; }): Promise { const systemContracts = await getSystemContracts({ rootDir, config }); - console.log("system contracts", systemContracts); // validate every system in config refers to an existing system contract const configSystems = Object.values(config.namespaces).flatMap((namespace) => Object.values(namespace.systems)); - console.log("config sysmtes", configSystems); const missingSystems = configSystems.filter( - (system) => - !systemContracts.some((s) => s.namespaceLabel === system.namespaceLabel && s.systemLabel === system.label), + (system) => !systemContracts.some((s) => s.namespaceLabel === system.namespace && s.systemLabel === system.label), ); if (missingSystems.length > 0) { throw new Error( @@ -45,7 +41,6 @@ export async function resolveSystems({ return { ...systemConfig, sourcePath: contract.sourcePath, - sourceName: contract.sourceName, }; }) // TODO: replace `excludeSystems` with `deploy.disabled` or `codegen.disabled` From 70569b4291376d409e01a80981d690c42595a230 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 16:15:48 +0100 Subject: [PATCH 11/14] don't overwrite files by default --- packages/cli/src/commands/pull.ts | 30 +++++++++++++++++-- packages/cli/src/pull/pull.ts | 48 ++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/commands/pull.ts b/packages/cli/src/commands/pull.ts index d2d69bdbca..c45395c03d 100644 --- a/packages/cli/src/commands/pull.ts +++ b/packages/cli/src/commands/pull.ts @@ -2,16 +2,21 @@ import type { CommandModule, InferredOptionTypes } from "yargs"; import { getRpcUrl } from "@latticexyz/common/foundry"; import { Address, createClient, http } from "viem"; import chalk from "chalk"; -import { pull } from "../pull/pull"; +import { WriteFileExistsError, pull } from "../pull/pull"; +import path from "node:path"; const options = { - worldAddress: { type: "string", required: true, desc: "World address" }, + worldAddress: { type: "string", required: true, desc: "Remote world address" }, profile: { type: "string", desc: "The foundry profile to use" }, rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" }, rpcBatch: { type: "boolean", desc: "Enable batch processing of RPC requests in viem client (defaults to batch size of 100 and wait of 1s)", }, + replace: { + type: "boolean", + desc: "Replace existing files and directories with data from remote world.", + }, } as const; type Options = InferredOptionTypes; @@ -40,8 +45,27 @@ const commandModule: CommandModule = { }); console.log(chalk.bgBlue(chalk.whiteBright(`\n Pulling MUD config from world at ${opts.worldAddress} \n`))); + const rootDir = process.cwd(); - await pull({ rootDir: process.cwd(), client, worldAddress: opts.worldAddress as Address }); + try { + await pull({ + rootDir, + client, + worldAddress: opts.worldAddress as Address, + replace: opts.replace, + }); + } catch (error) { + if (error instanceof WriteFileExistsError) { + console.log(); + console.log(chalk.bgRed(chalk.whiteBright(" Error "))); + console.log(` Attempted to write file at "${path.relative(rootDir, error.filename)}", but it already exists.`); + console.log(); + console.log(" To overwrite files, use `--replace` when running this command."); + console.log(); + return; + } + throw error; + } }, }; diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index f368de5c58..af6a744128 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -12,6 +12,7 @@ import { getFunctions } from "@latticexyz/world/internal"; import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/common/codegen"; import { debug } from "./debug"; import { defineWorld } from "@latticexyz/world"; +import { findUp } from "find-up"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); @@ -19,13 +20,27 @@ function namespaceToHex(namespace: string) { return resourceToHex({ type: "namespace", namespace, name: "" }); } +export class WriteFileExistsError extends Error { + name = "WriteFileExistsError"; + constructor(public filename: string) { + super(`Attempted to write file at "${filename}", but it already exists.`); + } +} + export type PullOptions = { rootDir: string; client: Client; worldAddress: Address; + /** + * Replace existing files and directories with data from remote world. + * Defaults to `true` if `rootDir` is within a git repo, otherwise `false`. + * */ + replace?: boolean; }; -export async function pull({ rootDir, client, worldAddress }: PullOptions) { +export async function pull({ rootDir, client, worldAddress, replace }: PullOptions) { + const replaceFiles = replace ?? (await findUp(".git", { cwd: rootDir })) != null; + const worldDeploy = await getWorldDeploy(client, worldAddress); const resourceIds = await getResourceIds({ client, worldDeploy }); const resources = resourceIds.map(hexToResource).filter((resource) => !ignoredNamespaces.has(resource.namespace)); @@ -156,34 +171,45 @@ export async function pull({ rootDir, client, worldAddress }: PullOptions) { export default defineWorld(${JSON.stringify(configInput)}); `), + { overwrite: replaceFiles }, ); + const remoteDir = path.join(config.sourceDirectory, "remote"); + if (replaceFiles) { + await fs.rm(remoteDir, { recursive: true, force: true }); + } + for (const system of systems.filter((system) => system.abi.length)) { const interfaceName = `I${system.label}`; - const interfaceFile = path.join( - config.sourceDirectory, - "namespaces", - system.namespaceLabel, - `${interfaceName}.sol`, - ); + const interfaceFile = path.join(remoteDir, "namespaces", system.namespaceLabel, `${interfaceName}.sol`); debug("writing system interface", interfaceName, "to", interfaceFile); const source = abiToInterface({ name: interfaceName, systemId: system.systemId, abi: system.abi }); - await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); + await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source), { overwrite: replaceFiles }); } const worldAbi = systems.flatMap((system) => system.worldAbi); if (worldAbi.length) { const interfaceName = "IWorldSystems"; - const interfaceFile = path.join(config.sourceDirectory, `${interfaceName}.sol`); + const interfaceFile = path.join(remoteDir, `${interfaceName}.sol`); debug("writing world systems interface to", interfaceFile); const source = abiToInterface({ name: interfaceName, abi: worldAbi }); - await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source)); + await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source), { overwrite: replaceFiles }); } } -async function writeFile(filename: string, contents: string) { +export async function exists(filename: string) { + return fs.access(filename).then( + () => true, + () => false, + ); +} + +export async function writeFile(filename: string, contents: string, opts: { overwrite?: boolean } = {}) { + if (!opts.overwrite && (await exists(filename))) { + throw new WriteFileExistsError(filename); + } await fs.mkdir(path.dirname(filename), { recursive: true }); await fs.writeFile(filename, contents); } From 48ca428deceaf78d3b3467ac8745f2832846ed9b Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 17:29:55 +0100 Subject: [PATCH 12/14] build after pull --- packages/cli/src/commands/pull.ts | 4 +++- packages/cli/src/pull/pull.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/pull.ts b/packages/cli/src/commands/pull.ts index c45395c03d..c6b6da8703 100644 --- a/packages/cli/src/commands/pull.ts +++ b/packages/cli/src/commands/pull.ts @@ -4,6 +4,7 @@ import { Address, createClient, http } from "viem"; import chalk from "chalk"; import { WriteFileExistsError, pull } from "../pull/pull"; import path from "node:path"; +import { build } from "../build"; const options = { worldAddress: { type: "string", required: true, desc: "Remote world address" }, @@ -48,12 +49,13 @@ const commandModule: CommandModule = { const rootDir = process.cwd(); try { - await pull({ + const { config } = await pull({ rootDir, client, worldAddress: opts.worldAddress as Address, replace: opts.replace, }); + await build({ rootDir, config, foundryProfile: profile }); } catch (error) { if (error instanceof WriteFileExistsError) { console.log(); diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index af6a744128..30bda1bd40 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -197,6 +197,8 @@ export async function pull({ rootDir, client, worldAddress, replace }: PullOptio const source = abiToInterface({ name: interfaceName, abi: worldAbi }); await writeFile(path.join(rootDir, interfaceFile), await formatSolidity(source), { overwrite: replaceFiles }); } + + return { config }; } export async function exists(filename: string) { From b407f05aae623a0828ba9a5574e85bde991f5b76 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Fri, 13 Sep 2024 09:35:19 -0700 Subject: [PATCH 13/14] Create thirty-eels-grab.md --- .changeset/thirty-eels-grab.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/thirty-eels-grab.md diff --git a/.changeset/thirty-eels-grab.md b/.changeset/thirty-eels-grab.md new file mode 100644 index 0000000000..294b688ec0 --- /dev/null +++ b/.changeset/thirty-eels-grab.md @@ -0,0 +1,9 @@ +--- +"@latticexyz/cli": patch +--- + +Added a `mud pull` command that downloads state from an existing world and uses it to generate a MUD config with tables and system interfaces. This makes it much easier to extend worlds. + +``` +mud pull --worldAddress 0x… --rpc https://… +``` From 706a16c3fbb906e54ed927465776406948350532 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Sun, 6 Oct 2024 15:10:54 +0100 Subject: [PATCH 14/14] skip bad signatures, fix interface gen --- packages/cli/src/deploy/getResourceAccess.ts | 1 + packages/cli/src/deploy/getResourceIds.ts | 1 + packages/cli/src/deploy/getTables.ts | 1 + packages/cli/src/pull/pull.ts | 32 +++++++++++++++---- .../codegen/render-solidity/abiToInterface.ts | 7 ++-- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/deploy/getResourceAccess.ts b/packages/cli/src/deploy/getResourceAccess.ts index 6a793979bc..61ecd4f688 100644 --- a/packages/cli/src/deploy/getResourceAccess.ts +++ b/packages/cli/src/deploy/getResourceAccess.ts @@ -19,6 +19,7 @@ export async function getResourceAccess({ const blockLogs = await fetchBlockLogs({ fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, + maxBlockRange: 100_000n, async getLogs({ fromBlock, toBlock }) { return getStoreLogs(client, { address: worldDeploy.address, diff --git a/packages/cli/src/deploy/getResourceIds.ts b/packages/cli/src/deploy/getResourceIds.ts index cd050e5f43..d0714c4a2d 100644 --- a/packages/cli/src/deploy/getResourceIds.ts +++ b/packages/cli/src/deploy/getResourceIds.ts @@ -17,6 +17,7 @@ export async function getResourceIds({ const blockLogs = await fetchBlockLogs({ fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, + maxBlockRange: 100_000n, async getLogs({ fromBlock, toBlock }) { return getStoreLogs(client, { address: worldDeploy.address, diff --git a/packages/cli/src/deploy/getTables.ts b/packages/cli/src/deploy/getTables.ts index f2624dc112..196b24421e 100644 --- a/packages/cli/src/deploy/getTables.ts +++ b/packages/cli/src/deploy/getTables.ts @@ -30,6 +30,7 @@ export async function getTables({ const blockLogs = await fetchBlockLogs({ fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, + maxBlockRange: 100_000n, async getLogs({ fromBlock, toBlock }) { return getStoreLogs(client, { address: worldDeploy.address, diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 30bda1bd40..8616c6f5d4 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -1,4 +1,4 @@ -import { Address, Client, hexToString, parseAbi, stringToHex } from "viem"; +import { Address, Client, hexToString, parseAbiItem, stringToHex } from "viem"; import { getTables } from "../deploy/getTables"; import { getWorldDeploy } from "../deploy/getWorldDeploy"; import { getSchemaTypes } from "@latticexyz/protocol-parser/internal"; @@ -13,6 +13,7 @@ import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/co import { debug } from "./debug"; import { defineWorld } from "@latticexyz/world"; import { findUp } from "find-up"; +import { isDefined } from "@latticexyz/common/utils"; const ignoredNamespaces = new Set(["store", "world", "metadata"]); @@ -106,12 +107,29 @@ export async function pull({ rootDir, client, worldAddress, replace }: PullOptio // If empty or unset ABI in metadata table, backfill with world functions. // These don't have parameter names or return values, but better than nothing? - const abi = parseAbi( - metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`), - ); - const worldAbi = parseAbi( - metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`), - ); + const abi = ( + metadataAbi.length ? metadataAbi : functions.map((func) => `function ${func.systemFunctionSignature}`) + ) + .map((sig) => { + try { + return parseAbiItem(sig); + } catch { + debug(`Skipping invalid system signature: ${sig}`); + } + }) + .filter(isDefined); + + const worldAbi = ( + metadataWorldAbi.length ? metadataWorldAbi : functions.map((func) => `function ${func.signature}`) + ) + .map((sig) => { + try { + return parseAbiItem(sig); + } catch { + debug(`Skipping invalid world signature: ${sig}`); + } + }) + .filter(isDefined); return { namespaceId, diff --git a/packages/common/src/codegen/render-solidity/abiToInterface.ts b/packages/common/src/codegen/render-solidity/abiToInterface.ts index ab81d96db7..ce624d696d 100644 --- a/packages/common/src/codegen/render-solidity/abiToInterface.ts +++ b/packages/common/src/codegen/render-solidity/abiToInterface.ts @@ -4,9 +4,10 @@ import { renderedSolidityHeader } from "./common"; import { hexToResource } from "../../hexToResource"; function formatParam(param: AbiParameter): string { - return param.type === "string" || param.type === "bytes" || param.type === "tuple" || param.type.endsWith("]") - ? `${formatAbiParameter(param)} memory` - : formatAbiParameter(param); + // return param.type === "string" || param.type === "bytes" || param.type === "tuple" || param.type.endsWith("]") + // ? `${formatAbiParameter(param)} memory` + // : formatAbiParameter(param); + return formatAbiParameter(param); } function formatFunction(item: AbiFunction): string {