Skip to content

Commit a1b1ebf

Browse files
authored
feat(cli): deploy with external modules (latticexyz#2803)
1 parent 36e1f76 commit a1b1ebf

22 files changed

+270
-125
lines changed

.changeset/chatty-frogs-smash.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@latticexyz/cli": patch
3+
"@latticexyz/world": patch
4+
---
5+
6+
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.
7+
8+
```diff
9+
defineWorld({
10+
// …
11+
modules: [
12+
{
13+
- name: "KeysWithValueModule",
14+
+ artifactPath: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json",
15+
root: true,
16+
args: [resolveTableId("Inventory")],
17+
},
18+
],
19+
});
20+
```
21+
22+
Note that the above assumes `@latticexyz/world-modules` is included as a dependency of your project.

e2e/packages/contracts/mud.config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ export default defineWorld({
6060
},
6161
modules: [
6262
{
63-
name: "Unstable_CallWithSignatureModule",
63+
artifactPath:
64+
"@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json",
6465
root: true,
65-
args: [],
6666
},
6767
],
6868
});

examples/minimal/packages/contracts/mud.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default defineWorld({
3939
},
4040
modules: [
4141
{
42-
name: "KeysWithValueModule",
42+
artifactPath: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json",
4343
root: true,
4444
args: [resolveTableId("Inventory")],
4545
},

packages/cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@latticexyz/utils": "workspace:*",
4747
"@latticexyz/world": "workspace:*",
4848
"@latticexyz/world-modules": "workspace:*",
49+
"abitype": "1.0.0",
4950
"asn1.js": "^5.4.1",
5051
"chalk": "^5.0.1",
5152
"chokidar": "^3.5.3",

packages/cli/src/commands/verify.ts

+2-12
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { worldToV1 } from "@latticexyz/world/config/v2";
77
import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
88
import { getExistingContracts } from "../utils/getExistingContracts";
99
import { getContractData } from "../utils/getContractData";
10-
import { defaultModuleContracts } from "../utils/defaultModuleContracts";
1110
import { Hex, createWalletClient, http } from "viem";
1211
import chalk from "chalk";
12+
import { configToModules } from "../deploy/configToModules";
1313

1414
const verifyOptions = {
1515
deployerAddress: {
@@ -82,17 +82,7 @@ const commandModule: CommandModule<Options, Options> = {
8282
};
8383
});
8484

85-
// Get modules
86-
const modules = config.modules.map((mod) => {
87-
const contractData =
88-
defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ??
89-
getContractData(`${mod.name}.sol`, mod.name, outDir);
90-
91-
return {
92-
name: mod.name,
93-
bytecode: contractData.bytecode,
94-
};
95-
});
85+
const modules = await configToModules(configV2, outDir);
9686

9787
await verify({
9888
client,

packages/cli/src/deploy/common.ts

-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,5 @@ export type ConfigInput = StoreConfig & WorldConfig;
117117
export type Config<config extends ConfigInput> = {
118118
readonly tables: Tables<config>;
119119
readonly systems: readonly System[];
120-
readonly modules: readonly Module[];
121120
readonly libraries: readonly Library[];
122121
};
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import path from "node:path";
2+
import { Module } from "./common";
3+
import { resolveWithContext } from "@latticexyz/config/library";
4+
import { encodeField } from "@latticexyz/protocol-parser/internal";
5+
import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type/internal";
6+
import { bytesToHex, hexToBytes } from "viem";
7+
import { createPrepareDeploy } from "./createPrepareDeploy";
8+
import { World } from "@latticexyz/world";
9+
import { getContractArtifact } from "../utils/getContractArtifact";
10+
import { knownModuleArtifacts } from "../utils/knownModuleArtifacts";
11+
12+
export async function configToModules<config extends World>(
13+
config: config,
14+
forgeOutDir: string,
15+
): Promise<readonly Module[]> {
16+
// this expects a namespaced table name when used with `resolveTableId`
17+
const resolveContext = {
18+
tableIds: Object.fromEntries(
19+
Object.entries(config.tables).map(([tableName, table]) => [tableName, hexToBytes(table.tableId)]),
20+
),
21+
};
22+
23+
const modules = await Promise.all(
24+
config.modules.map(async (mod): Promise<Module> => {
25+
let artifactPath = mod.artifactPath;
26+
27+
// Backwards compatibility
28+
// TODO: move this up a level so we don't need `forgeOutDir` in here?
29+
if (!artifactPath) {
30+
if (mod.name) {
31+
artifactPath =
32+
knownModuleArtifacts[mod.name as keyof typeof knownModuleArtifacts] ??
33+
path.join(forgeOutDir, `${mod.name}.sol`, `${mod.name}.json`);
34+
console.warn(
35+
[
36+
"",
37+
`⚠️ Your \`mud.config.ts\` is using a module with a \`name\`, but this option is deprecated.`,
38+
"",
39+
"To resolve this, you can replace this:",
40+
"",
41+
` name: ${JSON.stringify(mod.name)}`,
42+
"",
43+
"with this:",
44+
"",
45+
` artifactPath: ${JSON.stringify(artifactPath)}`,
46+
"",
47+
].join("\n"),
48+
);
49+
} else {
50+
throw new Error("No `artifactPath` provided for module.");
51+
}
52+
}
53+
54+
const name = path.basename(artifactPath, ".json");
55+
const artifact = await getContractArtifact({ artifactPath });
56+
57+
// TODO: replace args with something more strongly typed
58+
const installArgs = mod.args
59+
.map((arg) => resolveWithContext(arg, resolveContext))
60+
.map((arg) => {
61+
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
62+
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
63+
});
64+
65+
if (installArgs.length > 1) {
66+
throw new Error(`${name} module should only have 0-1 args, but had ${installArgs.length} args.`);
67+
}
68+
69+
return {
70+
name,
71+
installAsRoot: mod.root,
72+
installData: installArgs.length === 0 ? "0x" : installArgs[0],
73+
prepareDeploy: createPrepareDeploy(artifact.bytecode, artifact.placeholders),
74+
deployedBytecodeSize: artifact.deployedBytecodeSize,
75+
abi: artifact.abi,
76+
};
77+
}),
78+
);
79+
80+
return modules;
81+
}

packages/cli/src/deploy/deploy.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Account, Address, Chain, Client, Hex, Transport } from "viem";
22
import { ensureDeployer } from "./ensureDeployer";
33
import { deployWorld } from "./deployWorld";
44
import { ensureTables } from "./ensureTables";
5-
import { Config, ConfigInput, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common";
5+
import { Config, ConfigInput, Module, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common";
66
import { ensureSystems } from "./ensureSystems";
77
import { waitForTransactionReceipt } from "viem/actions";
88
import { getWorldDeploy } from "./getWorldDeploy";
@@ -19,6 +19,7 @@ import { ensureWorldFactory } from "./ensureWorldFactory";
1919
type DeployOptions<configInput extends ConfigInput> = {
2020
client: Client<Transport, Chain | undefined, Account>;
2121
config: Config<configInput>;
22+
modules?: readonly Module[];
2223
salt?: Hex;
2324
worldAddress?: Address;
2425
/**
@@ -40,6 +41,7 @@ type DeployOptions<configInput extends ConfigInput> = {
4041
export async function deploy<configInput extends ConfigInput>({
4142
client,
4243
config,
44+
modules = [],
4345
salt,
4446
worldAddress: existingWorldAddress,
4547
deployerAddress: initialDeployerAddress,
@@ -66,7 +68,7 @@ export async function deploy<configInput extends ConfigInput>({
6668
deployedBytecodeSize: system.deployedBytecodeSize,
6769
label: `${resourceToLabel(system)} system`,
6870
})),
69-
...config.modules.map((mod) => ({
71+
...modules.map((mod) => ({
7072
bytecode: mod.prepareDeploy(deployerAddress, config.libraries).bytecode,
7173
deployedBytecodeSize: mod.deployedBytecodeSize,
7274
label: `${mod.name} module`,
@@ -118,7 +120,7 @@ export async function deploy<configInput extends ConfigInput>({
118120
deployerAddress,
119121
libraries: config.libraries,
120122
worldDeploy,
121-
modules: config.modules,
123+
modules,
122124
});
123125

124126
const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs];

packages/cli/src/deploy/ensureModules.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,21 @@ export async function ensureModules({
3838
pRetry(
3939
async () => {
4040
try {
41+
const abi = [...worldAbi, ...mod.abi];
4142
const moduleAddress = mod.prepareDeploy(deployerAddress, libraries).address;
4243
return mod.installAsRoot
4344
? await writeContract(client, {
4445
chain: client.chain ?? null,
4546
address: worldDeploy.address,
46-
abi: worldAbi,
47+
abi,
4748
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
4849
functionName: "installRootModule",
4950
args: [moduleAddress, mod.installData],
5051
})
5152
: await writeContract(client, {
5253
chain: client.chain ?? null,
5354
address: worldDeploy.address,
54-
abi: worldAbi,
55+
abi,
5556
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
5657
functionName: "installModule",
5758
args: [moduleAddress, mod.installData],

packages/cli/src/deploy/resolveConfig.ts

+2-46
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import path from "path";
22
import { resolveWorldConfig } from "@latticexyz/world/internal";
3-
import { Config, ConfigInput, Library, Module, System, WorldFunction } from "./common";
3+
import { Config, ConfigInput, Library, System, WorldFunction } from "./common";
44
import { resourceToHex } from "@latticexyz/common";
5-
import { resolveWithContext } from "@latticexyz/config/library";
6-
import { encodeField } from "@latticexyz/protocol-parser/internal";
7-
import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type/internal";
8-
import { Hex, hexToBytes, bytesToHex, toFunctionSelector, toFunctionSignature } from "viem";
5+
import { Hex, toFunctionSelector, toFunctionSignature } from "viem";
96
import { getExistingContracts } from "../utils/getExistingContracts";
10-
import { defaultModuleContracts } from "../utils/defaultModuleContracts";
117
import { getContractData } from "../utils/getContractData";
128
import { configToTables } from "./configToTables";
139
import { groupBy } from "@latticexyz/common/utils";
@@ -100,49 +96,9 @@ export function resolveConfig<config extends ConfigInput>({
10096
);
10197
}
10298

103-
// ugh (https://github.com/latticexyz/mud/issues/1668)
104-
const resolveContext = {
105-
tableIds: Object.fromEntries(
106-
Object.entries(config.tables).map(([tableName, table]) => [
107-
tableName,
108-
hexToBytes(
109-
resourceToHex({
110-
type: table.offchainOnly ? "offchainTable" : "table",
111-
namespace: config.namespace,
112-
name: table.name,
113-
}),
114-
),
115-
]),
116-
),
117-
};
118-
119-
const modules = config.modules.map((mod): Module => {
120-
const contractData =
121-
defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ??
122-
getContractData(`${mod.name}.sol`, mod.name, forgeOutDir);
123-
const installArgs = mod.args
124-
.map((arg) => resolveWithContext(arg, resolveContext))
125-
.map((arg) => {
126-
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
127-
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
128-
});
129-
if (installArgs.length > 1) {
130-
throw new Error(`${mod.name} module should only have 0-1 args, but had ${installArgs.length} args.`);
131-
}
132-
return {
133-
name: mod.name,
134-
installAsRoot: mod.root,
135-
installData: installArgs.length === 0 ? "0x" : installArgs[0],
136-
prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
137-
deployedBytecodeSize: contractData.deployedBytecodeSize,
138-
abi: contractData.abi,
139-
};
140-
});
141-
14299
return {
143100
tables,
144101
systems,
145-
modules,
146102
libraries,
147103
};
148104
}

packages/cli/src/runDeploy.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { postDeploy } from "./utils/postDeploy";
1616
import { WorldDeploy } from "./deploy/common";
1717
import { build } from "./build";
1818
import { kmsKeyToAccount } from "@latticexyz/common/kms";
19+
import { configToModules } from "./deploy/configToModules";
1920

2021
export const deployOptions = {
2122
configPath: { type: "string", desc: "Path to the MUD config file" },
@@ -85,6 +86,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
8586
}
8687

8788
const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir });
89+
const modules = await configToModules(configV2, outDir);
8890

8991
const account = await (async () => {
9092
if (opts.kms) {
@@ -131,6 +133,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
131133
worldAddress: opts.worldAddress as Hex | undefined,
132134
client,
133135
config: resolvedConfig,
136+
modules,
134137
withWorldProxy: configV2.deploy.upgradeableWorldImplementation,
135138
});
136139
if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {

packages/cli/src/utils/defaultModuleContracts.ts

-39
This file was deleted.

0 commit comments

Comments
 (0)