diff --git a/.changeset/quiet-pugs-rule.md b/.changeset/quiet-pugs-rule.md new file mode 100644 index 0000000000..4399bc1c39 --- /dev/null +++ b/.changeset/quiet-pugs-rule.md @@ -0,0 +1,22 @@ +--- +"@latticexyz/world": patch +--- + +Added `deploy` config options to systems in the MUD config: + +- `disabled` to toggle deploying the system (defaults to `false`) +- `registerWorldFunctions` to toggle registering namespace-prefixed system functions on the world (defaults to `true`) + +```ts +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + systems: { + HiddenSystem: { + deploy: { + registerWorldFunctions: false, + }, + }, + }, +}); +``` diff --git a/docs/pages/config/reference.mdx b/docs/pages/config/reference.mdx index 8c2bb986b0..17c45833d8 100644 --- a/docs/pages/config/reference.mdx +++ b/docs/pages/config/reference.mdx @@ -65,6 +65,15 @@ The MUD config has two modes: single namespace and multiple namespaces. By defau Whether or not any address can call this system. Defaults to `true`. A list of contract addresses or system labels that can call this system, used with `openAccess: false`. + + Customize how to deploy this system. + + + Disable deployment of this system. Defaults to `false`. + Whether this system's functions should be registered on the world, prefixed with the system namespace. Defaults to `true`. + + + diff --git a/e2e/packages/contracts/mud.config.ts b/e2e/packages/contracts/mud.config.ts index 14c972dd50..db69f40144 100644 --- a/e2e/packages/contracts/mud.config.ts +++ b/e2e/packages/contracts/mud.config.ts @@ -58,6 +58,13 @@ export default defineWorld({ key: [], }, }, + systems: { + HiddenSystem: { + deploy: { + registerWorldFunctions: false, + }, + }, + }, modules: [ { artifactPath: diff --git a/e2e/packages/contracts/src/systems/HiddenSystem.sol b/e2e/packages/contracts/src/systems/HiddenSystem.sol new file mode 100644 index 0000000000..5ad4b56a11 --- /dev/null +++ b/e2e/packages/contracts/src/systems/HiddenSystem.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { System } from "@latticexyz/world/src/System.sol"; + +contract HiddenSystem is System { + function hidden() public { + // this system is only callable with system ID, not via world + } +} diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 4714d7a934..8116043f2b 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -89,7 +89,7 @@ export type System = DeterministicContract & { readonly allowAll: boolean; readonly allowedAddresses: readonly Hex[]; readonly allowedSystemIds: readonly Hex[]; - readonly functions: readonly WorldFunction[]; + readonly worldFunctions: readonly WorldFunction[]; }; export type DeployedSystem = Omit & { diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index ee148df050..76384c6de3 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -115,7 +115,7 @@ export async function deploy({ const functionTxs = await ensureFunctions({ client, worldDeploy, - functions: systems.flatMap((system) => system.functions), + functions: systems.flatMap((system) => system.worldFunctions), }); const moduleTxs = await ensureModules({ client, diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts index 07b14974a7..70b88132d2 100644 --- a/packages/cli/src/deploy/getSystems.ts +++ b/packages/cli/src/deploy/getSystems.ts @@ -31,7 +31,7 @@ export async function getSystems({ table: worldConfig.namespaces.world.tables.Systems, key: { systemId: system.resourceId }, }); - const systemFunctions = functions.filter((func) => func.systemId === system.resourceId); + const worldFunctions = functions.filter((func) => func.systemId === system.resourceId); return { address, namespace: system.namespace, @@ -41,7 +41,7 @@ export async function getSystems({ allowedAddresses: resourceAccess .filter(({ resourceId }) => resourceId === system.resourceId) .map(({ address }) => address), - functions: systemFunctions, + worldFunctions, }; }), ); diff --git a/packages/cli/src/deploy/resolveConfig.ts b/packages/cli/src/deploy/resolveConfig.ts index e22e8daffe..9209456f2f 100644 --- a/packages/cli/src/deploy/resolveConfig.ts +++ b/packages/cli/src/deploy/resolveConfig.ts @@ -41,45 +41,49 @@ export async function resolveConfig({ const configSystems = await resolveSystems({ rootDir, config }); - const systems = configSystems.map((system): System => { - const contractData = getContractData(`${system.label}.sol`, system.label, forgeOutDir); + const systems = configSystems + .filter((system) => !system.deploy.disabled) + .map((system): System => { + const contractData = getContractData(`${system.label}.sol`, system.label, forgeOutDir); - const systemFunctions = contractData.abi - .filter((item): item is typeof item & { type: "function" } => item.type === "function") - .map(toFunctionSignature) - .filter((sig) => !baseSystemFunctions.includes(sig)) - .map((sig): WorldFunction => { - // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708) - const worldSignature = system.namespace === "" ? sig : `${system.namespace}__${sig}`; - return { - signature: worldSignature, - selector: toFunctionSelector(worldSignature), - systemId: system.systemId, - systemFunctionSignature: sig, - systemFunctionSelector: toFunctionSelector(sig), - }; - }); + const worldFunctions = system.deploy.registerWorldFunctions + ? contractData.abi + .filter((item): item is typeof item & { type: "function" } => item.type === "function") + .map(toFunctionSignature) + .filter((sig) => !baseSystemFunctions.includes(sig)) + .map((sig): WorldFunction => { + // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708) + const worldSignature = system.namespace === "" ? sig : `${system.namespace}__${sig}`; + return { + signature: worldSignature, + selector: toFunctionSelector(worldSignature), + systemId: system.systemId, + systemFunctionSignature: sig, + systemFunctionSelector: toFunctionSelector(sig), + }; + }) + : []; - // 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)!; - return system.systemId; - }); + // 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)!; + return system.systemId; + }); - return { - ...system, - allowAll: system.openAccess, - allowedAddresses, - allowedSystemIds, - prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), - deployedBytecodeSize: contractData.deployedBytecodeSize, - abi: contractData.abi, - functions: systemFunctions, - }; - }); + return { + ...system, + allowAll: system.openAccess, + allowedAddresses, + allowedSystemIds, + prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), + deployedBytecodeSize: contractData.deployedBytecodeSize, + abi: contractData.abi, + worldFunctions, + }; + }); // Check for overlapping system IDs (since names get truncated when turning into IDs) // TODO: move this into the world config resolve step once it resolves system IDs diff --git a/packages/world/ts/config/v2/defaults.ts b/packages/world/ts/config/v2/defaults.ts index e6e7c571b8..13f77b09fb 100644 --- a/packages/world/ts/config/v2/defaults.ts +++ b/packages/world/ts/config/v2/defaults.ts @@ -1,10 +1,17 @@ -import { CodegenInput, DeployInput, ModuleInput, SystemInput, WorldInput } from "./input"; +import { CodegenInput, DeployInput, ModuleInput, SystemDeployInput, SystemInput, WorldInput } from "./input"; + +export const SYSTEM_DEPLOY_DEFAULTS = { + disabled: false, + registerWorldFunctions: true, +} as const satisfies Required; + +export type SYSTEM_DEPLOY_DEFAULTS = typeof SYSTEM_DEPLOY_DEFAULTS; export const SYSTEM_DEFAULTS = { namespace: "", openAccess: true, accessList: [], -} as const satisfies Omit, "label" | "name">; +} as const satisfies Omit, "label" | "name" | "deploy">; export type SYSTEM_DEFAULTS = typeof SYSTEM_DEFAULTS; diff --git a/packages/world/ts/config/v2/input.ts b/packages/world/ts/config/v2/input.ts index 48b7799019..9af5b30884 100644 --- a/packages/world/ts/config/v2/input.ts +++ b/packages/world/ts/config/v2/input.ts @@ -1,6 +1,8 @@ import { StoreInput, NamespaceInput as StoreNamespaceInput } from "@latticexyz/store/config/v2"; import { DynamicResolution, ValueWithType } from "./dynamicResolution"; -import { Codegen } from "./output"; +import { Codegen, SystemDeploy } from "./output"; + +export type SystemDeployInput = Partial; export type SystemInput = { /** @@ -22,6 +24,7 @@ export type SystemInput = { readonly openAccess?: boolean; /** An array of addresses or system names that can access the system */ readonly accessList?: readonly string[]; + readonly deploy?: SystemDeployInput; }; export type SystemsInput = { diff --git a/packages/world/ts/config/v2/output.ts b/packages/world/ts/config/v2/output.ts index 2f70ffd759..00e78c83b5 100644 --- a/packages/world/ts/config/v2/output.ts +++ b/packages/world/ts/config/v2/output.ts @@ -25,6 +25,20 @@ export type Module = { readonly artifactPath: string | undefined; }; +export type SystemDeploy = { + /** + * Whether or not to deploy the system. + * Defaults to `false`. + */ + readonly disabled: boolean; + /** + * Whether or not to register system functions on the world. + * System functions are prefixed with the system namespace when registering on the world, so system function names must be unique within their namespace. + * Defaults to `true`. + */ + readonly registerWorldFunctions: boolean; +}; + export type System = { /** * Human-readable system label. Used as config keys, interface names, and filenames. @@ -47,6 +61,7 @@ export type System = { readonly openAccess: boolean; /** An array of addresses or system names that can access the system */ readonly accessList: readonly string[]; + readonly deploy: SystemDeploy; }; export type Systems = { diff --git a/packages/world/ts/config/v2/system.test.ts b/packages/world/ts/config/v2/system.test.ts index 8664100d46..a1a11b08fe 100644 --- a/packages/world/ts/config/v2/system.test.ts +++ b/packages/world/ts/config/v2/system.test.ts @@ -16,6 +16,10 @@ describe("resolveSystem", () => { namespace: "", name: "ExampleSystem" as string, systemId: resourceToHex({ type: "system", namespace: "", name: "ExampleSystem" }), + deploy: { + disabled: false, + registerWorldFunctions: true, + }, } as const; attest(system).equals(expected); @@ -33,6 +37,10 @@ describe("resolveSystem", () => { namespace: "", name: "ExampleSystem" as string, systemId: resourceToHex({ type: "system", namespace: "", name: "ExampleSystem" }), + deploy: { + disabled: false, + registerWorldFunctions: true, + }, } as const; attest(system).equals(expected); @@ -51,6 +59,10 @@ describe("resolveSystem", () => { name: "ExampleSystem" as string, systemId: resourceToHex({ type: "system", namespace: "", name: "ExampleSystem" }), openAccess: false, + deploy: { + disabled: false, + registerWorldFunctions: true, + }, } as const; attest(system).equals(expected); diff --git a/packages/world/ts/config/v2/system.ts b/packages/world/ts/config/v2/system.ts index c756699eb1..cc201078a4 100644 --- a/packages/world/ts/config/v2/system.ts +++ b/packages/world/ts/config/v2/system.ts @@ -1,7 +1,7 @@ -import { SYSTEM_DEFAULTS } from "./defaults"; +import { SYSTEM_DEFAULTS, SYSTEM_DEPLOY_DEFAULTS } from "./defaults"; import { SystemInput } from "./input"; import { hasOwnKey, mergeIfUndefined } from "@latticexyz/store/config/v2"; -import { ErrorMessage, narrow, requiredKeyOf } from "@ark/util"; +import { ErrorMessage, narrow, requiredKeyOf, show } from "@ark/util"; import { Hex } from "viem"; import { resourceToHex } from "@latticexyz/common"; @@ -52,6 +52,9 @@ export type resolveSystem = input extends SystemInput readonly systemId: Hex; readonly openAccess: undefined extends input["openAccess"] ? SYSTEM_DEFAULTS["openAccess"] : input["openAccess"]; readonly accessList: undefined extends input["accessList"] ? SYSTEM_DEFAULTS["accessList"] : input["accessList"]; + readonly deploy: show< + mergeIfUndefined + >; } : never; @@ -68,6 +71,7 @@ export function resolveSystem(input: input): resolveS namespace, name, systemId, + deploy: mergeIfUndefined(input.deploy ?? {}, SYSTEM_DEPLOY_DEFAULTS), }, SYSTEM_DEFAULTS, ) as never; diff --git a/packages/world/ts/config/v2/world.test.ts b/packages/world/ts/config/v2/world.test.ts index 971840e36a..90c6a2aefd 100644 --- a/packages/world/ts/config/v2/world.test.ts +++ b/packages/world/ts/config/v2/world.test.ts @@ -581,6 +581,7 @@ describe("defineWorld", () => { namespace: "app", name: "Example", systemId: "0x737961707000000000000000000000004578616d706c65000000000000000000", + deploy: { disabled: false, registerWorldFunctions: true }, openAccess: true, accessList: [], }, @@ -592,6 +593,10 @@ describe("defineWorld", () => { readonly systemId: \`0x\${string}\` readonly openAccess: true readonly accessList: readonly [] + readonly deploy: { + readonly disabled: false + readonly registerWorldFunctions: true + } } }`); }); diff --git a/packages/world/ts/node/render-solidity/worldgen.ts b/packages/world/ts/node/render-solidity/worldgen.ts index 76b0ec8944..3305b8b27b 100644 --- a/packages/world/ts/node/render-solidity/worldgen.ts +++ b/packages/world/ts/node/render-solidity/worldgen.ts @@ -28,14 +28,17 @@ export async function worldgen({ const outputPath = path.join(outDir, config.codegen.worldInterfaceName + ".sol"); - const systems = (await resolveSystems({ rootDir, config })).map((system) => { - const interfaceName = `I${system.label}`; - return { - ...system, - interfaceName, - interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`), - }; - }); + const systems = (await resolveSystems({ rootDir, config })) + // TODO: move to codegen option or generate "system manifest" and codegen from that + .filter((system) => system.deploy.registerWorldFunctions) + .map((system) => { + const interfaceName = `I${system.label}`; + return { + ...system, + interfaceName, + interfacePath: path.join(path.dirname(outputPath), `${interfaceName}.sol`), + }; + }); const worldImports = systems.map( (system): ImportDatum => ({