diff --git a/.changeset/fuzzy-maps-sin.md b/.changeset/fuzzy-maps-sin.md new file mode 100644 index 0000000000..78e690ab5e --- /dev/null +++ b/.changeset/fuzzy-maps-sin.md @@ -0,0 +1,7 @@ +--- +"@latticexyz/cli": patch +--- + +Refactored package to use the new Store/World configs under the hood, removing compatibility layers. + +Removed `--srcDir` option from all commands in favor of using `sourceDirectory` in the project's MUD config. diff --git a/.changeset/lazy-impalas-rush.md b/.changeset/lazy-impalas-rush.md new file mode 100644 index 0000000000..54fbf517c0 --- /dev/null +++ b/.changeset/lazy-impalas-rush.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/world": patch +--- + +Refactored how worldgen resolves systems from the config and filesystem. diff --git a/.changeset/old-waves-juggle.md b/.changeset/old-waves-juggle.md new file mode 100644 index 0000000000..58f62d1012 --- /dev/null +++ b/.changeset/old-waves-juggle.md @@ -0,0 +1,7 @@ +--- +"@latticexyz/world-modules": patch +--- + +Moved build scripts to `mud build` now that CLI doesn't depend on this package. + +Removed generated world interfaces as this package isn't meant to be used as a "world", but as a set of individual modules. diff --git a/packages/cli/contracts/src/codegen/common.sol b/packages/cli/contracts/src/codegen/common.sol index 728b16cc27..57a807edd3 100644 --- a/packages/cli/contracts/src/codegen/common.sol +++ b/packages/cli/contracts/src/codegen/common.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.24; /* Autogenerated file. Do not edit manually. */ + enum Enum1 { E1, E2, diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 8d90517bd2..849558b7c1 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -1,14 +1,11 @@ -import path from "node:path"; import { tablegen } from "@latticexyz/store/codegen"; import { worldgen } from "@latticexyz/world/node"; import { World as WorldConfig } from "@latticexyz/world"; import { forge, getRemappings } from "@latticexyz/common/foundry"; -import { getExistingContracts } from "./utils/getExistingContracts"; import { execa } from "execa"; type BuildOptions = { foundryProfile?: string; - srcDir: string; /** * MUD project root directory where all other relative paths are resolved from. * @@ -21,16 +18,11 @@ type BuildOptions = { export async function build({ rootDir, config, - srcDir, foundryProfile = process.env.FOUNDRY_PROFILE, }: BuildOptions): Promise { - const outPath = path.join(srcDir, config.codegen.outputDirectory); const remappings = await getRemappings(foundryProfile); - await Promise.all([ - tablegen({ rootDir, config, remappings }), - worldgen(config, getExistingContracts(srcDir), outPath), - ]); + await Promise.all([tablegen({ rootDir, config, remappings }), worldgen({ rootDir, config })]); await forge(["build"], { profile: foundryProfile }); await execa("mud", ["abi-ts"], { stdio: "inherit" }); diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index fba961ee84..6a5187a9f2 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,7 +1,6 @@ import type { CommandModule } from "yargs"; import { loadConfig, resolveConfigPath } from "@latticexyz/config/node"; import { World as WorldConfig } from "@latticexyz/world"; -import { getSrcDirectory } from "@latticexyz/common/foundry"; import { build } from "../build"; import path from "node:path"; @@ -25,9 +24,8 @@ const commandModule: CommandModule = { async handler(opts) { const configPath = await resolveConfigPath(opts.configPath); const config = (await loadConfig(configPath)) as WorldConfig; - const srcDir = await getSrcDirectory(); - await build({ rootDir: path.dirname(configPath), config, srcDir, foundryProfile: opts.profile }); + await build({ rootDir: path.dirname(configPath), config, foundryProfile: opts.profile }); process.exit(0); }, diff --git a/packages/cli/src/commands/dev-contracts.ts b/packages/cli/src/commands/dev-contracts.ts index ab8d655c7a..dff19975dc 100644 --- a/packages/cli/src/commands/dev-contracts.ts +++ b/packages/cli/src/commands/dev-contracts.ts @@ -90,7 +90,6 @@ const commandModule: CommandModule = { }, configPath: { type: "string", description: "Path to the MUD config file" }, profile: { type: "string", description: "The foundry profile to use" }, - srcDir: { type: "string", description: "Source directory. Defaults to foundry src directory." }, rpc: { type: "string", description: "json rpc endpoint. Defaults to foundry's configured eth_rpc_url" }, }); }, async handler(args) { - args.profile ??= process.env.FOUNDRY_PROFILE; - const { profile } = args; - args.srcDir ??= await getSrcDirectory(profile); - args.rpc ??= await getRpcUrl(profile); - const { tx, configPath, srcDir, rpc } = args; + const configPath = await resolveConfigPath(args.configPath); + const rootDir = path.dirname(configPath); - const existingContracts = getExistingContracts(srcDir); + const profile = args.profile ?? process.env.FOUNDRY_PROFILE; + const rpc = args.rpc ?? (await getRpcUrl(profile)); // Load the config - const configV2 = (await loadConfig(configPath)) as WorldConfig; - const mudConfig = worldToV1(configV2); - - const resolvedConfig = resolveWorldConfig( - mudConfig, - existingContracts.map(({ basename }) => basename), - ); + const config = (await loadConfig(configPath)) as WorldConfig; // Get worldAddress either from args or from worldsFile - const worldAddress = args.worldAddress ?? (await getWorldAddress(mudConfig.worldsFile, rpc)); + const worldAddress = args.worldAddress ?? (await getWorldAddress(config.deploy.worldsFile, rpc)); // Create World contract instance from deployed address const provider = new ethers.providers.StaticJsonRpcProvider(rpc); const WorldContract = new ethers.Contract(worldAddress, IBaseWorldAbi, provider); - // TODO account for multiple namespaces (https://github.com/latticexyz/mud/issues/994) - const namespace = mudConfig.namespace; - const names = Object.values(resolvedConfig.systems).map(({ name }) => name); + // TODO: replace with system.namespace + const namespace = config.namespace; + const systems = await resolveSystems({ rootDir, config }); // Fetch system table field layout from chain const systemTableFieldLayout = await WorldContract.getFieldLayout(systemsTableId); - const labels: { name: string; address: string }[] = []; - for (const name of names) { - const systemSelector = resourceToHex({ type: "system", namespace, name }); - // Get the first field of `Systems` table (the table maps system name to its address and other data) - const address = await WorldContract.getField(systemsTableId, [systemSelector], 0, systemTableFieldLayout); - labels.push({ name, address }); - } + const labels = await Promise.all( + systems.map(async (system) => { + // TODO: replace with system.systemId + const systemId = resourceToHex({ type: "system", namespace, name: system.name }); + // Get the first field of `Systems` table (the table maps system name to its address and other data) + const address = await WorldContract.getField(systemsTableId, [systemId], 0, systemTableFieldLayout); + return { name: system.name, address }; + }), + ); const result = await cast([ "run", "--label", `${worldAddress}:World`, ...labels.map(({ name, address }) => ["--label", `${address}:${name}`]).flat(), - `${tx}`, + `${args.tx}`, ]); console.log(result); diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index ba5935ef91..cfaa6e625c 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -1,15 +1,14 @@ import type { CommandModule, InferredOptionTypes } from "yargs"; import { verify } from "../verify"; -import { loadConfig } from "@latticexyz/config/node"; +import { loadConfig, resolveConfigPath } from "@latticexyz/config/node"; import { World as WorldConfig } from "@latticexyz/world"; -import { resolveWorldConfig } from "@latticexyz/world/internal"; -import { worldToV1 } from "@latticexyz/world/config/v2"; -import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry"; -import { getExistingContracts } from "../utils/getExistingContracts"; +import { resolveSystems } from "@latticexyz/world/node"; +import { getOutDirectory, getRpcUrl } from "@latticexyz/common/foundry"; import { getContractData } from "../utils/getContractData"; import { Hex, createWalletClient, http } from "viem"; import chalk from "chalk"; import { configToModules } from "../deploy/configToModules"; +import path from "node:path"; const verifyOptions = { deployerAddress: { @@ -24,7 +23,6 @@ const verifyOptions = { type: "boolean", desc: "Enable batch processing of RPC requests in viem client (defaults to batch size of 100 and wait of 1s)", }, - srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." }, verifier: { type: "string", desc: "The verifier to use. Defaults to blockscout", default: "blockscout" }, verifierUrl: { type: "string", @@ -46,10 +44,11 @@ const commandModule: CommandModule = { async handler(opts) { const profile = opts.profile ?? process.env.FOUNDRY_PROFILE; - const configV2 = (await loadConfig(opts.configPath)) as WorldConfig; - const config = worldToV1(configV2); + const configPath = await resolveConfigPath(opts.configPath); + const rootDir = path.dirname(configPath); + + const config = (await loadConfig(configPath)) as WorldConfig; - const srcDir = opts.srcDir ?? (await getSrcDirectory(profile)); const outDir = await getOutDirectory(profile); const rpc = opts.rpc ?? (await getRpcUrl(profile)); @@ -70,19 +69,17 @@ const commandModule: CommandModule = { }), }); - const contractNames = getExistingContracts(srcDir).map(({ basename }) => basename); - const resolvedWorldConfig = resolveWorldConfig(config, contractNames); - - const systems = Object.keys(resolvedWorldConfig.systems).map((name) => { - const contractData = getContractData(`${name}.sol`, name, outDir); - + // TODO: replace with `resolveConfig` and support for linked libs + const configSystems = await resolveSystems({ rootDir, config }); + const systems = configSystems.map((system) => { + const contractData = getContractData(`${system.name}.sol`, system.name, outDir); return { - name, + name: system.name, bytecode: contractData.bytecode, }; }); - const modules = await configToModules(configV2, outDir); + const modules = await configToModules(config, outDir); await verify({ client, diff --git a/packages/cli/src/commands/worldgen.ts b/packages/cli/src/commands/worldgen.ts index 3a87a7348b..9ebdaa3a0a 100644 --- a/packages/cli/src/commands/worldgen.ts +++ b/packages/cli/src/commands/worldgen.ts @@ -1,17 +1,12 @@ +import path from "node:path"; import type { CommandModule } from "yargs"; -import { loadConfig } from "@latticexyz/config/node"; +import { loadConfig, resolveConfigPath } from "@latticexyz/config/node"; import { World as WorldConfig } from "@latticexyz/world"; import { worldgen } from "@latticexyz/world/node"; -import { getSrcDirectory } from "@latticexyz/common/foundry"; -import path from "path"; -import { rmSync } from "fs"; -import { getExistingContracts } from "../utils/getExistingContracts"; type Options = { configPath?: string; clean?: boolean; - srcDir?: string; - config?: WorldConfig; }; const commandModule: CommandModule = { @@ -37,22 +32,11 @@ const commandModule: CommandModule = { }; export async function worldgenHandler(args: Options) { - const srcDir = args.srcDir ?? (await getSrcDirectory()); + const configPath = await resolveConfigPath(args.configPath); + const config = (await loadConfig(configPath)) as WorldConfig; + const rootDir = path.dirname(configPath); - const existingContracts = getExistingContracts(srcDir); - - // Load the config - const mudConfig = args.config ?? ((await loadConfig(args.configPath)) as WorldConfig); - - const outputBaseDirectory = path.join(srcDir, mudConfig.codegen.outputDirectory); - - // clear the worldgen directory - if (args.clean) { - rmSync(path.join(outputBaseDirectory, mudConfig.codegen.worldgenDirectory), { recursive: true, force: true }); - } - - // generate new interfaces - await worldgen(mudConfig, existingContracts, outputBaseDirectory); + await worldgen({ rootDir, config, clean: args.clean }); } export default commandModule; diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 0a409b8cbb..b1d38daefe 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -2,22 +2,16 @@ import { Abi, Address, Hex, padHex } from "viem"; import storeConfig from "@latticexyz/store/mud.config"; import worldConfig from "@latticexyz/world/mud.config"; import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" }; -import { Tables, configToTables } from "./configToTables"; import { helloStoreEvent } from "@latticexyz/store"; -import { StoreConfig } from "@latticexyz/store/internal"; import { helloWorldEvent } from "@latticexyz/world"; -import { WorldConfig } from "@latticexyz/world/internal"; -import { storeToV1 } from "@latticexyz/store/config/v2"; -import { worldToV1 } from "@latticexyz/world/config/v2"; export const salt = padHex("0x", { size: 32 }); // https://eips.ethereum.org/EIPS/eip-170 export const contractSizeLimit = parseInt("6000", 16); -// TODO: add `as const` to mud config so these get more strongly typed (blocked by current config parsing not using readonly) -export const storeTables = configToTables(storeToV1(storeConfig)); -export const worldTables = configToTables(worldToV1(worldConfig)); +export const storeTables = storeConfig.tables; +export const worldTables = worldConfig.tables; export const worldDeployEvents = [helloStoreEvent, helloWorldEvent] as const; @@ -93,6 +87,7 @@ export type Library = DeterministicContract & { }; export type System = DeterministicContract & { + // TODO: add label readonly namespace: string; readonly name: string; readonly systemId: Hex; @@ -111,10 +106,3 @@ export type Module = DeterministicContract & { readonly installAsRoot: boolean; readonly installData: Hex; // TODO: figure out better naming for this }; - -export type ConfigInput = StoreConfig & WorldConfig; -export type Config = { - readonly tables: Tables; - readonly systems: readonly System[]; - readonly libraries: readonly Library[]; -}; diff --git a/packages/cli/src/deploy/configToTables.ts b/packages/cli/src/deploy/configToTables.ts deleted file mode 100644 index 82392e1b50..0000000000 --- a/packages/cli/src/deploy/configToTables.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { resourceToHex } from "@latticexyz/common"; -import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser/internal"; -import { SchemaAbiType, StaticAbiType } from "@latticexyz/schema-type/internal"; -import { StoreConfig, resolveUserTypes } from "@latticexyz/store/internal"; -import { Hex } from "viem"; - -// TODO: we shouldn't need this file once our config parsing returns nicely formed tables - -type UserTypes = config["userTypes"]; -// TODO: fix strong enum types and avoid every schema getting `{ [k: string]: "uint8" }` -// type UserTypes = config["userTypes"] & { -// [k in keyof config["enums"]]: { internalType: "uint8" }; -// }; - -export type TableKey< - config extends StoreConfig = StoreConfig, - table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]], -> = `${config["namespace"]}_${table["name"]}`; - -export type Table< - config extends StoreConfig = StoreConfig, - table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]], -> = { - readonly namespace: config["namespace"]; - readonly name: table["name"]; - readonly tableId: Hex; - readonly keySchema: table["keySchema"] extends KeySchema> - ? KeySchema & { - readonly [k in keyof table["keySchema"]]: UserTypes[table["keySchema"][k]]["internalType"] extends StaticAbiType - ? UserTypes[table["keySchema"][k]]["internalType"] - : table["keySchema"][k]; - } - : KeySchema; - readonly valueSchema: table["valueSchema"] extends ValueSchema> - ? { - readonly [k in keyof table["valueSchema"]]: UserTypes[table["valueSchema"][k]]["internalType"] extends SchemaAbiType - ? UserTypes[table["valueSchema"][k]]["internalType"] - : table["valueSchema"][k]; - } - : ValueSchema; -}; - -export type Tables = { - readonly [k in keyof config["tables"] as TableKey]: Table; -}; - -export function configToTables(config: config): Tables { - const userTypes = { - ...config.userTypes, - ...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)), - }; - return Object.fromEntries( - Object.entries(config.tables).map(([tableName, table]) => [ - `${config.namespace}_${tableName}` satisfies TableKey, - { - namespace: config.namespace, - name: table.name, - tableId: resourceToHex({ - type: table.offchainOnly ? "offchainTable" : "table", - namespace: config.namespace, - name: table.name, - }), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - keySchema: resolveUserTypes(table.keySchema, userTypes) as any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - valueSchema: resolveUserTypes(table.valueSchema, userTypes) as any, - } satisfies Table, - ]), - ) as Tables; -} diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index e407a44116..b14378be88 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -2,23 +2,25 @@ import { Account, Address, Chain, Client, Hex, Transport } from "viem"; import { ensureDeployer } from "./ensureDeployer"; import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; -import { Config, ConfigInput, Module, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; +import { Library, Module, System, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; import { ensureSystems } from "./ensureSystems"; import { waitForTransactionReceipt } from "viem/actions"; import { getWorldDeploy } from "./getWorldDeploy"; import { ensureFunctions } from "./ensureFunctions"; import { ensureModules } from "./ensureModules"; -import { Table } from "./configToTables"; import { ensureNamespaceOwner } from "./ensureNamespaceOwner"; import { debug } from "./debug"; import { resourceToLabel } from "@latticexyz/common"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { randomBytes } from "crypto"; import { ensureWorldFactory } from "./ensureWorldFactory"; +import { Table } from "@latticexyz/config"; -type DeployOptions = { +type DeployOptions = { client: Client; - config: Config; + tables: readonly Table[]; + systems: readonly System[]; + libraries: readonly Library[]; modules?: readonly Module[]; salt?: Hex; worldAddress?: Address; @@ -38,17 +40,17 @@ type DeployOptions = { * amount of work to make the world match the config (e.g. deploy new tables, * replace systems, etc.) */ -export async function deploy({ +export async function deploy({ client, - config, + tables, + systems, + libraries, modules = [], salt, worldAddress: existingWorldAddress, deployerAddress: initialDeployerAddress, withWorldProxy, -}: DeployOptions): Promise { - const tables = Object.values(config.tables) as Table[]; - +}: DeployOptions): Promise { const deployerAddress = initialDeployerAddress ?? (await ensureDeployer(client)); await ensureWorldFactory(client, deployerAddress, withWorldProxy); @@ -58,18 +60,18 @@ export async function deploy({ client, deployerAddress, contracts: [ - ...config.libraries.map((library) => ({ - bytecode: library.prepareDeploy(deployerAddress, config.libraries).bytecode, + ...libraries.map((library) => ({ + bytecode: library.prepareDeploy(deployerAddress, libraries).bytecode, deployedBytecodeSize: library.deployedBytecodeSize, label: `${library.path}:${library.name} library`, })), - ...config.systems.map((system) => ({ - bytecode: system.prepareDeploy(deployerAddress, config.libraries).bytecode, + ...systems.map((system) => ({ + bytecode: system.prepareDeploy(deployerAddress, libraries).bytecode, deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceToLabel(system)} system`, })), ...modules.map((mod) => ({ - bytecode: mod.prepareDeploy(deployerAddress, config.libraries).bytecode, + bytecode: mod.prepareDeploy(deployerAddress, libraries).bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, })), @@ -90,7 +92,7 @@ export async function deploy({ const namespaceTxs = await ensureNamespaceOwner({ client, worldDeploy, - resourceIds: [...tables.map((table) => table.tableId), ...config.systems.map((system) => system.systemId)], + resourceIds: [...tables.map((table) => table.tableId), ...systems.map((system) => system.systemId)], }); debug("waiting for all namespace registration transactions to confirm"); @@ -106,19 +108,19 @@ export async function deploy({ const systemTxs = await ensureSystems({ client, deployerAddress, - libraries: config.libraries, + libraries, worldDeploy, - systems: config.systems, + systems, }); const functionTxs = await ensureFunctions({ client, worldDeploy, - functions: config.systems.flatMap((system) => system.functions), + functions: systems.flatMap((system) => system.functions), }); const moduleTxs = await ensureModules({ client, deployerAddress, - libraries: config.libraries, + libraries, worldDeploy, modules, }); diff --git a/packages/cli/src/deploy/ensureNamespaceOwner.ts b/packages/cli/src/deploy/ensureNamespaceOwner.ts index 0771951e90..b90acc1f0f 100644 --- a/packages/cli/src/deploy/ensureNamespaceOwner.ts +++ b/packages/cli/src/deploy/ensureNamespaceOwner.ts @@ -35,7 +35,7 @@ export async function ensureNamespaceOwner({ const { owner } = await getTableValue({ client, worldDeploy, - table: worldTables.world_NamespaceOwner, + table: worldTables.world__NamespaceOwner, key: { namespaceId: resourceToHex({ type: "namespace", namespace, name: "" }) }, }); return [namespace, owner]; diff --git a/packages/cli/src/deploy/ensureTables.ts b/packages/cli/src/deploy/ensureTables.ts index 6fa4416c29..03ae8ffec6 100644 --- a/packages/cli/src/deploy/ensureTables.ts +++ b/packages/cli/src/deploy/ensureTables.ts @@ -1,12 +1,20 @@ import { Client, Transport, Chain, Account, Hex } from "viem"; -import { Table } from "./configToTables"; import { resourceToLabel, writeContract } from "@latticexyz/common"; import { WorldDeploy, worldAbi } from "./common"; -import { valueSchemaToFieldLayoutHex, keySchemaToHex, valueSchemaToHex } from "@latticexyz/protocol-parser/internal"; +import { + valueSchemaToFieldLayoutHex, + keySchemaToHex, + valueSchemaToHex, + getSchemaTypes, + getValueSchema, + getKeySchema, + KeySchema, +} from "@latticexyz/protocol-parser/internal"; import { debug } from "./debug"; import { getTables } from "./getTables"; import pRetry from "p-retry"; import { wait } from "@latticexyz/common/utils"; +import { Table } from "@latticexyz/config"; export async function ensureTables({ client, @@ -29,8 +37,10 @@ export async function ensureTables({ if (missingTables.length) { debug("registering tables", missingTables.map(resourceToLabel).join(", ")); return await Promise.all( - missingTables.map((table) => - pRetry( + missingTables.map((table) => { + const keySchema = getSchemaTypes(getKeySchema(table)); + const valueSchema = getSchemaTypes(getValueSchema(table)); + return pRetry( () => writeContract(client, { chain: client.chain ?? null, @@ -40,11 +50,11 @@ export async function ensureTables({ functionName: "registerTable", args: [ table.tableId, - valueSchemaToFieldLayoutHex(table.valueSchema), - keySchemaToHex(table.keySchema), - valueSchemaToHex(table.valueSchema), - Object.keys(table.keySchema), - Object.keys(table.valueSchema), + valueSchemaToFieldLayoutHex(valueSchema), + keySchemaToHex(keySchema as KeySchema), + valueSchemaToHex(valueSchema), + Object.keys(keySchema), + Object.keys(valueSchema), ], }), { @@ -55,8 +65,8 @@ export async function ensureTables({ await wait(delay); }, }, - ), - ), + ); + }), ); } diff --git a/packages/cli/src/deploy/getFunctions.ts b/packages/cli/src/deploy/getFunctions.ts index 5508b49480..5d24a0a3f2 100644 --- a/packages/cli/src/deploy/getFunctions.ts +++ b/packages/cli/src/deploy/getFunctions.ts @@ -3,7 +3,13 @@ import { WorldDeploy, WorldFunction, worldTables } from "./common"; import { debug } from "./debug"; import { storeSetRecordEvent } from "@latticexyz/store"; import { getLogs } from "viem/actions"; -import { decodeKey, decodeValueArgs } from "@latticexyz/protocol-parser/internal"; +import { + decodeKey, + decodeValueArgs, + getKeySchema, + getSchemaTypes, + getValueSchema, +} from "@latticexyz/protocol-parser/internal"; export async function getFunctions({ client, @@ -20,13 +26,13 @@ export async function getFunctions({ toBlock: worldDeploy.stateBlock, address: worldDeploy.address, event: parseAbiItem(storeSetRecordEvent), - args: { tableId: worldTables.world_FunctionSelectors.tableId }, + args: { tableId: worldTables.world__FunctionSelectors.tableId }, }); const selectors = selectorLogs.map((log) => { return { - ...decodeValueArgs(worldTables.world_FunctionSelectors.valueSchema, log.args), - ...decodeKey(worldTables.world_FunctionSelectors.keySchema, log.args.keyTuple), + ...decodeValueArgs(getSchemaTypes(getValueSchema(worldTables.world__FunctionSelectors)), log.args), + ...decodeKey(getSchemaTypes(getKeySchema(worldTables.world__FunctionSelectors)), log.args.keyTuple), }; }); debug("found", selectors.length, "function selectors for", worldDeploy.address); @@ -39,14 +45,16 @@ export async function getFunctions({ toBlock: worldDeploy.stateBlock, address: worldDeploy.address, event: parseAbiItem(storeSetRecordEvent), - args: { tableId: worldTables.world_FunctionSignatures.tableId }, + args: { tableId: worldTables.world__FunctionSignatures.tableId }, }); const selectorToSignature = Object.fromEntries( signatureLogs.map((log) => { return [ - decodeKey(worldTables.world_FunctionSignatures.keySchema, log.args.keyTuple).functionSelector, - decodeValueArgs(worldTables.world_FunctionSignatures.valueSchema, log.args).functionSignature, + decodeKey(getSchemaTypes(getKeySchema(worldTables.world__FunctionSignatures)), log.args.keyTuple) + .functionSelector, + decodeValueArgs(getSchemaTypes(getValueSchema(worldTables.world__FunctionSignatures)), log.args) + .functionSignature, ]; }), ); diff --git a/packages/cli/src/deploy/getResourceAccess.ts b/packages/cli/src/deploy/getResourceAccess.ts index 56a2dbba8a..e35a50c9d2 100644 --- a/packages/cli/src/deploy/getResourceAccess.ts +++ b/packages/cli/src/deploy/getResourceAccess.ts @@ -3,7 +3,7 @@ import { WorldDeploy, worldTables } from "./common"; import { debug } from "./debug"; import { storeSpliceStaticDataEvent } from "@latticexyz/store"; import { getLogs } from "viem/actions"; -import { decodeKey } from "@latticexyz/protocol-parser/internal"; +import { decodeKey, getKeySchema, getSchemaTypes } from "@latticexyz/protocol-parser/internal"; import { getTableValue } from "./getTableValue"; export async function getResourceAccess({ @@ -26,16 +26,18 @@ export async function getResourceAccess({ // our usage of `ResourceAccess._set(...)` emits a splice instead of set record // TODO: https://github.com/latticexyz/mud/issues/479 event: parseAbiItem(storeSpliceStaticDataEvent), - args: { tableId: worldTables.world_ResourceAccess.tableId }, + args: { tableId: worldTables.world__ResourceAccess.tableId }, }); - const keys = logs.map((log) => decodeKey(worldTables.world_ResourceAccess.keySchema, log.args.keyTuple)); + const keys = logs.map((log) => + decodeKey(getSchemaTypes(getKeySchema(worldTables.world__ResourceAccess)), log.args.keyTuple), + ); const access = ( await Promise.all( keys.map( async (key) => - [key, await getTableValue({ client, worldDeploy, table: worldTables.world_ResourceAccess, key })] as const, + [key, await getTableValue({ client, worldDeploy, table: worldTables.world__ResourceAccess, key })] as const, ), ) ) diff --git a/packages/cli/src/deploy/getResourceIds.ts b/packages/cli/src/deploy/getResourceIds.ts index 239709f0df..d019b25d47 100644 --- a/packages/cli/src/deploy/getResourceIds.ts +++ b/packages/cli/src/deploy/getResourceIds.ts @@ -24,7 +24,7 @@ export async function getResourceIds({ fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, event: parseAbiItem(storeSpliceStaticDataEvent), - args: { tableId: storeTables.store_ResourceIds.tableId }, + args: { tableId: storeTables.store__ResourceIds.tableId }, }), { retries: 3, diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts index 1c2831e26e..43454d16e9 100644 --- a/packages/cli/src/deploy/getSystems.ts +++ b/packages/cli/src/deploy/getSystems.ts @@ -27,7 +27,7 @@ export async function getSystems({ const { system: address, publicAccess } = await getTableValue({ client, worldDeploy, - table: worldTables.world_Systems, + table: worldTables.world__Systems, key: { systemId: system.resourceId }, }); const systemFunctions = functions.filter((func) => func.systemId === system.resourceId); diff --git a/packages/cli/src/deploy/getTableValue.ts b/packages/cli/src/deploy/getTableValue.ts index c0bcc5d662..fbc63f5aba 100644 --- a/packages/cli/src/deploy/getTableValue.ts +++ b/packages/cli/src/deploy/getTableValue.ts @@ -1,8 +1,15 @@ -import { SchemaToPrimitives, decodeValueArgs, encodeKey } from "@latticexyz/protocol-parser/internal"; +import { + decodeValueArgs, + encodeKey, + getKeySchema, + getSchemaPrimitives, + getSchemaTypes, + getValueSchema, +} from "@latticexyz/protocol-parser/internal"; import { WorldDeploy, worldAbi } from "./common"; import { Client, Hex } from "viem"; import { readContract } from "viem/actions"; -import { Table } from "./configToTables"; +import { Table } from "@latticexyz/config"; export async function getTableValue({ client, @@ -13,17 +20,17 @@ export async function getTableValue
({ readonly client: Client; readonly worldDeploy: WorldDeploy; readonly table: table; - readonly key: SchemaToPrimitives; -}): Promise> { + readonly key: getSchemaPrimitives>; +}): Promise>> { const [staticData, encodedLengths, dynamicData] = (await readContract(client, { blockNumber: worldDeploy.stateBlock, address: worldDeploy.address, abi: worldAbi, functionName: "getRecord", - args: [table.tableId, encodeKey(table.keySchema, key)], + args: [table.tableId, encodeKey(getSchemaTypes(getKeySchema(table)) as never, key as never)], // TODO: remove cast once https://github.com/wevm/viem/issues/2125 is resolved })) as [Hex, Hex, Hex]; - return decodeValueArgs(table.valueSchema, { + return decodeValueArgs(getSchemaTypes(getValueSchema(table)), { staticData, encodedLengths, dynamicData, diff --git a/packages/cli/src/deploy/getTables.ts b/packages/cli/src/deploy/getTables.ts index 660c593c1a..8dd07727d7 100644 --- a/packages/cli/src/deploy/getTables.ts +++ b/packages/cli/src/deploy/getTables.ts @@ -1,11 +1,18 @@ import { Client, parseAbiItem, decodeAbiParameters, parseAbiParameters } from "viem"; -import { Table } from "./configToTables"; import { hexToResource } from "@latticexyz/common"; import { WorldDeploy, storeTables } from "./common"; import { debug } from "./debug"; import { storeSetRecordEvent } from "@latticexyz/store"; import { getLogs } from "viem/actions"; -import { KeySchema, ValueSchema, decodeKey, decodeValueArgs, hexToSchema } from "@latticexyz/protocol-parser/internal"; +import { + decodeKey, + decodeValueArgs, + getKeySchema, + getSchemaTypes, + getValueSchema, + hexToSchema, +} from "@latticexyz/protocol-parser/internal"; +import { Schema, Table } from "@latticexyz/config"; export async function getTables({ client, @@ -27,30 +34,40 @@ export async function getTables({ toBlock: worldDeploy.stateBlock, address: worldDeploy.address, event: parseAbiItem(storeSetRecordEvent), - args: { tableId: storeTables.store_Tables.tableId }, + args: { tableId: storeTables.store__Tables.tableId }, }); // TODO: combine with store-sync logToTable and export from somewhere - const tables = logs.map((log) => { - const { tableId } = decodeKey(storeTables.store_Tables.keySchema, log.args.keyTuple); - const { namespace, name } = hexToResource(tableId); - const value = decodeValueArgs(storeTables.store_Tables.valueSchema, log.args); - - // TODO: migrate to better helper - const keySchemaFields = hexToSchema(value.keySchema); - const valueSchemaFields = hexToSchema(value.valueSchema); + const tables = logs.map((log): Table => { + const { tableId } = decodeKey(getSchemaTypes(getKeySchema(storeTables.store__Tables)), log.args.keyTuple); + const { type, namespace, name } = hexToResource(tableId); + const value = decodeValueArgs(getSchemaTypes(getValueSchema(storeTables.store__Tables)), log.args); + + const solidityKeySchema = hexToSchema(value.keySchema); + const solidityValueSchema = hexToSchema(value.valueSchema); const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames)[0]; const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames)[0]; - const valueAbiTypes = [...valueSchemaFields.staticFields, ...valueSchemaFields.dynamicFields]; + const valueAbiTypes = [...solidityValueSchema.staticFields, ...solidityValueSchema.dynamicFields]; const keySchema = Object.fromEntries( - keySchemaFields.staticFields.map((abiType, i) => [keyNames[i], abiType]), - ) as KeySchema; - const valueSchema = Object.fromEntries(valueAbiTypes.map((abiType, i) => [fieldNames[i], abiType])) as ValueSchema; + solidityKeySchema.staticFields.map((abiType, i) => [keyNames[i], { type: abiType, internalType: abiType }]), + ) satisfies Schema; + + const valueSchema = Object.fromEntries( + valueAbiTypes.map((abiType, i) => [fieldNames[i], { type: abiType, internalType: abiType }]), + ) satisfies Schema; - return { namespace, name, tableId, keySchema, valueSchema } as const; + return { + type: type as never, + namespace, + name, + tableId, + schema: { ...keySchema, ...valueSchema }, + key: Object.keys(keySchema), + }; }); + // TODO: filter/detect duplicates? debug("found", tables.length, "tables for", worldDeploy.address); diff --git a/packages/cli/src/deploy/resolveConfig.ts b/packages/cli/src/deploy/resolveConfig.ts index 0a4466f7dc..6855edc344 100644 --- a/packages/cli/src/deploy/resolveConfig.ts +++ b/packages/cli/src/deploy/resolveConfig.ts @@ -1,26 +1,28 @@ import path from "path"; -import { resolveWorldConfig } from "@latticexyz/world/internal"; -import { Config, ConfigInput, Library, System, WorldFunction } from "./common"; +import { resolveSystems } from "@latticexyz/world/node"; +import { Library, System, WorldFunction } from "./common"; import { resourceToHex } from "@latticexyz/common"; -import { Hex, toFunctionSelector, toFunctionSignature } from "viem"; -import { getExistingContracts } from "../utils/getExistingContracts"; +import { Hex, isHex, toFunctionSelector, toFunctionSignature } from "viem"; import { getContractData } from "../utils/getContractData"; -import { configToTables } from "./configToTables"; import { groupBy } from "@latticexyz/common/utils"; import { findLibraries } from "./findLibraries"; import { createPrepareDeploy } from "./createPrepareDeploy"; +import { World } from "@latticexyz/world"; -// TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668 +// TODO: replace this with a manifest/combined config output -export function resolveConfig({ +export async function resolveConfig({ + rootDir, config, - forgeSourceDir, forgeOutDir, }: { - config: config; - forgeSourceDir: string; + rootDir: string; + config: World; forgeOutDir: string; -}): Config { +}): Promise<{ + readonly systems: readonly System[]; + readonly libraries: readonly Library[]; +}> { const libraries = findLibraries(forgeOutDir).map((library): Library => { // foundry/solc flattens artifacts, so we just use the path basename const contractData = getContractData(path.basename(library.path), library.name, forgeOutDir); @@ -33,21 +35,21 @@ export function resolveConfig({ }; }); - const tables = configToTables(config); - - // TODO: should the config parser/loader help with resolving systems? - const contractNames = getExistingContracts(forgeSourceDir).map(({ basename }) => basename); - const resolvedConfig = resolveWorldConfig(config, contractNames); const baseSystemContractData = getContractData("System.sol", "System", forgeOutDir); const baseSystemFunctions = baseSystemContractData.abi .filter((item): item is typeof item & { type: "function" } => item.type === "function") .map(toFunctionSignature); - const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]): System => { + const configSystems = await resolveSystems({ rootDir, config }); + + const systems = configSystems.map((system): System => { + // TODO: replace with system.namespace const namespace = config.namespace; const name = system.name; + // TODO: replace with system.systemId const systemId = resourceToHex({ type: "system", namespace, name }); - const contractData = getContractData(`${systemName}.sol`, systemName, forgeOutDir); + // TODO: replace system.name with system.label + const contractData = getContractData(`${system.label}.sol`, system.label, forgeOutDir); const systemFunctions = contractData.abi .filter((item): item is typeof item & { type: "function" } => item.type === "function") @@ -65,15 +67,23 @@ export function resolveConfig({ }; }); + // TODO: move to resolveSystems? + const allowedAddresses = system.accessList.filter((target): target is Hex => isHex(target)); + const allowedSystemIds = system.accessList + .filter((target) => !isHex(target)) + .map((label) => { + const system = configSystems.find((s) => s.label === label)!; + // TODO: replace with system.systemId + return resourceToHex({ type: "system", namespace, name: system.name }); + }); + return { namespace, name, systemId, allowAll: system.openAccess, - allowedAddresses: system.accessListAddresses as Hex[], - allowedSystemIds: system.accessListSystems.map((name) => - resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name }), - ), + allowedAddresses, + allowedSystemIds, prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, @@ -97,7 +107,6 @@ export function resolveConfig({ } return { - tables, systems, libraries, }; diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts index c8c9c3fa09..9ef63357c9 100644 --- a/packages/cli/src/runDeploy.ts +++ b/packages/cli/src/runDeploy.ts @@ -6,8 +6,7 @@ import { createWalletClient, http, Hex, isHex } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { loadConfig, resolveConfigPath } from "@latticexyz/config/node"; import { World as WorldConfig } from "@latticexyz/world"; -import { worldToV1 } from "@latticexyz/world/config/v2"; -import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry"; +import { getOutDirectory, getRpcUrl } from "@latticexyz/common/foundry"; import chalk from "chalk"; import { MUDError } from "@latticexyz/common/errors"; import { resolveConfig } from "./deploy/resolveConfig"; @@ -33,7 +32,6 @@ export const deployOptions = { desc: "Deploy using an existing deterministic deployer (https://github.com/Arachnid/deterministic-deployment-proxy)", }, worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" }, - srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." }, skipBuild: { type: "boolean", desc: "Skip rebuilding the contracts before deploying" }, alwaysRunPostDeploy: { type: "boolean", @@ -65,13 +63,13 @@ export async function runDeploy(opts: DeployOptions): Promise { const profile = opts.profile ?? process.env.FOUNDRY_PROFILE; const configPath = await resolveConfigPath(opts.configPath); - const configV2 = (await loadConfig(configPath)) as WorldConfig; - const config = worldToV1(configV2); + const config = (await loadConfig(configPath)) as WorldConfig; + const rootDir = path.dirname(configPath); + if (opts.printConfig) { console.log(chalk.green("\nResolved config:\n"), JSON.stringify(config, null, 2)); } - const srcDir = opts.srcDir ?? (await getSrcDirectory(profile)); const outDir = await getOutDirectory(profile); const rpc = opts.rpc ?? (await getRpcUrl(profile)); @@ -83,11 +81,15 @@ export async function runDeploy(opts: DeployOptions): Promise { // Run build if (!opts.skipBuild) { - await build({ rootDir: path.dirname(configPath), config: configV2, srcDir, foundryProfile: profile }); + await build({ rootDir, config, foundryProfile: profile }); } - const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir }); - const modules = await configToModules(configV2, outDir); + const { systems, libraries } = await resolveConfig({ + rootDir, + config, + forgeOutDir: outDir, + }); + const modules = await configToModules(config, outDir); const account = await (async () => { if (opts.kms) { @@ -133,13 +135,15 @@ export async function runDeploy(opts: DeployOptions): Promise { salt, worldAddress: opts.worldAddress as Hex | undefined, client, - config: resolvedConfig, + tables: Object.values(config.tables), + systems, + libraries, modules, - withWorldProxy: configV2.deploy.upgradeableWorldImplementation, + withWorldProxy: config.deploy.upgradeableWorldImplementation, }); if (opts.worldAddress == null || opts.alwaysRunPostDeploy) { await postDeploy( - config.postDeployScript, + config.deploy.postDeployScript, worldDeploy.address, rpc, profile, @@ -156,23 +160,27 @@ export async function runDeploy(opts: DeployOptions): Promise { if (opts.saveDeployment) { const chainId = await getChainId(client); - const deploysDir = path.join(config.deploysDirectory, chainId.toString()); + const deploysDir = path.join(config.deploy.deploysDirectory, chainId.toString()); mkdirSync(deploysDir, { recursive: true }); writeFileSync(path.join(deploysDir, "latest.json"), JSON.stringify(deploymentInfo, null, 2)); writeFileSync(path.join(deploysDir, Date.now() + ".json"), JSON.stringify(deploymentInfo, null, 2)); const localChains = [1337, 31337]; - const deploys = existsSync(config.worldsFile) ? JSON.parse(readFileSync(config.worldsFile, "utf-8")) : {}; + const deploys = existsSync(config.deploy.worldsFile) + ? JSON.parse(readFileSync(config.deploy.worldsFile, "utf-8")) + : {}; deploys[chainId] = { address: deploymentInfo.worldAddress, // We expect the worlds file to be committed and since local deployments are often // a consistent address but different block number, we'll ignore the block number. blockNumber: localChains.includes(chainId) ? undefined : deploymentInfo.blockNumber, }; - writeFileSync(config.worldsFile, JSON.stringify(deploys, null, 2)); + writeFileSync(config.deploy.worldsFile, JSON.stringify(deploys, null, 2)); console.log( - chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${config.worldsFile} and ${deploysDir}): \n`)), + chalk.bgGreen( + chalk.whiteBright(`\n Deployment result (written to ${config.deploy.worldsFile} and ${deploysDir}): \n`), + ), ); } diff --git a/packages/common/src/codegen/render-solidity/renderEnums.test.ts b/packages/common/src/codegen/render-solidity/renderEnums.test.ts new file mode 100644 index 0000000000..937595dc73 --- /dev/null +++ b/packages/common/src/codegen/render-solidity/renderEnums.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { renderEnums } from "./renderEnums"; + +describe("renderEnums", () => { + it("renders a Solidity file with enums", () => { + expect( + renderEnums({ + Direction: ["left", "up", "right", "down"], + }), + ).toMatchInlineSnapshot(` + " + // SPDX-License-Identifier: MIT + pragma solidity >=0.8.24; + + /* Autogenerated file. Do not edit manually. */ + + enum Direction { + left, up, right, down + } + + " + `); + }); +}); diff --git a/packages/common/src/codegen/render-solidity/renderEnums.ts b/packages/common/src/codegen/render-solidity/renderEnums.ts index 74d456d81a..9fa1e46307 100644 --- a/packages/common/src/codegen/render-solidity/renderEnums.ts +++ b/packages/common/src/codegen/render-solidity/renderEnums.ts @@ -1,20 +1,24 @@ -import { renderArguments, renderList, renderedSolidityHeader } from "./common"; -import { RenderEnum } from "./types"; +import { renderedSolidityHeader } from "./common"; + +// importing this from config or store would be a cyclic dependency :( +type Enums = { + readonly [name: string]: readonly [string, ...string[]]; +}; /** * Render a list of enum data as solidity enum definitions */ -export function renderEnums(enums: RenderEnum[]): string { - let result = renderedSolidityHeader; - - result += renderList( - enums, - ({ name, memberNames }) => ` - enum ${name} { - ${renderArguments(memberNames)} - } - `, +export function renderEnums(enums: Enums): string { + const enumDefinitions = Object.entries(enums).map( + ([name, values]) => ` + enum ${name} { + ${values.join(", ")} + } + `, ); - return result; + return ` + ${renderedSolidityHeader} + ${enumDefinitions.join("")} + `; } diff --git a/packages/common/src/codegen/utils/formatAndWrite.ts b/packages/common/src/codegen/utils/formatAndWrite.ts index 1121d58262..f315eddfd4 100644 --- a/packages/common/src/codegen/utils/formatAndWrite.ts +++ b/packages/common/src/codegen/utils/formatAndWrite.ts @@ -1,5 +1,5 @@ -import { mkdir, writeFile } from "fs/promises"; -import { dirname } from "path"; +import fs from "node:fs/promises"; +import path from "node:path"; import { formatSolidity, formatTypescript } from "./format"; import { debug } from "../debug"; @@ -12,9 +12,9 @@ import { debug } from "../debug"; export async function formatAndWriteSolidity(output: string, fullOutputPath: string, logPrefix: string): Promise { const formattedOutput = await formatSolidity(output); - await mkdir(dirname(fullOutputPath), { recursive: true }); + await fs.mkdir(path.dirname(fullOutputPath), { recursive: true }); - await writeFile(fullOutputPath, formattedOutput); + await fs.writeFile(fullOutputPath, formattedOutput); debug(`${logPrefix}: ${fullOutputPath}`); } @@ -31,8 +31,8 @@ export async function formatAndWriteTypescript( ): Promise { const formattedOutput = await formatTypescript(output); - await mkdir(dirname(fullOutputPath), { recursive: true }); + await fs.mkdir(path.dirname(fullOutputPath), { recursive: true }); - await writeFile(fullOutputPath, formattedOutput); + await fs.writeFile(fullOutputPath, formattedOutput); debug(`${logPrefix}: ${fullOutputPath}`); } diff --git a/packages/store/test/codegen/common.sol b/packages/store/test/codegen/common.sol index 88e23a3c6d..3479f63bab 100644 --- a/packages/store/test/codegen/common.sol +++ b/packages/store/test/codegen/common.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.24; /* Autogenerated file. Do not edit manually. */ + enum ExampleEnum { None, First, diff --git a/packages/store/ts/codegen/renderTypesFromConfig.ts b/packages/store/ts/codegen/renderTypesFromConfig.ts index 94d660a5ab..982ce0f1e5 100644 --- a/packages/store/ts/codegen/renderTypesFromConfig.ts +++ b/packages/store/ts/codegen/renderTypesFromConfig.ts @@ -1,14 +1,9 @@ import { renderEnums } from "@latticexyz/common/codegen"; -import { StoreConfig } from "../config"; +import { Store } from "../config/v2"; /** * Renders Solidity code for enums defined in the provided config */ -export function renderTypesFromConfig(config: StoreConfig) { - const enums = Object.keys(config.enums).map((name) => ({ - name, - memberNames: config.enums[name], - })); - - return renderEnums(enums); +export function renderTypesFromConfig(config: Store) { + return renderEnums(config.enums); } diff --git a/packages/store/ts/codegen/tablegen.ts b/packages/store/ts/codegen/tablegen.ts index fe547032c3..af9c7aa1a7 100644 --- a/packages/store/ts/codegen/tablegen.ts +++ b/packages/store/ts/codegen/tablegen.ts @@ -1,12 +1,12 @@ +import fs from "node:fs/promises"; import path from "node:path"; import { formatAndWriteSolidity, loadAndExtractUserTypes } from "@latticexyz/common/codegen"; import { getTableOptions } from "./tableOptions"; import { renderTable } from "./renderTable"; import { renderTypesFromConfig } from "./renderTypesFromConfig"; import { renderTableIndex } from "./renderTableIndex"; -import { rm } from "fs/promises"; import { Store as StoreConfig } from "../config/v2/output"; -import { storeToV1 } from "../config/v2/compat"; +import { mapObject } from "@latticexyz/common/utils"; export type TablegenOptions = { /** @@ -19,14 +19,17 @@ export type TablegenOptions = { export async function tablegen({ rootDir, config, remappings }: TablegenOptions) { const outputDirectory = path.join(rootDir, config.sourceDirectory, config.codegen.outputDirectory); - const configV1 = storeToV1(config); - const solidityUserTypes = loadAndExtractUserTypes(configV1.userTypes, outputDirectory, remappings); + const solidityUserTypes = loadAndExtractUserTypes( + mapObject(config.userTypes, (type) => ({ ...type, internalType: type.type })), + outputDirectory, + remappings, + ); const allTableOptions = getTableOptions(config, solidityUserTypes); const uniqueTableDirectories = Array.from(new Set(allTableOptions.map(({ outputPath }) => path.dirname(outputPath)))); await Promise.all( uniqueTableDirectories.map(async (tableDir) => { - await rm(path.join(outputDirectory, tableDir), { recursive: true, force: true }); + await fs.rm(path.join(outputDirectory, tableDir), { recursive: true, force: true }); }), ); @@ -47,9 +50,9 @@ export async function tablegen({ rootDir, config, remappings }: TablegenOptions) } // write types to file - if (Object.keys(configV1.enums).length > 0) { + if (Object.keys(config.enums).length > 0) { const fullOutputPath = path.join(outputDirectory, config.codegen.userTypesFilename); - const output = renderTypesFromConfig(configV1); + const output = renderTypesFromConfig(config); await formatAndWriteSolidity(output, fullOutputPath, "Generated types file"); } } diff --git a/packages/world-modules/.gitignore b/packages/world-modules/.gitignore index 411b5668db..5ff362ddd2 100644 --- a/packages/world-modules/.gitignore +++ b/packages/world-modules/.gitignore @@ -6,3 +6,5 @@ DOCS.md artifacts yarn-error.log API + +src/codegen diff --git a/packages/world-modules/mud.config.ts b/packages/world-modules/mud.config.ts index c9813db299..6bf21c83b2 100644 --- a/packages/world-modules/mud.config.ts +++ b/packages/world-modules/mud.config.ts @@ -1,11 +1,6 @@ import { defineWorld } from "@latticexyz/world"; export default defineWorld({ - codegen: { - worldgenDirectory: "interfaces", - worldInterfaceName: "IBaseWorld", - outputDirectory: ".", - }, userTypes: { ResourceId: { filePath: "@latticexyz/store/src/ResourceId.sol", type: "bytes32" }, }, @@ -22,7 +17,7 @@ export default defineWorld({ }, key: ["valueHash"], codegen: { - outputDirectory: "modules/keyswithvalue/tables", + outputDirectory: "../modules/keyswithvalue/tables", tableIdArgument: true, storeArgument: true, }, @@ -43,7 +38,7 @@ export default defineWorld({ }, key: ["sourceTableId"], codegen: { - outputDirectory: "modules/keysintable/tables", + outputDirectory: "../modules/keysintable/tables", storeArgument: true, }, }, @@ -56,7 +51,7 @@ export default defineWorld({ }, key: ["sourceTableId", "keysHash"], codegen: { - outputDirectory: "modules/keysintable/tables", + outputDirectory: "../modules/keysintable/tables", dataStruct: false, storeArgument: true, }, @@ -72,7 +67,7 @@ export default defineWorld({ }, key: [], codegen: { - outputDirectory: "modules/uniqueentity/tables", + outputDirectory: "../modules/uniqueentity/tables", tableIdArgument: true, storeArgument: true, }, @@ -92,7 +87,7 @@ export default defineWorld({ }, key: ["delegator", "delegatee", "systemId", "callDataHash"], codegen: { - outputDirectory: "modules/std-delegations/tables", + outputDirectory: "../modules/std-delegations/tables", }, }, SystemboundDelegations: { @@ -104,7 +99,7 @@ export default defineWorld({ }, key: ["delegator", "delegatee", "systemId"], codegen: { - outputDirectory: "modules/std-delegations/tables", + outputDirectory: "../modules/std-delegations/tables", }, }, TimeboundDelegations: { @@ -115,7 +110,7 @@ export default defineWorld({ }, key: ["delegator", "delegatee"], codegen: { - outputDirectory: "modules/std-delegations/tables", + outputDirectory: "../modules/std-delegations/tables", }, }, /************************************************************************ @@ -130,7 +125,7 @@ export default defineWorld({ }, key: ["systemId"], codegen: { - outputDirectory: "modules/puppet/tables", + outputDirectory: "../modules/puppet/tables", tableIdArgument: true, }, }, @@ -146,7 +141,7 @@ export default defineWorld({ }, key: ["account"], codegen: { - outputDirectory: "modules/tokens/tables", + outputDirectory: "../modules/tokens/tables", tableIdArgument: true, }, }, @@ -163,7 +158,7 @@ export default defineWorld({ }, key: [], codegen: { - outputDirectory: "modules/erc20-puppet/tables", + outputDirectory: "../modules/erc20-puppet/tables", tableIdArgument: true, }, }, @@ -175,7 +170,7 @@ export default defineWorld({ }, key: ["account", "spender"], codegen: { - outputDirectory: "modules/erc20-puppet/tables", + outputDirectory: "../modules/erc20-puppet/tables", tableIdArgument: true, }, }, @@ -185,7 +180,7 @@ export default defineWorld({ }, key: [], codegen: { - outputDirectory: "modules/erc20-puppet/tables", + outputDirectory: "../modules/erc20-puppet/tables", tableIdArgument: true, }, }, @@ -196,7 +191,7 @@ export default defineWorld({ }, key: ["namespaceId"], codegen: { - outputDirectory: "modules/erc20-puppet/tables", + outputDirectory: "../modules/erc20-puppet/tables", tableIdArgument: true, }, }, @@ -213,7 +208,7 @@ export default defineWorld({ }, key: [], codegen: { - outputDirectory: "modules/erc721-puppet/tables", + outputDirectory: "../modules/erc721-puppet/tables", tableIdArgument: true, }, }, @@ -224,7 +219,7 @@ export default defineWorld({ }, key: ["tokenId"], codegen: { - outputDirectory: "modules/erc721-puppet/tables", + outputDirectory: "../modules/erc721-puppet/tables", tableIdArgument: true, }, }, @@ -235,7 +230,7 @@ export default defineWorld({ }, key: ["tokenId"], codegen: { - outputDirectory: "modules/erc721-puppet/tables", + outputDirectory: "../modules/erc721-puppet/tables", tableIdArgument: true, }, }, @@ -246,7 +241,7 @@ export default defineWorld({ }, key: ["tokenId"], codegen: { - outputDirectory: "modules/erc721-puppet/tables", + outputDirectory: "../modules/erc721-puppet/tables", tableIdArgument: true, }, }, @@ -258,7 +253,7 @@ export default defineWorld({ }, key: ["owner", "operator"], codegen: { - outputDirectory: "modules/erc721-puppet/tables", + outputDirectory: "../modules/erc721-puppet/tables", tableIdArgument: true, }, }, @@ -269,7 +264,7 @@ export default defineWorld({ }, key: ["namespaceId"], codegen: { - outputDirectory: "modules/erc721-puppet/tables", + outputDirectory: "../modules/erc721-puppet/tables", tableIdArgument: true, }, }, @@ -282,7 +277,7 @@ export default defineWorld({ schema: { signer: "address", nonce: "uint256" }, key: ["signer"], codegen: { - outputDirectory: "modules/callwithsignature/tables", + outputDirectory: "../modules/callwithsignature/tables", }, }, }, diff --git a/packages/world-modules/package.json b/packages/world-modules/package.json index d33320e1ab..e8b271611c 100644 --- a/packages/world-modules/package.json +++ b/packages/world-modules/package.json @@ -30,7 +30,7 @@ "build:abi": "forge build", "build:abi-ts": "abi-ts", "build:js": "tsup", - "build:mud": "tsx ./ts/scripts/tablegen.ts && tsx ./ts/scripts/worldgen.ts", + "build:mud": "mud build", "clean": "pnpm run clean:abi && pnpm run clean:js && pnpm run clean:mud", "clean:abi": "forge clean", "clean:js": "rimraf dist", @@ -51,6 +51,7 @@ }, "devDependencies": { "@latticexyz/abi-ts": "workspace:*", + "@latticexyz/cli": "workspace:*", "@latticexyz/gas-report": "workspace:*", "@types/ejs": "^3.1.1", "@types/mocha": "^9.1.1", diff --git a/packages/world-modules/src/index.sol b/packages/world-modules/src/index.sol deleted file mode 100644 index 5fa265ec1f..0000000000 --- a/packages/world-modules/src/index.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -import { KeysWithValue } from "./modules/keyswithvalue/tables/KeysWithValue.sol"; -import { KeysInTable, KeysInTableData } from "./modules/keysintable/tables/KeysInTable.sol"; -import { UsedKeysIndex } from "./modules/keysintable/tables/UsedKeysIndex.sol"; -import { UniqueEntity } from "./modules/uniqueentity/tables/UniqueEntity.sol"; -import { CallboundDelegations } from "./modules/std-delegations/tables/CallboundDelegations.sol"; -import { SystemboundDelegations } from "./modules/std-delegations/tables/SystemboundDelegations.sol"; -import { TimeboundDelegations } from "./modules/std-delegations/tables/TimeboundDelegations.sol"; -import { PuppetRegistry } from "./modules/puppet/tables/PuppetRegistry.sol"; -import { Balances } from "./modules/tokens/tables/Balances.sol"; -import { ERC20Metadata, ERC20MetadataData } from "./modules/erc20-puppet/tables/ERC20Metadata.sol"; -import { Allowances } from "./modules/erc20-puppet/tables/Allowances.sol"; -import { TotalSupply } from "./modules/erc20-puppet/tables/TotalSupply.sol"; -import { ERC20Registry } from "./modules/erc20-puppet/tables/ERC20Registry.sol"; -import { ERC721Metadata, ERC721MetadataData } from "./modules/erc721-puppet/tables/ERC721Metadata.sol"; -import { TokenURI } from "./modules/erc721-puppet/tables/TokenURI.sol"; -import { Owners } from "./modules/erc721-puppet/tables/Owners.sol"; -import { TokenApproval } from "./modules/erc721-puppet/tables/TokenApproval.sol"; -import { OperatorApproval } from "./modules/erc721-puppet/tables/OperatorApproval.sol"; -import { ERC721Registry } from "./modules/erc721-puppet/tables/ERC721Registry.sol"; -import { CallWithSignatureNonces } from "./modules/callwithsignature/tables/CallWithSignatureNonces.sol"; diff --git a/packages/world-modules/src/interfaces/IBaseWorld.sol b/packages/world-modules/src/interfaces/IBaseWorld.sol deleted file mode 100644 index c71f2becc9..0000000000 --- a/packages/world-modules/src/interfaces/IBaseWorld.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -import { IStore } from "@latticexyz/store/src/IStore.sol"; -import { IWorldKernel } from "@latticexyz/world/src/IWorldKernel.sol"; - -/** - * @title IBaseWorld - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @notice This interface integrates all systems and associated function selectors - * that are dynamically registered in the World during deployment. - * @dev This is an autogenerated file; do not edit manually. - */ -interface IBaseWorld is IStore, IWorldKernel {} diff --git a/packages/world-modules/src/interfaces/IERC20System.sol b/packages/world-modules/src/interfaces/IERC20System.sol deleted file mode 100644 index 18a6bd63a7..0000000000 --- a/packages/world-modules/src/interfaces/IERC20System.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -/** - * @title IERC20System - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. - */ -interface IERC20System { - function name() external view returns (string memory); - - function symbol() external view returns (string memory); - - function decimals() external view returns (uint8); - - function totalSupply() external view returns (uint256); - - function balanceOf(address account) external view returns (uint256); - - function allowance(address owner, address spender) external view returns (uint256); - - function transfer(address to, uint256 value) external returns (bool); - - function approve(address spender, uint256 value) external returns (bool); - - function transferFrom(address from, address to, uint256 value) external returns (bool); - - function mint(address account, uint256 value) external; - - function burn(address account, uint256 value) external; -} diff --git a/packages/world-modules/src/interfaces/IERC721System.sol b/packages/world-modules/src/interfaces/IERC721System.sol deleted file mode 100644 index 58a9e86b41..0000000000 --- a/packages/world-modules/src/interfaces/IERC721System.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -/** - * @title IERC721System - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. - */ -interface IERC721System { - function balanceOf(address owner) external view returns (uint256); - - function ownerOf(uint256 tokenId) external view returns (address); - - function name() external view returns (string memory); - - function symbol() external view returns (string memory); - - function tokenURI(uint256 tokenId) external view returns (string memory); - - function approve(address to, uint256 tokenId) external; - - function getApproved(uint256 tokenId) external view returns (address); - - function setApprovalForAll(address operator, bool approved) external; - - function isApprovedForAll(address owner, address operator) external view returns (bool); - - function transferFrom(address from, address to, uint256 tokenId) external; - - function safeTransferFrom(address from, address to, uint256 tokenId) external; - - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) external; - - function mint(address to, uint256 tokenId) external; - - function safeMint(address to, uint256 tokenId) external; - - function safeMint(address to, uint256 tokenId, bytes memory data) external; - - function burn(uint256 tokenId) external; -} diff --git a/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol b/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol deleted file mode 100644 index 773b02e31b..0000000000 --- a/packages/world-modules/src/interfaces/IPuppetFactorySystem.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; - -/** - * @title IPuppetFactorySystem - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. - */ -interface IPuppetFactorySystem { - function createPuppet(ResourceId systemId) external returns (address puppet); -} diff --git a/packages/world-modules/src/interfaces/IUniqueEntitySystem.sol b/packages/world-modules/src/interfaces/IUniqueEntitySystem.sol deleted file mode 100644 index 91c66c9208..0000000000 --- a/packages/world-modules/src/interfaces/IUniqueEntitySystem.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -/** - * @title IUniqueEntitySystem - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. - */ -interface IUniqueEntitySystem { - function getUniqueEntity() external returns (bytes32); -} diff --git a/packages/world-modules/src/interfaces/IUnstable_CallWithSignatureSystem.sol b/packages/world-modules/src/interfaces/IUnstable_CallWithSignatureSystem.sol deleted file mode 100644 index 035eb5306a..0000000000 --- a/packages/world-modules/src/interfaces/IUnstable_CallWithSignatureSystem.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; - -/** - * @title IUnstable_CallWithSignatureSystem - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. - */ -interface IUnstable_CallWithSignatureSystem { - function callWithSignature( - address signer, - ResourceId systemId, - bytes memory callData, - bytes memory signature - ) external payable returns (bytes memory); -} diff --git a/packages/world-modules/ts/scripts/tablegen.ts b/packages/world-modules/ts/scripts/tablegen.ts deleted file mode 100644 index f97cc26648..0000000000 --- a/packages/world-modules/ts/scripts/tablegen.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { loadConfig, resolveConfigPath } from "@latticexyz/config/node"; -import { getRemappings } from "@latticexyz/common/foundry"; -import { Store as StoreConfig } from "@latticexyz/store"; -import { tablegen } from "@latticexyz/store/codegen"; -import path from "node:path"; - -const configPath = await resolveConfigPath(); -const config = (await loadConfig(configPath)) as StoreConfig; -const remappings = await getRemappings(); - -await tablegen({ rootDir: path.dirname(configPath), config, remappings }); diff --git a/packages/world-modules/ts/scripts/worldgen.ts b/packages/world-modules/ts/scripts/worldgen.ts deleted file mode 100644 index 16852d9730..0000000000 --- a/packages/world-modules/ts/scripts/worldgen.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { globSync } from "glob"; -import path, { basename } from "path"; -import { rmSync } from "fs"; -import { loadConfig } from "@latticexyz/config/node"; -import { getSrcDirectory } from "@latticexyz/common/foundry"; -import { World as WorldConfig } from "@latticexyz/world"; -import { worldgen } from "@latticexyz/world/node"; - -// TODO dedupe this and cli's worldgen command -const configPath = undefined; -const clean = false; -const srcDir = await getSrcDirectory(); - -// Get a list of all contract names -const existingContracts = globSync(`${srcDir}/**/*.sol`) - .sort() - .map((path) => ({ - path, - basename: basename(path, ".sol"), - })); - -// Load and resolve the config -const mudConfig = (await loadConfig(configPath)) as WorldConfig; - -const outputBaseDirectory = path.join(srcDir, mudConfig.codegen.outputDirectory); - -// clear the worldgen directory -if (clean) { - rmSync(path.join(outputBaseDirectory, mudConfig.codegen.worldgenDirectory), { recursive: true, force: true }); -} - -// generate new interfaces -await worldgen(mudConfig, existingContracts, outputBaseDirectory); diff --git a/packages/world/ts/config/resolveWorldConfig.ts b/packages/world/ts/config/resolveWorldConfig.ts deleted file mode 100644 index 7dd9766464..0000000000 --- a/packages/world/ts/config/resolveWorldConfig.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { UnrecognizedSystemErrorFactory } from "@latticexyz/config/library"; -import { StoreConfig } from "@latticexyz/store/internal"; -import { SystemConfig, WorldConfig } from "./types"; - -export type ResolvedSystemConfig = ReturnType; - -export type ResolvedWorldConfig = ReturnType; - -/** - * Resolves the world config by combining the default and overridden system configs, - * filtering out excluded systems, validate system names refer to existing contracts, and - * splitting the access list into addresses and system names. - */ -export function resolveWorldConfig( - config: Pick, - existingContracts?: string[], -) { - // Include contract names ending in "System", but not the base "System" contract, and not Interfaces - const defaultSystemNames = - existingContracts?.filter((name) => name.endsWith("System") && name !== "System" && !name.match(/^I[A-Z]/)) ?? []; - const overriddenSystemNames = Object.keys(config.systems); - - // Validate every key in systems refers to an existing system contract (and is not called "World") - if (existingContracts) { - for (const systemName of overriddenSystemNames) { - if (!existingContracts.includes(systemName) || systemName === "World") { - throw UnrecognizedSystemErrorFactory(["systems", systemName], systemName); - } - } - } - - // Combine the default and overridden system names and filter out excluded systems - const systemNames = [...new Set([...defaultSystemNames, ...overriddenSystemNames])].filter( - (name) => !config.excludeSystems.includes(name), - ); - - // Resolve the config - const resolvedSystems: Record = systemNames.reduce((acc, systemName) => { - return { - ...acc, - [systemName]: resolveSystemConfig(systemName, config.systems[systemName], existingContracts), - }; - }, {}); - - return { systems: resolvedSystems }; -} - -/** - * Resolves the system config by combining the default and overridden system configs, - * @param systemName name of the system - * @param config optional SystemConfig object, if none is provided the default config is used - * @param existingContracts optional list of existing contract names, used to validate system names in the access list. If not provided, no validation is performed. - * @returns ResolvedSystemConfig object - * Default value for name is `systemName` - * Default value for registerFunctionSelectors is true - * Default value for openAccess is true - * Default value for accessListAddresses is [] - * Default value for accessListSystems is [] - */ -export function resolveSystemConfig(systemName: string, config?: SystemConfig, existingContracts?: string[]) { - const name = config?.name ?? systemName; - const registerFunctionSelectors = config?.registerFunctionSelectors ?? true; - const openAccess = config?.openAccess ?? true; - const accessListAddresses: string[] = []; - const accessListSystems: string[] = []; - const accessList = config && !config.openAccess ? config.accessList : []; - - // Split the access list into addresses and system names - for (const accessListItem of accessList) { - if (accessListItem.startsWith("0x")) { - accessListAddresses.push(accessListItem); - } else { - // Validate every system refers to an existing system contract - if (existingContracts && !existingContracts.includes(accessListItem)) { - throw UnrecognizedSystemErrorFactory(["systems", systemName, "accessList"], accessListItem); - } - accessListSystems.push(accessListItem); - } - } - - return { name, registerFunctionSelectors, openAccess, accessListAddresses, accessListSystems }; -} diff --git a/packages/world/ts/config/v2/input.ts b/packages/world/ts/config/v2/input.ts index 2967703ded..df9c97f1e1 100644 --- a/packages/world/ts/config/v2/input.ts +++ b/packages/world/ts/config/v2/input.ts @@ -90,9 +90,9 @@ export type WorldInput = show< * The value is a SystemConfig object. */ systems?: SystemsInput; - /** System names to exclude from automatic deployment */ + /** System names to exclude from codegen and deployment */ excludeSystems?: readonly string[]; - /** Modules to in the World */ + /** Modules to install in the World */ modules?: readonly ModuleInput[]; /** Deploy config */ deploy?: DeployInput; diff --git a/packages/world/ts/exports/internal.ts b/packages/world/ts/exports/internal.ts index 64819f4a4d..c2bb98bcd6 100644 --- a/packages/world/ts/exports/internal.ts +++ b/packages/world/ts/exports/internal.ts @@ -1,5 +1,4 @@ export * from "../config/defaults"; -export * from "../config/resolveWorldConfig"; export * from "../config/types"; export * from "../config/worldConfig"; diff --git a/packages/world/ts/node/findSolidityFiles.ts b/packages/world/ts/node/findSolidityFiles.ts index 6e61125fbd..3d37a77360 100644 --- a/packages/world/ts/node/findSolidityFiles.ts +++ b/packages/world/ts/node/findSolidityFiles.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { glob } from "glob"; import { World } from "../config/v2/output"; +// TODO: move to common codegen? export async function findSolidityFiles({ rootDir, config }: { rootDir: string; config: World }) { const files = await glob(path.join(config.sourceDirectory, "**", "*.sol"), { cwd: rootDir, diff --git a/packages/world/ts/node/getSystemContracts.ts b/packages/world/ts/node/getSystemContracts.ts new file mode 100644 index 0000000000..93c0aaa81f --- /dev/null +++ b/packages/world/ts/node/getSystemContracts.ts @@ -0,0 +1,33 @@ +import { World } from "../config/v2/output"; +import { findSolidityFiles } from "./findSolidityFiles"; + +export type SolidityContract = { + readonly sourcePath: string; + readonly name: string; +}; + +export type GetSystemContractsOptions = { + readonly rootDir: string; + readonly config: World; +}; + +export async function getSystemContracts({ + rootDir, + config, +}: GetSystemContractsOptions): Promise { + const solidityFiles = await findSolidityFiles({ rootDir, config }); + + return solidityFiles + .map((file) => ({ + sourcePath: file.filename, + name: file.basename, + })) + .filter( + (file) => + file.name.endsWith("System") && + // exclude the base System contract + file.name !== "System" && + // exclude interfaces + !/^I[A-Z]/.test(file.name), + ); +} diff --git a/packages/world/ts/node/index.ts b/packages/world/ts/node/index.ts index 3fea3930b7..e73f48effc 100644 --- a/packages/world/ts/node/index.ts +++ b/packages/world/ts/node/index.ts @@ -1 +1,4 @@ export * from "./render-solidity"; +export * from "./findSolidityFiles"; +export * from "./getSystemContracts"; +export * from "./resolveSystems"; diff --git a/packages/world/ts/node/render-solidity/worldgen.ts b/packages/world/ts/node/render-solidity/worldgen.ts index 4f9c096486..a2df30ae66 100644 --- a/packages/world/ts/node/render-solidity/worldgen.ts +++ b/packages/world/ts/node/render-solidity/worldgen.ts @@ -1,38 +1,40 @@ -import { readFileSync } from "fs"; +import fs from "fs"; import path from "path"; import { formatAndWriteSolidity, contractToInterface, type RelativeImportDatum } from "@latticexyz/common/codegen"; import { renderSystemInterface } from "./renderSystemInterface"; import { renderWorldInterface } from "./renderWorldInterface"; -import { resolveWorldConfig } from "../../config/resolveWorldConfig"; import { World as WorldConfig } from "../../config/v2/output"; -import { worldToV1 } from "../../config/v2/compat"; +import { resolveSystems } from "../resolveSystems"; -export async function worldgen( - configV2: WorldConfig, - existingContracts: { path: string; basename: string }[], - outputBaseDirectory: string, -) { - const config = worldToV1(configV2); - const resolvedConfig = resolveWorldConfig( - config, - existingContracts.map(({ basename }) => basename), - ); +export async function worldgen({ + rootDir, + config, + clean = true, +}: { + rootDir: string; + config: WorldConfig; + clean?: boolean; +}) { + const outDir = path.join(config.sourceDirectory, config.codegen.outputDirectory, config.codegen.worldgenDirectory); - const worldgenBaseDirectory = path.join(outputBaseDirectory, config.worldgenDirectory); - const systems = existingContracts.filter(({ basename }) => Object.keys(resolvedConfig.systems).includes(basename)); + if (clean) { + fs.rmSync(outDir, { recursive: true, force: true }); + } + + const systems = await resolveSystems({ rootDir, config }); const systemInterfaceImports: RelativeImportDatum[] = []; for (const system of systems) { - const data = readFileSync(system.path, "utf8"); + const data = fs.readFileSync(system.sourcePath, "utf8"); // get external funcions from a contract - const { functions, errors, symbolImports } = contractToInterface(data, system.basename); + const { functions, errors, symbolImports } = contractToInterface(data, system.label); const imports = symbolImports.map((symbolImport) => { if (symbolImport.path[0] === ".") { // relative import return { symbol: symbolImport.symbol, - fromPath: path.join(path.dirname(system.path), symbolImport.path), - usedInPath: worldgenBaseDirectory, + fromPath: path.join(path.dirname(system.sourcePath), symbolImport.path), + usedInPath: outDir, }; } else { // absolute import @@ -42,7 +44,7 @@ export async function worldgen( }; } }); - const systemInterfaceName = `I${system.basename}`; + const systemInterfaceName = `I${system.label}`; const output = renderSystemInterface({ name: systemInterfaceName, functionPrefix: config.namespace === "" ? "" : `${config.namespace}__`, @@ -51,7 +53,7 @@ export async function worldgen( imports, }); // write to file - const fullOutputPath = path.join(worldgenBaseDirectory, systemInterfaceName + ".sol"); + const fullOutputPath = path.join(rootDir, outDir, systemInterfaceName + ".sol"); await formatAndWriteSolidity(output, fullOutputPath, "Generated system interface"); // prepare imports for IWorld @@ -64,12 +66,12 @@ export async function worldgen( // render IWorld const output = renderWorldInterface({ - interfaceName: config.worldInterfaceName, + interfaceName: config.codegen.worldInterfaceName, imports: systemInterfaceImports, - storeImportPath: configV2.codegen.storeImportPath, - worldImportPath: config.worldImportPath, + storeImportPath: config.codegen.storeImportPath, + worldImportPath: config.codegen.worldImportPath, }); // write to file - const fullOutputPath = path.join(worldgenBaseDirectory, config.worldInterfaceName + ".sol"); + const fullOutputPath = path.join(rootDir, outDir, config.codegen.worldInterfaceName + ".sol"); await formatAndWriteSolidity(output, fullOutputPath, "Generated world interface"); } diff --git a/packages/world/ts/node/resolveSystems.ts b/packages/world/ts/node/resolveSystems.ts new file mode 100644 index 0000000000..1d22ee3e16 --- /dev/null +++ b/packages/world/ts/node/resolveSystems.ts @@ -0,0 +1,55 @@ +import { isHex } from "viem"; +import { getSystemContracts } from "."; +import { System, World } from "../config/v2"; +import { SYSTEM_DEFAULTS } from "../config/v2/defaults"; + +export type ResolvedSystem = System & { + // TODO: move label into System config output + readonly label: string; + readonly sourcePath: string; +}; + +export async function resolveSystems({ + rootDir, + config, +}: { + rootDir: string; + config: World; +}): Promise { + const systemContracts = await getSystemContracts({ rootDir, config }); + const contractNames = systemContracts.map((contract) => contract.name); + + // validate every system in config refers to an existing system contract + const missingSystems = Object.keys(config.systems).filter((systemLabel) => !contractNames.includes(systemLabel)); + if (missingSystems.length > 0) { + throw new Error(`Found systems in config with no corresponding system contract: ${missingSystems.join(", ")}`); + } + + const systems = systemContracts + .map((contract): ResolvedSystem => { + // TODO: replace name with label + const systemConfig = config.systems[contract.name] ?? { ...SYSTEM_DEFAULTS, name: contract.name }; + return { + ...systemConfig, + label: contract.name, + sourcePath: contract.sourcePath, + }; + }) + // TODO: replace `excludeSystems` with `deploy.disabled` or `codegen.disabled` + .filter((system) => !config.excludeSystems.includes(system.label)); + + const systemLabels = systems.map((system) => system.label); + + // validate every system has a valid access list + for (const system of systems) { + for (const accessListItem of system.accessList) { + if (isHex(accessListItem)) continue; + if (systemLabels.includes(accessListItem)) continue; + throw new Error( + `Access list item (${accessListItem}) for system (${system.label}) had no matching system contract.`, + ); + } + } + + return systems; +} diff --git a/packages/world/ts/scripts/build.ts b/packages/world/ts/scripts/build.ts index 6d9401776e..8a44243225 100644 --- a/packages/world/ts/scripts/build.ts +++ b/packages/world/ts/scripts/build.ts @@ -2,7 +2,6 @@ import path from "node:path"; import { loadConfig, resolveConfigPath } from "@latticexyz/config/node"; import { getRemappings } from "@latticexyz/common/foundry"; import { tablegen } from "@latticexyz/store/codegen"; -import { findSolidityFiles } from "../node/findSolidityFiles"; import { World } from "../config/v2"; import { worldgen } from "../node"; @@ -18,24 +17,14 @@ const rootDir = path.dirname(configPath); const config = (await loadConfig(configPath)) as World; const remappings = await getRemappings(); -// TODO: move this into worldgen -const existingContracts = (await findSolidityFiles({ rootDir, config })).map((file) => ({ - path: file.filename, - basename: file.basename, -})); -const codegenDirectory = path.join(config.sourceDirectory, config.codegen.outputDirectory); - -// TODO: clean - await Promise.all([ tablegen({ rootDir, config, remappings }), - worldgen( - { + worldgen({ + rootDir, + config: { ...config, // override the namespace to be the root namespace for generating the core system interface namespace: "", }, - existingContracts, - codegenDirectory, - ), + }), ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38b03accc0..c35e9cf157 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1109,6 +1109,9 @@ importers: '@latticexyz/abi-ts': specifier: workspace:* version: link:../abi-ts + '@latticexyz/cli': + specifier: workspace:* + version: link:../cli '@latticexyz/gas-report': specifier: workspace:* version: link:../gas-report diff --git a/test/mock-game-contracts/src/codegen/common.sol b/test/mock-game-contracts/src/codegen/common.sol index 2c1249cfb8..96d1607fc5 100644 --- a/test/mock-game-contracts/src/codegen/common.sol +++ b/test/mock-game-contracts/src/codegen/common.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.24; /* Autogenerated file. Do not edit manually. */ + enum TerrainType { None, Ocean,