@latticexyz/cli 2.0.0-snapshot-test-32d38619 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/chunk-QXUPZVZL.js +4 -0
  2. package/dist/chunk-QXUPZVZL.js.map +1 -0
  3. package/dist/commands-WWEJJZV4.js +36 -0
  4. package/dist/commands-WWEJJZV4.js.map +1 -0
  5. package/dist/errors-MZURIB7V.js +2 -0
  6. package/dist/errors-MZURIB7V.js.map +1 -0
  7. package/dist/index.js +0 -1
  8. package/dist/mud.js +1 -14
  9. package/dist/mud.js.map +1 -1
  10. package/package.json +21 -15
  11. package/src/build.ts +48 -0
  12. package/src/commands/build.ts +35 -0
  13. package/src/commands/deploy.ts +8 -31
  14. package/src/commands/dev-contracts.ts +80 -141
  15. package/src/commands/index.ts +2 -0
  16. package/src/commands/set-version.ts +24 -62
  17. package/src/commands/tablegen.ts +2 -2
  18. package/src/commands/test.ts +30 -36
  19. package/src/commands/trace.ts +15 -11
  20. package/src/commands/worldgen.ts +7 -6
  21. package/src/common.ts +1 -0
  22. package/src/debug.ts +10 -0
  23. package/src/deploy/common.ts +122 -0
  24. package/src/deploy/configToTables.ts +70 -0
  25. package/src/deploy/create2/README.md +13 -0
  26. package/src/deploy/create2/deployment.json +8 -0
  27. package/src/deploy/createPrepareDeploy.ts +28 -0
  28. package/src/deploy/debug.ts +10 -0
  29. package/src/deploy/deploy.ts +133 -0
  30. package/src/deploy/deployWorld.ts +38 -0
  31. package/src/deploy/ensureContract.ts +66 -0
  32. package/src/deploy/ensureContractsDeployed.ts +33 -0
  33. package/src/deploy/ensureDeployer.ts +75 -0
  34. package/src/deploy/ensureFunctions.ts +86 -0
  35. package/src/deploy/ensureModules.ts +79 -0
  36. package/src/deploy/ensureNamespaceOwner.ts +71 -0
  37. package/src/deploy/ensureSystems.ts +187 -0
  38. package/src/deploy/ensureTables.ts +64 -0
  39. package/src/deploy/ensureWorldFactory.ts +105 -0
  40. package/src/deploy/findLibraries.ts +36 -0
  41. package/src/deploy/getFunctions.ts +58 -0
  42. package/src/deploy/getResourceAccess.ts +51 -0
  43. package/src/deploy/getResourceIds.ts +31 -0
  44. package/src/deploy/getSystems.ts +47 -0
  45. package/src/deploy/getTableValue.ts +30 -0
  46. package/src/deploy/getTables.ts +59 -0
  47. package/src/deploy/getWorldDeploy.ts +39 -0
  48. package/src/deploy/logsToWorldDeploy.ts +49 -0
  49. package/src/deploy/orderByDependencies.ts +12 -0
  50. package/src/deploy/resolveConfig.ts +148 -0
  51. package/src/index.ts +1 -1
  52. package/src/mud.ts +37 -31
  53. package/src/mudPackages.ts +24 -0
  54. package/src/runDeploy.ts +149 -0
  55. package/src/utils/defaultModuleContracts.ts +30 -0
  56. package/src/utils/errors.ts +1 -1
  57. package/src/utils/findPlaceholders.ts +27 -0
  58. package/src/utils/getContractData.ts +38 -0
  59. package/src/utils/{utils/postDeploy.ts → postDeploy.ts} +2 -2
  60. package/src/utils/printMUD.ts +1 -1
  61. package/dist/chunk-WERDORTY.js +0 -11
  62. package/dist/chunk-WERDORTY.js.map +0 -1
  63. package/src/utils/deploy.ts +0 -255
  64. package/src/utils/deployHandler.ts +0 -93
  65. package/src/utils/modules/constants.ts +0 -23
  66. package/src/utils/modules/getInstallModuleCallData.ts +0 -27
  67. package/src/utils/modules/getUserModules.ts +0 -5
  68. package/src/utils/modules/types.ts +0 -14
  69. package/src/utils/systems/getGrantAccessCallData.ts +0 -29
  70. package/src/utils/systems/getRegisterFunctionSelectorsCallData.ts +0 -57
  71. package/src/utils/systems/getRegisterSystemCallData.ts +0 -17
  72. package/src/utils/systems/types.ts +0 -9
  73. package/src/utils/systems/utils.ts +0 -42
  74. package/src/utils/tables/getRegisterTableCallData.ts +0 -49
  75. package/src/utils/tables/getTableIds.ts +0 -21
  76. package/src/utils/tables/types.ts +0 -12
  77. package/src/utils/utils/confirmNonce.ts +0 -24
  78. package/src/utils/utils/deployContract.ts +0 -33
  79. package/src/utils/utils/fastTxExecute.ts +0 -56
  80. package/src/utils/utils/getChainId.ts +0 -10
  81. package/src/utils/utils/getContractData.ts +0 -29
  82. package/src/utils/utils/setInternalFeePerGas.ts +0 -49
  83. package/src/utils/utils/toBytes16.ts +0 -16
  84. package/src/utils/utils/types.ts +0 -21
  85. package/src/utils/world.ts +0 -28
@@ -0,0 +1,39 @@
1
+ import { Client, Address, getAddress, parseAbi } from "viem";
2
+ import { getBlockNumber, getLogs } from "viem/actions";
3
+ import { WorldDeploy, worldDeployEvents } from "./common";
4
+ import { debug } from "./debug";
5
+ import { logsToWorldDeploy } from "./logsToWorldDeploy";
6
+
7
+ const deploys = new Map<Address, WorldDeploy>();
8
+
9
+ export async function getWorldDeploy(client: Client, worldAddress: Address): Promise<WorldDeploy> {
10
+ const address = getAddress(worldAddress);
11
+
12
+ let deploy = deploys.get(address);
13
+ if (deploy != null) {
14
+ return deploy;
15
+ }
16
+
17
+ debug("looking up world deploy for", address);
18
+
19
+ const stateBlock = await getBlockNumber(client);
20
+ const logs = await getLogs(client, {
21
+ strict: true,
22
+ address,
23
+ events: parseAbi(worldDeployEvents),
24
+ // this may fail for certain RPC providers with block range limits
25
+ // if so, could potentially use our fetchLogs helper (which does pagination)
26
+ fromBlock: "earliest",
27
+ toBlock: stateBlock,
28
+ });
29
+
30
+ deploy = {
31
+ ...logsToWorldDeploy(logs),
32
+ stateBlock,
33
+ };
34
+ deploys.set(address, deploy);
35
+
36
+ debug("found world deploy for", address, "at block", deploy.deployBlock);
37
+
38
+ return deploy;
39
+ }
@@ -0,0 +1,49 @@
1
+ import { AbiEventSignatureNotFoundError, Log, decodeEventLog, hexToString, parseAbi } from "viem";
2
+ import { WorldDeploy, worldDeployEvents } from "./common";
3
+ import { isDefined } from "@latticexyz/common/utils";
4
+
5
+ export function logsToWorldDeploy(logs: readonly Log<bigint, number, false>[]): Omit<WorldDeploy, "stateBlock"> {
6
+ const deployLogs = logs
7
+ .map((log) => {
8
+ try {
9
+ return {
10
+ ...log,
11
+ ...decodeEventLog({
12
+ strict: true,
13
+ abi: parseAbi(worldDeployEvents),
14
+ topics: log.topics,
15
+ data: log.data,
16
+ }),
17
+ };
18
+ } catch (error: unknown) {
19
+ if (error instanceof AbiEventSignatureNotFoundError) {
20
+ return;
21
+ }
22
+ throw error;
23
+ }
24
+ })
25
+ .filter(isDefined);
26
+
27
+ // TODO: should this test for/validate that only one of each of these events is present? and that the address/block number don't change between each?
28
+ const { address, deployBlock, worldVersion, storeVersion } = deployLogs.reduce<Partial<WorldDeploy>>(
29
+ (deploy, log) => ({
30
+ ...deploy,
31
+ address: log.address,
32
+ deployBlock: log.blockNumber,
33
+ ...(log.eventName === "HelloWorld"
34
+ ? { worldVersion: hexToString(log.args.worldVersion).replace(/\0+$/, "") }
35
+ : null),
36
+ ...(log.eventName === "HelloStore"
37
+ ? { storeVersion: hexToString(log.args.storeVersion).replace(/\0+$/, "") }
38
+ : null),
39
+ }),
40
+ {},
41
+ );
42
+
43
+ if (address == null) throw new Error("could not find world address");
44
+ if (deployBlock == null) throw new Error("could not find world deploy block number");
45
+ if (worldVersion == null) throw new Error("could not find world version");
46
+ if (storeVersion == null) throw new Error("could not find store version");
47
+
48
+ return { address, deployBlock, worldVersion, storeVersion };
49
+ }
@@ -0,0 +1,12 @@
1
+ import toposort from "toposort";
2
+
3
+ export function orderByDependencies<T>(
4
+ items: readonly T[],
5
+ itemKey: (item: T) => string,
6
+ dependencyKeys: (item: T) => string[],
7
+ ): readonly T[] {
8
+ const dependencyOrder = toposort(
9
+ items.flatMap((item) => dependencyKeys(item).map((dependency) => [itemKey(item), dependency] as [string, string])),
10
+ );
11
+ return [...items].sort((a, b) => dependencyOrder.indexOf(itemKey(a)) - dependencyOrder.indexOf(itemKey(b)));
12
+ }
@@ -0,0 +1,148 @@
1
+ import path from "path";
2
+ import { resolveWorldConfig } from "@latticexyz/world/internal";
3
+ import { Config, ConfigInput, Library, Module, System, WorldFunction } from "./common";
4
+ 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";
9
+ import { getExistingContracts } from "../utils/getExistingContracts";
10
+ import { defaultModuleContracts } from "../utils/defaultModuleContracts";
11
+ import { getContractData } from "../utils/getContractData";
12
+ import { configToTables } from "./configToTables";
13
+ import { groupBy } from "@latticexyz/common/utils";
14
+ import { findLibraries } from "./findLibraries";
15
+ import { createPrepareDeploy } from "./createPrepareDeploy";
16
+
17
+ // TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668
18
+
19
+ export function resolveConfig<config extends ConfigInput>({
20
+ config,
21
+ forgeSourceDir,
22
+ forgeOutDir,
23
+ }: {
24
+ config: config;
25
+ forgeSourceDir: string;
26
+ forgeOutDir: string;
27
+ }): Config<config> {
28
+ const libraries = findLibraries(forgeOutDir).map((library): Library => {
29
+ // foundry/solc flattens artifacts, so we just use the path basename
30
+ const contractData = getContractData(path.basename(library.path), library.name, forgeOutDir);
31
+ return {
32
+ path: library.path,
33
+ name: library.name,
34
+ abi: contractData.abi,
35
+ prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
36
+ deployedBytecodeSize: contractData.deployedBytecodeSize,
37
+ };
38
+ });
39
+
40
+ const tables = configToTables(config);
41
+
42
+ // TODO: should the config parser/loader help with resolving systems?
43
+ const contractNames = getExistingContracts(forgeSourceDir).map(({ basename }) => basename);
44
+ const resolvedConfig = resolveWorldConfig(config, contractNames);
45
+ const baseSystemContractData = getContractData("System.sol", "System", forgeOutDir);
46
+ const baseSystemFunctions = baseSystemContractData.abi
47
+ .filter((item): item is typeof item & { type: "function" } => item.type === "function")
48
+ .map(toFunctionSignature);
49
+
50
+ const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]): System => {
51
+ const namespace = config.namespace;
52
+ const name = system.name;
53
+ const systemId = resourceToHex({ type: "system", namespace, name });
54
+ const contractData = getContractData(`${systemName}.sol`, systemName, forgeOutDir);
55
+
56
+ const systemFunctions = contractData.abi
57
+ .filter((item): item is typeof item & { type: "function" } => item.type === "function")
58
+ .map(toFunctionSignature)
59
+ .filter((sig) => !baseSystemFunctions.includes(sig))
60
+ .map((sig): WorldFunction => {
61
+ // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
62
+ const worldSignature = namespace === "" ? sig : `${namespace}__${sig}`;
63
+ return {
64
+ signature: worldSignature,
65
+ selector: toFunctionSelector(worldSignature),
66
+ systemId,
67
+ systemFunctionSignature: sig,
68
+ systemFunctionSelector: toFunctionSelector(sig),
69
+ };
70
+ });
71
+
72
+ return {
73
+ namespace,
74
+ name,
75
+ systemId,
76
+ allowAll: system.openAccess,
77
+ allowedAddresses: system.accessListAddresses as Hex[],
78
+ allowedSystemIds: system.accessListSystems.map((name) =>
79
+ resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name }),
80
+ ),
81
+ prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
82
+ deployedBytecodeSize: contractData.deployedBytecodeSize,
83
+ abi: contractData.abi,
84
+ functions: systemFunctions,
85
+ };
86
+ });
87
+
88
+ // Check for overlapping system IDs (since names get truncated when turning into IDs)
89
+ // TODO: move this into the world config resolve step once it resolves system IDs
90
+ const systemsById = groupBy(systems, (system) => system.systemId);
91
+ const overlappingSystems = Array.from(systemsById.values())
92
+ .filter((matches) => matches.length > 1)
93
+ .flat();
94
+ if (overlappingSystems.length) {
95
+ const names = overlappingSystems.map((system) => system.name);
96
+ throw new Error(
97
+ `Found systems with overlapping system ID: ${names.join(
98
+ ", ",
99
+ )}.\n\nSystem IDs are generated from the first 16 bytes of the name, so you may need to rename them to avoid the overlap.`,
100
+ );
101
+ }
102
+
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
+
142
+ return {
143
+ tables,
144
+ systems,
145
+ modules,
146
+ libraries,
147
+ };
148
+ }
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export * from "./utils/deployHandler";
1
+ // nothing to export
package/src/mud.ts CHANGED
@@ -1,39 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import yargs from "yargs";
4
- import { hideBin } from "yargs/helpers";
5
- import { commands } from "./commands";
6
- import { logError } from "./utils/errors";
7
-
8
3
  // Load .env file into process.env
9
4
  import * as dotenv from "dotenv";
10
- import chalk from "chalk";
11
5
  dotenv.config();
12
6
 
13
- yargs(hideBin(process.argv))
14
- // Explicit name to display in help (by default it's the entry file, which may not be "mud" for e.g. ts-node)
15
- .scriptName("mud")
16
- // Use the commands directory to scaffold
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- command array overload isn't typed, see https://github.com/yargs/yargs/blob/main/docs/advanced.md#esm-hierarchy
18
- .command(commands as any)
19
- // Enable strict mode.
20
- .strict()
21
- // Custom error handler
22
- .fail((msg, err) => {
23
- console.error(chalk.red(msg));
24
- if (msg.includes("Missing required argument")) {
25
- console.log(
26
- chalk.yellow(`Run 'pnpm mud ${process.argv[2]} --help' for a list of available and required arguments.`)
27
- );
28
- }
29
- console.log("");
30
- // Even though `.fail` type says we should get an `Error`, this can sometimes be undefined
31
- if (err != null) {
32
- logError(err);
7
+ async function run() {
8
+ // Import everything else async so they can pick up env vars in .env
9
+ const { default: yargs } = await import("yargs");
10
+ const { default: chalk } = await import("chalk");
11
+ const { hideBin } = await import("yargs/helpers");
12
+ const { logError } = await import("./utils/errors");
13
+ const { commands } = await import("./commands");
14
+
15
+ yargs(hideBin(process.argv))
16
+ // Explicit name to display in help (by default it's the entry file, which may not be "mud" for e.g. ts-node)
17
+ .scriptName("mud")
18
+ // Use the commands directory to scaffold
19
+ // command array overload isn't typed, see https://github.com/yargs/yargs/blob/main/docs/advanced.md#esm-hierarchy
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ .command(commands as any)
22
+ // Enable strict mode.
23
+ .strict()
24
+ // Custom error handler
25
+ .fail((msg, err) => {
26
+ console.error(chalk.red(msg));
27
+ if (msg.includes("Missing required argument")) {
28
+ console.log(
29
+ chalk.yellow(`Run 'pnpm mud ${process.argv[2]} --help' for a list of available and required arguments.`),
30
+ );
31
+ }
33
32
  console.log("");
34
- }
33
+ // Even though `.fail` type says we should get an `Error`, this can sometimes be undefined
34
+ if (err != null) {
35
+ logError(err);
36
+ console.log("");
37
+ }
38
+
39
+ process.exit(1);
40
+ })
41
+ // Useful aliases.
42
+ .alias({ h: "help" }).argv;
43
+ }
35
44
 
36
- process.exit(1);
37
- })
38
- // Useful aliases.
39
- .alias({ h: "help" }).argv;
45
+ run();
@@ -0,0 +1,24 @@
1
+ import { ZodError, z } from "zod";
2
+ import { MudPackages } from "./common";
3
+
4
+ const envSchema = z.object({
5
+ MUD_PACKAGES: z.string().transform((value) => JSON.parse(value) as MudPackages),
6
+ });
7
+
8
+ function parseEnv(): z.infer<typeof envSchema> {
9
+ try {
10
+ return envSchema.parse({
11
+ // tsup replaces the env vars with their values at compile time
12
+ MUD_PACKAGES: process.env.MUD_PACKAGES,
13
+ });
14
+ } catch (error) {
15
+ if (error instanceof ZodError) {
16
+ const { ...invalidEnvVars } = error.format();
17
+ console.error(`\nMissing or invalid environment variables:\n\n ${Object.keys(invalidEnvVars).join("\n ")}\n`);
18
+ process.exit(1);
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ export const mudPackages = parseEnv().MUD_PACKAGES;
@@ -0,0 +1,149 @@
1
+ import path from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { InferredOptionTypes, Options } from "yargs";
4
+ import { deploy } from "./deploy/deploy";
5
+ import { createWalletClient, http, Hex, isHex } from "viem";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ import { loadConfig } from "@latticexyz/config/node";
8
+ import { World as WorldConfig } from "@latticexyz/world";
9
+ import { worldToV1 } from "@latticexyz/world/config/v2";
10
+ import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
11
+ import chalk from "chalk";
12
+ import { MUDError } from "@latticexyz/common/errors";
13
+ import { resolveConfig } from "./deploy/resolveConfig";
14
+ import { getChainId } from "viem/actions";
15
+ import { postDeploy } from "./utils/postDeploy";
16
+ import { WorldDeploy } from "./deploy/common";
17
+ import { build } from "./build";
18
+
19
+ export const deployOptions = {
20
+ configPath: { type: "string", desc: "Path to the config file" },
21
+ printConfig: { type: "boolean", desc: "Print the resolved config" },
22
+ profile: { type: "string", desc: "The foundry profile to use" },
23
+ saveDeployment: { type: "boolean", desc: "Save the deployment info to a file", default: true },
24
+ rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" },
25
+ rpcBatch: {
26
+ type: "boolean",
27
+ desc: "Enable batch processing of RPC requests in viem client (defaults to batch size of 100 and wait of 1s)",
28
+ },
29
+ deployerAddress: {
30
+ type: "string",
31
+ desc: "Deploy using an existing deterministic deployer (https://github.com/Arachnid/deterministic-deployment-proxy)",
32
+ },
33
+ worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" },
34
+ srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." },
35
+ skipBuild: { type: "boolean", desc: "Skip rebuilding the contracts before deploying" },
36
+ alwaysRunPostDeploy: {
37
+ type: "boolean",
38
+ desc: "Always run PostDeploy.s.sol after each deploy (including during upgrades). By default, PostDeploy.s.sol is only run once after a new world is deployed.",
39
+ },
40
+ salt: {
41
+ type: "string",
42
+ desc: "The deployment salt to use. Defaults to a random salt.",
43
+ },
44
+ } as const satisfies Record<string, Options>;
45
+
46
+ export type DeployOptions = InferredOptionTypes<typeof deployOptions>;
47
+
48
+ /**
49
+ * Given some CLI arguments, finds and resolves a MUD config, foundry profile, and runs a deploy.
50
+ * This is used by the deploy, test, and dev-contracts CLI commands.
51
+ */
52
+ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
53
+ const salt = opts.salt;
54
+ if (salt != null && !isHex(salt)) {
55
+ throw new MUDError("Expected hex string for salt");
56
+ }
57
+
58
+ const profile = opts.profile ?? process.env.FOUNDRY_PROFILE;
59
+
60
+ const configV2 = (await loadConfig(opts.configPath)) as WorldConfig;
61
+ const config = worldToV1(configV2);
62
+ if (opts.printConfig) {
63
+ console.log(chalk.green("\nResolved config:\n"), JSON.stringify(config, null, 2));
64
+ }
65
+
66
+ const srcDir = opts.srcDir ?? (await getSrcDirectory(profile));
67
+ const outDir = await getOutDirectory(profile);
68
+
69
+ const rpc = opts.rpc ?? (await getRpcUrl(profile));
70
+ console.log(
71
+ chalk.bgBlue(
72
+ chalk.whiteBright(`\n Deploying MUD contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`),
73
+ ),
74
+ );
75
+
76
+ // Run build
77
+ if (!opts.skipBuild) {
78
+ await build({ config: configV2, srcDir, foundryProfile: profile });
79
+ }
80
+
81
+ const privateKey = process.env.PRIVATE_KEY as Hex;
82
+ if (!privateKey) {
83
+ throw new MUDError(
84
+ `Missing PRIVATE_KEY environment variable.
85
+ Run 'echo "PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" > .env'
86
+ in your contracts directory to use the default anvil private key.`,
87
+ );
88
+ }
89
+
90
+ const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir });
91
+
92
+ const client = createWalletClient({
93
+ transport: http(rpc, {
94
+ batch: opts.rpcBatch
95
+ ? {
96
+ batchSize: 100,
97
+ wait: 1000,
98
+ }
99
+ : undefined,
100
+ }),
101
+ account: privateKeyToAccount(privateKey),
102
+ });
103
+
104
+ console.log("Deploying from", client.account.address);
105
+
106
+ const startTime = Date.now();
107
+ const worldDeploy = await deploy({
108
+ deployerAddress: opts.deployerAddress as Hex | undefined,
109
+ salt,
110
+ worldAddress: opts.worldAddress as Hex | undefined,
111
+ client,
112
+ config: resolvedConfig,
113
+ });
114
+ if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {
115
+ await postDeploy(config.postDeployScript, worldDeploy.address, rpc, profile);
116
+ }
117
+ console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
118
+
119
+ const deploymentInfo = {
120
+ worldAddress: worldDeploy.address,
121
+ blockNumber: Number(worldDeploy.deployBlock),
122
+ };
123
+
124
+ if (opts.saveDeployment) {
125
+ const chainId = await getChainId(client);
126
+ const deploysDir = path.join(config.deploysDirectory, chainId.toString());
127
+ mkdirSync(deploysDir, { recursive: true });
128
+ writeFileSync(path.join(deploysDir, "latest.json"), JSON.stringify(deploymentInfo, null, 2));
129
+ writeFileSync(path.join(deploysDir, Date.now() + ".json"), JSON.stringify(deploymentInfo, null, 2));
130
+
131
+ const localChains = [1337, 31337];
132
+ const deploys = existsSync(config.worldsFile) ? JSON.parse(readFileSync(config.worldsFile, "utf-8")) : {};
133
+ deploys[chainId] = {
134
+ address: deploymentInfo.worldAddress,
135
+ // We expect the worlds file to be committed and since local deployments are often
136
+ // a consistent address but different block number, we'll ignore the block number.
137
+ blockNumber: localChains.includes(chainId) ? undefined : deploymentInfo.blockNumber,
138
+ };
139
+ writeFileSync(config.worldsFile, JSON.stringify(deploys, null, 2));
140
+
141
+ console.log(
142
+ chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${config.worldsFile} and ${deploysDir}): \n`)),
143
+ );
144
+ }
145
+
146
+ console.log(deploymentInfo);
147
+
148
+ return worldDeploy;
149
+ }
@@ -0,0 +1,30 @@
1
+ import KeysWithValueModuleData from "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json" assert { type: "json" };
2
+ import KeysInTableModuleData from "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" };
3
+ import UniqueEntityModuleData from "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" };
4
+ import { Abi, Hex, size } from "viem";
5
+ import { findPlaceholders } from "./findPlaceholders";
6
+
7
+ // These modules are always deployed
8
+ export const defaultModuleContracts = [
9
+ {
10
+ name: "KeysWithValueModule",
11
+ abi: KeysWithValueModuleData.abi as Abi,
12
+ bytecode: KeysWithValueModuleData.bytecode.object as Hex,
13
+ placeholders: findPlaceholders(KeysWithValueModuleData.bytecode.linkReferences),
14
+ deployedBytecodeSize: size(KeysWithValueModuleData.deployedBytecode.object as Hex),
15
+ },
16
+ {
17
+ name: "KeysInTableModule",
18
+ abi: KeysInTableModuleData.abi as Abi,
19
+ bytecode: KeysInTableModuleData.bytecode.object as Hex,
20
+ placeholders: findPlaceholders(KeysInTableModuleData.bytecode.linkReferences),
21
+ deployedBytecodeSize: size(KeysInTableModuleData.deployedBytecode.object as Hex),
22
+ },
23
+ {
24
+ name: "UniqueEntityModule",
25
+ abi: UniqueEntityModuleData.abi as Abi,
26
+ bytecode: UniqueEntityModuleData.bytecode.object as Hex,
27
+ placeholders: findPlaceholders(UniqueEntityModuleData.bytecode.linkReferences),
28
+ deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex),
29
+ },
30
+ ];
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { ZodError } from "zod";
3
3
  import { fromZodError, ValidationError } from "zod-validation-error";
4
- import { NotInsideProjectError } from "@latticexyz/config";
4
+ import { NotInsideProjectError } from "@latticexyz/config/library";
5
5
  import { MUDError } from "@latticexyz/common/errors";
6
6
 
7
7
  export function logError(error: unknown) {
@@ -0,0 +1,27 @@
1
+ import { LibraryPlaceholder } from "../deploy/common";
2
+
3
+ // TODO: move this to a broader solc artifact type
4
+ /** From `artifact.bytecode.linkReferences` where `artifact` is the solc JSON output of a compiled Solidity contract */
5
+ export type LinkReferences = {
6
+ [filename: string]: {
7
+ [name: string]: {
8
+ start: number;
9
+ length: number;
10
+ }[];
11
+ };
12
+ };
13
+
14
+ export function findPlaceholders(linkReferences: LinkReferences): readonly LibraryPlaceholder[] {
15
+ return Object.entries(linkReferences).flatMap(([path, contracts]) =>
16
+ Object.entries(contracts).flatMap(([contractName, locations]) =>
17
+ locations.map(
18
+ (location): LibraryPlaceholder => ({
19
+ path,
20
+ name: contractName,
21
+ start: location.start,
22
+ length: location.length,
23
+ }),
24
+ ),
25
+ ),
26
+ );
27
+ }
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from "fs";
2
+ import path from "path";
3
+ import { MUDError } from "@latticexyz/common/errors";
4
+ import { Abi, Hex, size } from "viem";
5
+ import { LibraryPlaceholder } from "../deploy/common";
6
+ import { findPlaceholders } from "./findPlaceholders";
7
+
8
+ /**
9
+ * Load the contract's abi and bytecode from the file system
10
+ * @param contractName: Name of the contract to load
11
+ */
12
+ export function getContractData(
13
+ filename: string,
14
+ contractName: string,
15
+ forgeOutDirectory: string,
16
+ ): { bytecode: Hex; placeholders: readonly LibraryPlaceholder[]; abi: Abi; deployedBytecodeSize: number } {
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ let data: any;
19
+ const contractDataPath = path.join(forgeOutDirectory, filename, contractName + ".json");
20
+ try {
21
+ data = JSON.parse(readFileSync(contractDataPath, "utf8"));
22
+ } catch (error) {
23
+ throw new MUDError(`Error reading file at ${contractDataPath}`);
24
+ }
25
+
26
+ const bytecode = data?.bytecode?.object;
27
+ if (!bytecode) throw new MUDError(`No bytecode found in ${contractDataPath}`);
28
+
29
+ const deployedBytecode = data?.deployedBytecode?.object;
30
+ if (!deployedBytecode) throw new MUDError(`No deployed bytecode found in ${contractDataPath}`);
31
+
32
+ const abi = data?.abi;
33
+ if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`);
34
+
35
+ const placeholders = findPlaceholders(data?.bytecode?.linkReferences ?? {});
36
+
37
+ return { abi, bytecode, placeholders, deployedBytecodeSize: size(deployedBytecode as Hex) };
38
+ }
@@ -7,7 +7,7 @@ export async function postDeploy(
7
7
  postDeployScript: string,
8
8
  worldAddress: string,
9
9
  rpc: string,
10
- profile: string | undefined
10
+ profile: string | undefined,
11
11
  ): Promise<void> {
12
12
  // Execute postDeploy forge script
13
13
  const postDeployPath = path.join(await getScriptDirectory(), postDeployScript + ".s.sol");
@@ -17,7 +17,7 @@ export async function postDeploy(
17
17
  ["script", postDeployScript, "--sig", "run(address)", worldAddress, "--broadcast", "--rpc-url", rpc, "-vvv"],
18
18
  {
19
19
  profile: profile,
20
- }
20
+ },
21
21
  );
22
22
  } else {
23
23
  console.log(`No script at ${postDeployPath}, skipping post deploy hook`);
@@ -9,6 +9,6 @@ export function printMUD() {
9
9
  | :\\/: || :\\/: || (__) |
10
10
  | '--'M|| '--'U|| '--'D|
11
11
  '------''------''------'
12
- `)
12
+ `),
13
13
  );
14
14
  }