diff --git a/.changeset/chatty-frogs-smash.md b/.changeset/chatty-frogs-smash.md new file mode 100644 index 0000000000..be634b4108 --- /dev/null +++ b/.changeset/chatty-frogs-smash.md @@ -0,0 +1,22 @@ +--- +"@latticexyz/cli": patch +"@latticexyz/world": patch +--- + +Worlds can now be deployed with external modules, defined by a module's `artifactPath` in your MUD config, resolved with Node's module resolution. This allows for modules to be published to and imported from npm. + +```diff + defineWorld({ + // … + modules: [ + { +- name: "KeysWithValueModule", ++ artifactPath: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json", + root: true, + args: [resolveTableId("Inventory")], + }, + ], + }); +``` + +Note that the above assumes `@latticexyz/world-modules` is included as a dependency of your project. diff --git a/e2e/packages/contracts/mud.config.ts b/e2e/packages/contracts/mud.config.ts index 12b3b6fd5d..14c972dd50 100644 --- a/e2e/packages/contracts/mud.config.ts +++ b/e2e/packages/contracts/mud.config.ts @@ -60,9 +60,9 @@ export default defineWorld({ }, modules: [ { - name: "Unstable_CallWithSignatureModule", + artifactPath: + "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json", root: true, - args: [], }, ], }); diff --git a/examples/minimal/packages/contracts/mud.config.ts b/examples/minimal/packages/contracts/mud.config.ts index 68b53a991a..60325fca87 100644 --- a/examples/minimal/packages/contracts/mud.config.ts +++ b/examples/minimal/packages/contracts/mud.config.ts @@ -39,7 +39,7 @@ export default defineWorld({ }, modules: [ { - name: "KeysWithValueModule", + artifactPath: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json", root: true, args: [resolveTableId("Inventory")], }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 6adf8c39ab..a331159cf1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,6 +46,7 @@ "@latticexyz/utils": "workspace:*", "@latticexyz/world": "workspace:*", "@latticexyz/world-modules": "workspace:*", + "abitype": "1.0.0", "asn1.js": "^5.4.1", "chalk": "^5.0.1", "chokidar": "^3.5.3", diff --git a/packages/cli/src/commands/verify.ts b/packages/cli/src/commands/verify.ts index 3f46ea5989..ba5935ef91 100644 --- a/packages/cli/src/commands/verify.ts +++ b/packages/cli/src/commands/verify.ts @@ -7,9 +7,9 @@ import { worldToV1 } from "@latticexyz/world/config/v2"; import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry"; import { getExistingContracts } from "../utils/getExistingContracts"; import { getContractData } from "../utils/getContractData"; -import { defaultModuleContracts } from "../utils/defaultModuleContracts"; import { Hex, createWalletClient, http } from "viem"; import chalk from "chalk"; +import { configToModules } from "../deploy/configToModules"; const verifyOptions = { deployerAddress: { @@ -82,17 +82,7 @@ const commandModule: CommandModule = { }; }); - // Get modules - const modules = config.modules.map((mod) => { - const contractData = - defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ?? - getContractData(`${mod.name}.sol`, mod.name, outDir); - - return { - name: mod.name, - bytecode: contractData.bytecode, - }; - }); + const modules = await configToModules(configV2, outDir); await verify({ client, diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index da4a2f29c1..89d201ec15 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -117,6 +117,5 @@ export type ConfigInput = StoreConfig & WorldConfig; export type Config = { readonly tables: Tables; readonly systems: readonly System[]; - readonly modules: readonly Module[]; readonly libraries: readonly Library[]; }; diff --git a/packages/cli/src/deploy/configToModules.ts b/packages/cli/src/deploy/configToModules.ts new file mode 100644 index 0000000000..5a01091cdf --- /dev/null +++ b/packages/cli/src/deploy/configToModules.ts @@ -0,0 +1,81 @@ +import path from "node:path"; +import { Module } from "./common"; +import { resolveWithContext } from "@latticexyz/config/library"; +import { encodeField } from "@latticexyz/protocol-parser/internal"; +import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type/internal"; +import { bytesToHex, hexToBytes } from "viem"; +import { createPrepareDeploy } from "./createPrepareDeploy"; +import { World } from "@latticexyz/world"; +import { getContractArtifact } from "../utils/getContractArtifact"; +import { knownModuleArtifacts } from "../utils/knownModuleArtifacts"; + +export async function configToModules( + config: config, + forgeOutDir: string, +): Promise { + // this expects a namespaced table name when used with `resolveTableId` + const resolveContext = { + tableIds: Object.fromEntries( + Object.entries(config.tables).map(([tableName, table]) => [tableName, hexToBytes(table.tableId)]), + ), + }; + + const modules = await Promise.all( + config.modules.map(async (mod): Promise => { + let artifactPath = mod.artifactPath; + + // Backwards compatibility + // TODO: move this up a level so we don't need `forgeOutDir` in here? + if (!artifactPath) { + if (mod.name) { + artifactPath = + knownModuleArtifacts[mod.name as keyof typeof knownModuleArtifacts] ?? + path.join(forgeOutDir, `${mod.name}.sol`, `${mod.name}.json`); + console.warn( + [ + "", + `⚠️ Your \`mud.config.ts\` is using a module with a \`name\`, but this option is deprecated.`, + "", + "To resolve this, you can replace this:", + "", + ` name: ${JSON.stringify(mod.name)}`, + "", + "with this:", + "", + ` artifactPath: ${JSON.stringify(artifactPath)}`, + "", + ].join("\n"), + ); + } else { + throw new Error("No `artifactPath` provided for module."); + } + } + + const name = path.basename(artifactPath, ".json"); + const artifact = await getContractArtifact({ artifactPath }); + + // TODO: replace args with something more strongly typed + const installArgs = mod.args + .map((arg) => resolveWithContext(arg, resolveContext)) + .map((arg) => { + const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value; + return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType); + }); + + if (installArgs.length > 1) { + throw new Error(`${name} module should only have 0-1 args, but had ${installArgs.length} args.`); + } + + return { + name, + installAsRoot: mod.root, + installData: installArgs.length === 0 ? "0x" : installArgs[0], + prepareDeploy: createPrepareDeploy(artifact.bytecode, artifact.placeholders), + deployedBytecodeSize: artifact.deployedBytecodeSize, + abi: artifact.abi, + }; + }), + ); + + return modules; +} diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index c7c95f322f..e407a44116 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -2,7 +2,7 @@ import { Account, Address, Chain, Client, Hex, Transport } from "viem"; import { ensureDeployer } from "./ensureDeployer"; import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; -import { Config, ConfigInput, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; +import { Config, ConfigInput, Module, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; import { ensureSystems } from "./ensureSystems"; import { waitForTransactionReceipt } from "viem/actions"; import { getWorldDeploy } from "./getWorldDeploy"; @@ -19,6 +19,7 @@ import { ensureWorldFactory } from "./ensureWorldFactory"; type DeployOptions = { client: Client; config: Config; + modules?: readonly Module[]; salt?: Hex; worldAddress?: Address; /** @@ -40,6 +41,7 @@ type DeployOptions = { export async function deploy({ client, config, + modules = [], salt, worldAddress: existingWorldAddress, deployerAddress: initialDeployerAddress, @@ -66,7 +68,7 @@ export async function deploy({ deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceToLabel(system)} system`, })), - ...config.modules.map((mod) => ({ + ...modules.map((mod) => ({ bytecode: mod.prepareDeploy(deployerAddress, config.libraries).bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, @@ -118,7 +120,7 @@ export async function deploy({ deployerAddress, libraries: config.libraries, worldDeploy, - modules: config.modules, + modules, }); const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs]; diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 31d689d489..4af03fe9b3 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -38,12 +38,13 @@ export async function ensureModules({ pRetry( async () => { try { + const abi = [...worldAbi, ...mod.abi]; const moduleAddress = mod.prepareDeploy(deployerAddress, libraries).address; return mod.installAsRoot ? await writeContract(client, { chain: client.chain ?? null, address: worldDeploy.address, - abi: worldAbi, + abi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "installRootModule", args: [moduleAddress, mod.installData], @@ -51,7 +52,7 @@ export async function ensureModules({ : await writeContract(client, { chain: client.chain ?? null, address: worldDeploy.address, - abi: worldAbi, + abi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "installModule", args: [moduleAddress, mod.installData], diff --git a/packages/cli/src/deploy/resolveConfig.ts b/packages/cli/src/deploy/resolveConfig.ts index 85d6b2938d..0a4466f7dc 100644 --- a/packages/cli/src/deploy/resolveConfig.ts +++ b/packages/cli/src/deploy/resolveConfig.ts @@ -1,13 +1,9 @@ import path from "path"; import { resolveWorldConfig } from "@latticexyz/world/internal"; -import { Config, ConfigInput, Library, Module, System, WorldFunction } from "./common"; +import { Config, ConfigInput, Library, System, WorldFunction } from "./common"; import { resourceToHex } from "@latticexyz/common"; -import { resolveWithContext } from "@latticexyz/config/library"; -import { encodeField } from "@latticexyz/protocol-parser/internal"; -import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type/internal"; -import { Hex, hexToBytes, bytesToHex, toFunctionSelector, toFunctionSignature } from "viem"; +import { Hex, toFunctionSelector, toFunctionSignature } from "viem"; import { getExistingContracts } from "../utils/getExistingContracts"; -import { defaultModuleContracts } from "../utils/defaultModuleContracts"; import { getContractData } from "../utils/getContractData"; import { configToTables } from "./configToTables"; import { groupBy } from "@latticexyz/common/utils"; @@ -100,49 +96,9 @@ export function resolveConfig({ ); } - // ugh (https://github.com/latticexyz/mud/issues/1668) - const resolveContext = { - tableIds: Object.fromEntries( - Object.entries(config.tables).map(([tableName, table]) => [ - tableName, - hexToBytes( - resourceToHex({ - type: table.offchainOnly ? "offchainTable" : "table", - namespace: config.namespace, - name: table.name, - }), - ), - ]), - ), - }; - - const modules = config.modules.map((mod): Module => { - const contractData = - defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ?? - getContractData(`${mod.name}.sol`, mod.name, forgeOutDir); - const installArgs = mod.args - .map((arg) => resolveWithContext(arg, resolveContext)) - .map((arg) => { - const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value; - return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType); - }); - if (installArgs.length > 1) { - throw new Error(`${mod.name} module should only have 0-1 args, but had ${installArgs.length} args.`); - } - return { - name: mod.name, - installAsRoot: mod.root, - installData: installArgs.length === 0 ? "0x" : installArgs[0], - prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), - deployedBytecodeSize: contractData.deployedBytecodeSize, - abi: contractData.abi, - }; - }); - return { tables, systems, - modules, libraries, }; } diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts index e0920733f3..e9619fd6a3 100644 --- a/packages/cli/src/runDeploy.ts +++ b/packages/cli/src/runDeploy.ts @@ -16,6 +16,7 @@ import { postDeploy } from "./utils/postDeploy"; import { WorldDeploy } from "./deploy/common"; import { build } from "./build"; import { kmsKeyToAccount } from "@latticexyz/common/kms"; +import { configToModules } from "./deploy/configToModules"; export const deployOptions = { configPath: { type: "string", desc: "Path to the MUD config file" }, @@ -85,6 +86,7 @@ export async function runDeploy(opts: DeployOptions): Promise { } const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir }); + const modules = await configToModules(configV2, outDir); const account = await (async () => { if (opts.kms) { @@ -131,6 +133,7 @@ export async function runDeploy(opts: DeployOptions): Promise { worldAddress: opts.worldAddress as Hex | undefined, client, config: resolvedConfig, + modules, withWorldProxy: configV2.deploy.upgradeableWorldImplementation, }); if (opts.worldAddress == null || opts.alwaysRunPostDeploy) { diff --git a/packages/cli/src/utils/defaultModuleContracts.ts b/packages/cli/src/utils/defaultModuleContracts.ts deleted file mode 100644 index 9356a51b5b..0000000000 --- a/packages/cli/src/utils/defaultModuleContracts.ts +++ /dev/null @@ -1,39 +0,0 @@ -import KeysWithValueModuleData from "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json" assert { type: "json" }; -import KeysInTableModuleData from "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" }; -import UniqueEntityModuleData from "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" }; -// eslint-disable-next-line max-len -import Unstable_CallWithSignatureModuleData from "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json" assert { type: "json" }; -import { Abi, Hex, size } from "viem"; -import { findPlaceholders } from "./findPlaceholders"; - -// These modules are always deployed -export const defaultModuleContracts = [ - { - name: "KeysWithValueModule", - abi: KeysWithValueModuleData.abi as Abi, - bytecode: KeysWithValueModuleData.bytecode.object as Hex, - placeholders: findPlaceholders(KeysWithValueModuleData.bytecode.linkReferences), - deployedBytecodeSize: size(KeysWithValueModuleData.deployedBytecode.object as Hex), - }, - { - name: "KeysInTableModule", - abi: KeysInTableModuleData.abi as Abi, - bytecode: KeysInTableModuleData.bytecode.object as Hex, - placeholders: findPlaceholders(KeysInTableModuleData.bytecode.linkReferences), - deployedBytecodeSize: size(KeysInTableModuleData.deployedBytecode.object as Hex), - }, - { - name: "UniqueEntityModule", - abi: UniqueEntityModuleData.abi as Abi, - bytecode: UniqueEntityModuleData.bytecode.object as Hex, - placeholders: findPlaceholders(UniqueEntityModuleData.bytecode.linkReferences), - deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex), - }, - { - name: "Unstable_CallWithSignatureModule", - abi: Unstable_CallWithSignatureModuleData.abi as Abi, - bytecode: Unstable_CallWithSignatureModuleData.bytecode.object as Hex, - placeholders: findPlaceholders(Unstable_CallWithSignatureModuleData.bytecode.linkReferences), - deployedBytecodeSize: size(Unstable_CallWithSignatureModuleData.deployedBytecode.object as Hex), - }, -]; diff --git a/packages/cli/src/utils/getContractArtifact.ts b/packages/cli/src/utils/getContractArtifact.ts new file mode 100644 index 0000000000..0ddd566e25 --- /dev/null +++ b/packages/cli/src/utils/getContractArtifact.ts @@ -0,0 +1,73 @@ +import { Abi, Hex, isHex, size } from "viem"; +import { LibraryPlaceholder } from "../deploy/common"; +import { findPlaceholders } from "./findPlaceholders"; +import { z } from "zod"; +import { Abi as abiSchema } from "abitype/zod"; + +export type GetContractArtifactOptions = { + /** + * Import path to contract's forge/solc JSON artifact with the contract's compiled bytecode. + * + * This path is resolved using node's contract resolution, so this supports both relative file paths (`../path/to/MyModule.json`) as well as JS import paths + * (`@latticexyz/world-contracts/out/CallWithSignatureModule.sol/CallWithSignatureModule.json`). + */ + artifactPath: string; +}; + +export type GetContractArtifactResult = { + bytecode: Hex; + placeholders: readonly LibraryPlaceholder[]; + abi: Abi; + deployedBytecodeSize: number; +}; + +const bytecodeSchema = z.object({ + object: z.string().refine(isHex), + linkReferences: z.record( + z.record( + z.array( + z.object({ + start: z.number(), + length: z.number(), + }), + ), + ), + ), +}); + +const artifactSchema = z.object({ + bytecode: bytecodeSchema, + deployedBytecode: bytecodeSchema, + abi: abiSchema, +}); + +export async function getContractArtifact({ + artifactPath, +}: GetContractArtifactOptions): Promise { + let importedArtifact; + try { + importedArtifact = ( + await import(artifactPath, { + with: { type: "json" }, + // `with` is the new approach, but `assert` is kept for backwards-compatibility with Node 18 + assert: { type: "json" }, + }) + ).default; + } catch (error) { + console.error(); + console.error("Could not import contract artifact at", artifactPath); + console.error(); + throw error; + } + + // TODO: improve errors or replace with arktype? + const artifact = artifactSchema.parse(importedArtifact); + const placeholders = findPlaceholders(artifact.bytecode.linkReferences); + + return { + abi: artifact.abi, + bytecode: artifact.bytecode.object, + placeholders, + deployedBytecodeSize: size(artifact.deployedBytecode.object), + }; +} diff --git a/packages/cli/src/utils/knownModuleArtifacts.ts b/packages/cli/src/utils/knownModuleArtifacts.ts new file mode 100644 index 0000000000..a71c4fa2e4 --- /dev/null +++ b/packages/cli/src/utils/knownModuleArtifacts.ts @@ -0,0 +1,8 @@ +/** @deprecated Please don't add to this list! These are kept for backwards compatibility and assumes the downstream project has this module installed as a dependency. */ +export const knownModuleArtifacts = { + KeysWithValueModule: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json", + KeysInTableModule: "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json", + UniqueEntityModule: "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json", + Unstable_CallWithSignatureModule: + "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json", +}; diff --git a/packages/cli/src/verify.ts b/packages/cli/src/verify.ts index 7c18b507cd..b5acaad720 100644 --- a/packages/cli/src/verify.ts +++ b/packages/cli/src/verify.ts @@ -5,7 +5,7 @@ import PQueue from "p-queue"; import { getWorldProxyFactoryContracts } from "./deploy/getWorldProxyFactoryContracts"; import { getDeployer } from "./deploy/getDeployer"; import { MUDError } from "@latticexyz/common/errors"; -import { salt } from "./deploy/common"; +import { Module, salt } from "./deploy/common"; import { getStorageAt } from "viem/actions"; import { execa } from "execa"; @@ -15,7 +15,7 @@ type VerifyOptions = { verifier: string; verifierUrl?: string; systems: { name: string; bytecode: Hex }[]; - modules: { name: string; bytecode: Hex }[]; + modules: readonly Module[]; worldAddress: Hex; /** * Address of determinstic deployment proxy: https://github.com/Arachnid/deterministic-deployment-proxy @@ -39,7 +39,7 @@ export async function verify({ }: VerifyOptions): Promise { const deployerAddress = initialDeployerAddress ?? (await getDeployer(client)); if (!deployerAddress) { - throw new MUDError(`No deployer`); + throw new MUDError("No deployer address provided or found."); } // If the proxy implementation storage slot is set on the World, the World was deployed as a proxy. @@ -104,24 +104,22 @@ export async function verify({ ), ); - modules.map(({ name, bytecode }) => - verifyQueue.add(() => + modules.map(({ name, prepareDeploy }) => { + const { address } = prepareDeploy(deployerAddress, []); + return verifyQueue.add(() => verifyContract({ + // TODO: figure out dir from artifactPath via import.meta.resolve? cwd: "node_modules/@latticexyz/world-modules", - name: name, + name, rpc, verifier, verifierUrl, - address: getCreate2Address({ - from: deployerAddress, - bytecode: bytecode, - salt, - }), + address, }).catch((error) => { console.error(`Error verifying module contract ${name}:`, error); }), - ), - ); + ); + }); // If the world was deployed as a Proxy, verify the proxy and implementation. if (usesProxy) { diff --git a/packages/world/ts/config/v2/compat.ts b/packages/world/ts/config/v2/compat.ts index 04d7a82b9b..8083360f5d 100644 --- a/packages/world/ts/config/v2/compat.ts +++ b/packages/world/ts/config/v2/compat.ts @@ -4,7 +4,7 @@ import { Store } from "@latticexyz/store"; import { storeToV1 } from "@latticexyz/store/config/v2"; type modulesToV1 = { - [key in keyof modules]: modules[key]; + [key in keyof modules]: Omit; }; function modulesToV1(modules: modules): modulesToV1 { diff --git a/packages/world/ts/config/v2/defaults.ts b/packages/world/ts/config/v2/defaults.ts index b070a1cbc6..8e6297f552 100644 --- a/packages/world/ts/config/v2/defaults.ts +++ b/packages/world/ts/config/v2/defaults.ts @@ -9,6 +9,7 @@ export type SYSTEM_DEFAULTS = typeof SYSTEM_DEFAULTS; export const MODULE_DEFAULTS = { root: false, args: [], + artifactPath: undefined, } as const; export type MODULE_DEFAULTS = typeof MODULE_DEFAULTS; diff --git a/packages/world/ts/config/v2/input.ts b/packages/world/ts/config/v2/input.ts index 43f2f33770..6e784cabfc 100644 --- a/packages/world/ts/config/v2/input.ts +++ b/packages/world/ts/config/v2/input.ts @@ -21,10 +21,32 @@ export type SystemInput = { export type SystemsInput = { [key: string]: SystemInput }; -export type ModuleInput = { - /** The name of the module */ - readonly name: string; - /** Should this module be installed as a root module? */ +type ModuleInputArtifactPath = + | { + /** + * Import path to module's forge/solc JSON artifact with the module's compiled bytecode. This is used to create consistent, deterministic deploys for already-built modules + * like those installed and imported from npm. + * + * This path is resolved using node's module resolution, so this supports both relative file paths (`../path/to/MyModule.json`) as well as JS import paths + * (`@latticexyz/world-modules/out/CallWithSignatureModule.sol/CallWithSignatureModule.json`). + */ + readonly artifactPath: string; + readonly name?: never; + } + | { + /** + * The name of the module, used to construct the import path relative to the project directory. + * @deprecated Use `artifactPath` instead. + */ + readonly name: string; + readonly artifactPath?: never; + }; + +export type ModuleInput = ModuleInputArtifactPath & { + /** + * Should this module be installed as a root module? + * @default false + * */ readonly root?: boolean; /** Arguments to be passed to the module's install method */ // TODO: make more strongly typed by taking in tables input diff --git a/packages/world/ts/config/v2/output.ts b/packages/world/ts/config/v2/output.ts index 95c374767a..618d55002c 100644 --- a/packages/world/ts/config/v2/output.ts +++ b/packages/world/ts/config/v2/output.ts @@ -2,12 +2,25 @@ import { Store } from "@latticexyz/store"; import { DynamicResolution, ValueWithType } from "./dynamicResolution"; export type Module = { - /** The name of the module */ + /** + * The name of the module + * @deprecated + */ readonly name: string; /** Should this module be installed as a root module? */ readonly root: boolean; /** Arguments to be passed to the module's install method */ readonly args: readonly (ValueWithType | DynamicResolution)[]; + /** + * Import path to module's forge/solc JSON artifact with the module's compiled bytecode. This is used to create consistent, deterministic deploys for already-built modules + * like those installed and imported from npm. + * + * This path is resolved using node's module resolution, so this supports both relative file paths (`../path/to/MyModule.json`) as well as JS import paths + * (`@latticexyz/world-modules/out/CallWithSignatureModule.sol/CallWithSignatureModule.json`). + * + * If not provided, it's assumed that this is a local module as part of the project's source and the artifact will be looked up in forge's output directory. + */ + readonly artifactPath: string | undefined; }; export type System = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7f0709406..b3a13810f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,6 +178,9 @@ importers: '@latticexyz/world-modules': specifier: workspace:* version: link:../world-modules + abitype: + specifier: 1.0.0 + version: 1.0.0(typescript@5.4.2)(zod@3.23.7) asn1.js: specifier: ^5.4.1 version: 5.4.1 @@ -1191,6 +1194,9 @@ importers: '@latticexyz/world': specifier: workspace:* version: link:../../packages/world + '@latticexyz/world-modules': + specifier: workspace:* + version: link:../../packages/world-modules dotenv: specifier: ^16.0.3 version: 16.0.3 diff --git a/test/mock-game-contracts/mud.config.ts b/test/mock-game-contracts/mud.config.ts index 064ffbeea7..4421913cd8 100644 --- a/test/mock-game-contracts/mud.config.ts +++ b/test/mock-game-contracts/mud.config.ts @@ -52,4 +52,11 @@ export default defineWorld({ key: ["x", "y"], }, }, + modules: [ + { + artifactPath: + "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json", + root: true, + }, + ], }); diff --git a/test/mock-game-contracts/package.json b/test/mock-game-contracts/package.json index 6b2f60df3f..b2580c5d5c 100644 --- a/test/mock-game-contracts/package.json +++ b/test/mock-game-contracts/package.json @@ -13,6 +13,7 @@ "@latticexyz/schema-type": "workspace:*", "@latticexyz/store": "workspace:*", "@latticexyz/world": "workspace:*", + "@latticexyz/world-modules": "workspace:*", "dotenv": "^16.0.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", "forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1",