Skip to content

Commit

Permalink
feat(cli): deploy with external modules (#2803)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic committed May 10, 2024
1 parent 36e1f76 commit a1b1ebf
Show file tree
Hide file tree
Showing 22 changed files with 270 additions and 125 deletions.
22 changes: 22 additions & 0 deletions .changeset/chatty-frogs-smash.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions e2e/packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
],
});
2 changes: 1 addition & 1 deletion examples/minimal/packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default defineWorld({
},
modules: [
{
name: "KeysWithValueModule",
artifactPath: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json",
root: true,
args: [resolveTableId("Inventory")],
},
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 2 additions & 12 deletions packages/cli/src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -82,17 +82,7 @@ const commandModule: CommandModule<Options, Options> = {
};
});

// 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,
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,5 @@ export type ConfigInput = StoreConfig & WorldConfig;
export type Config<config extends ConfigInput> = {
readonly tables: Tables<config>;
readonly systems: readonly System[];
readonly modules: readonly Module[];
readonly libraries: readonly Library[];
};
81 changes: 81 additions & 0 deletions packages/cli/src/deploy/configToModules.ts
Original file line number Diff line number Diff line change
@@ -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 extends World>(
config: config,
forgeOutDir: string,
): Promise<readonly Module[]> {
// 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<Module> => {
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<SchemaAbiType>);
});

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;
}
8 changes: 5 additions & 3 deletions packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,6 +19,7 @@ import { ensureWorldFactory } from "./ensureWorldFactory";
type DeployOptions<configInput extends ConfigInput> = {
client: Client<Transport, Chain | undefined, Account>;
config: Config<configInput>;
modules?: readonly Module[];
salt?: Hex;
worldAddress?: Address;
/**
Expand All @@ -40,6 +41,7 @@ type DeployOptions<configInput extends ConfigInput> = {
export async function deploy<configInput extends ConfigInput>({
client,
config,
modules = [],
salt,
worldAddress: existingWorldAddress,
deployerAddress: initialDeployerAddress,
Expand All @@ -66,7 +68,7 @@ export async function deploy<configInput extends ConfigInput>({
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`,
Expand Down Expand Up @@ -118,7 +120,7 @@ export async function deploy<configInput extends ConfigInput>({
deployerAddress,
libraries: config.libraries,
worldDeploy,
modules: config.modules,
modules,
});

const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs];
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/deploy/ensureModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,21 @@ 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],
})
: 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],
Expand Down
48 changes: 2 additions & 46 deletions packages/cli/src/deploy/resolveConfig.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -100,49 +96,9 @@ export function resolveConfig<config extends ConfigInput>({
);
}

// 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<SchemaAbiType>);
});
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,
};
}
3 changes: 3 additions & 0 deletions packages/cli/src/runDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -85,6 +86,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
}

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

const account = await (async () => {
if (opts.kms) {
Expand Down Expand Up @@ -131,6 +133,7 @@ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
worldAddress: opts.worldAddress as Hex | undefined,
client,
config: resolvedConfig,
modules,
withWorldProxy: configV2.deploy.upgradeableWorldImplementation,
});
if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {
Expand Down
39 changes: 0 additions & 39 deletions packages/cli/src/utils/defaultModuleContracts.ts

This file was deleted.

0 comments on commit a1b1ebf

Please sign in to comment.