@latticexyz/cli 2.0.0-skystrife-playtest-9e9511d4 → 2.0.0-transaction-context-324984c5

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 (61) hide show
  1. package/dist/chunk-22IIKR4S.js +4 -0
  2. package/dist/chunk-22IIKR4S.js.map +1 -0
  3. package/dist/commands-3JV3U43E.js +27 -0
  4. package/dist/commands-3JV3U43E.js.map +1 -0
  5. package/dist/errors-XGN6V2Y3.js +2 -0
  6. package/dist/errors-XGN6V2Y3.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 +20 -13
  11. package/src/build.ts +44 -0
  12. package/src/commands/build.ts +36 -0
  13. package/src/commands/deploy.ts +7 -30
  14. package/src/commands/dev-contracts.ts +76 -128
  15. package/src/commands/index.ts +2 -0
  16. package/src/commands/set-version.ts +23 -61
  17. package/src/commands/tablegen.ts +3 -2
  18. package/src/commands/test.ts +30 -36
  19. package/src/commands/trace.ts +13 -6
  20. package/src/commands/worldgen.ts +1 -1
  21. package/src/common.ts +1 -0
  22. package/src/debug.ts +10 -0
  23. package/src/deploy/common.ts +76 -0
  24. package/src/deploy/configToTables.ts +68 -0
  25. package/src/deploy/create2/README.md +9 -0
  26. package/src/deploy/create2/deployment.json +7 -0
  27. package/src/deploy/debug.ts +10 -0
  28. package/src/deploy/deploy.ts +116 -0
  29. package/src/deploy/deployWorld.ts +37 -0
  30. package/src/deploy/ensureContract.ts +61 -0
  31. package/src/deploy/ensureContractsDeployed.ts +25 -0
  32. package/src/deploy/ensureDeployer.ts +36 -0
  33. package/src/deploy/ensureFunctions.ts +86 -0
  34. package/src/deploy/ensureModules.ts +73 -0
  35. package/src/deploy/ensureNamespaceOwner.ts +71 -0
  36. package/src/deploy/ensureSystems.ts +162 -0
  37. package/src/deploy/ensureTables.ts +65 -0
  38. package/src/deploy/ensureWorldFactory.ts +118 -0
  39. package/src/deploy/getFunctions.ts +58 -0
  40. package/src/deploy/getResourceAccess.ts +51 -0
  41. package/src/deploy/getResourceIds.ts +31 -0
  42. package/src/deploy/getSystems.ts +48 -0
  43. package/src/deploy/getTableValue.ts +30 -0
  44. package/src/deploy/getTables.ts +59 -0
  45. package/src/deploy/getWorldDeploy.ts +39 -0
  46. package/src/deploy/logsToWorldDeploy.ts +49 -0
  47. package/src/deploy/resolveConfig.ts +151 -0
  48. package/src/deploy/resourceLabel.ts +3 -0
  49. package/src/index.ts +1 -1
  50. package/src/mud.ts +37 -31
  51. package/src/mudPackages.ts +24 -0
  52. package/src/runDeploy.ts +131 -0
  53. package/src/utils/modules/constants.ts +26 -0
  54. package/src/utils/utils/getContractData.ts +32 -0
  55. package/src/utils/utils/postDeploy.ts +25 -0
  56. package/dist/chunk-OJAPOMSC.js +0 -11
  57. package/dist/chunk-OJAPOMSC.js.map +0 -1
  58. package/src/utils/deploy.ts +0 -620
  59. package/src/utils/deployHandler.ts +0 -93
  60. package/src/utils/getChainId.ts +0 -10
  61. package/src/utils/index.ts +0 -6
@@ -0,0 +1,59 @@
1
+ import { Client, parseAbiItem, decodeAbiParameters, parseAbiParameters } from "viem";
2
+ import { Table } from "./configToTables";
3
+ import { hexToResource } from "@latticexyz/common";
4
+ import { WorldDeploy, storeTables } from "./common";
5
+ import { debug } from "./debug";
6
+ import { storeSetRecordEvent } from "@latticexyz/store";
7
+ import { getLogs } from "viem/actions";
8
+ import { KeySchema, ValueSchema, decodeKey, decodeValueArgs, hexToSchema } from "@latticexyz/protocol-parser";
9
+
10
+ export async function getTables({
11
+ client,
12
+ worldDeploy,
13
+ }: {
14
+ readonly client: Client;
15
+ readonly worldDeploy: WorldDeploy;
16
+ }): Promise<readonly Table[]> {
17
+ // This assumes we only use `Tables._set(...)`, which is true as of this writing.
18
+ // TODO: PR to viem's getLogs to accept topics array so we can filter on all store events and quickly recreate this table's current state
19
+ // TODO: consider moving this to a batched getRecord for Tables table
20
+
21
+ debug("looking up tables for", worldDeploy.address);
22
+ const logs = await getLogs(client, {
23
+ strict: true,
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: worldDeploy.deployBlock,
27
+ toBlock: worldDeploy.stateBlock,
28
+ address: worldDeploy.address,
29
+ event: parseAbiItem(storeSetRecordEvent),
30
+ args: { tableId: storeTables.store_Tables.tableId },
31
+ });
32
+
33
+ // TODO: combine with store-sync logToTable and export from somewhere
34
+ const tables = logs.map((log) => {
35
+ const { tableId } = decodeKey(storeTables.store_Tables.keySchema, log.args.keyTuple);
36
+ const { namespace, name } = hexToResource(tableId);
37
+ const value = decodeValueArgs(storeTables.store_Tables.valueSchema, log.args);
38
+
39
+ // TODO: migrate to better helper
40
+ const keySchemaFields = hexToSchema(value.keySchema);
41
+ const valueSchemaFields = hexToSchema(value.valueSchema);
42
+ const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames)[0];
43
+ const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames)[0];
44
+
45
+ const valueAbiTypes = [...valueSchemaFields.staticFields, ...valueSchemaFields.dynamicFields];
46
+
47
+ const keySchema = Object.fromEntries(
48
+ keySchemaFields.staticFields.map((abiType, i) => [keyNames[i], abiType])
49
+ ) as KeySchema;
50
+ const valueSchema = Object.fromEntries(valueAbiTypes.map((abiType, i) => [fieldNames[i], abiType])) as ValueSchema;
51
+
52
+ return { namespace, name, tableId, keySchema, valueSchema } as const;
53
+ });
54
+ // TODO: filter/detect duplicates?
55
+
56
+ debug("found", tables.length, "tables for", worldDeploy.address);
57
+
58
+ return tables;
59
+ }
@@ -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, trim } 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(trim(log.args.worldVersion, { dir: "right" })) }
35
+ : null),
36
+ ...(log.eventName === "HelloStore"
37
+ ? { storeVersion: hexToString(trim(log.args.storeVersion, { dir: "right" })) }
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,151 @@
1
+ import { resolveWorldConfig } from "@latticexyz/world";
2
+ import { Config, ConfigInput, WorldFunction, salt } from "./common";
3
+ import { resourceToHex, hexToResource } from "@latticexyz/common";
4
+ import { resolveWithContext } from "@latticexyz/config";
5
+ import { encodeField } from "@latticexyz/protocol-parser";
6
+ import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type";
7
+ import {
8
+ getFunctionSelector,
9
+ Hex,
10
+ getCreate2Address,
11
+ getAddress,
12
+ hexToBytes,
13
+ Abi,
14
+ bytesToHex,
15
+ getFunctionSignature,
16
+ } from "viem";
17
+ import { getExistingContracts } from "../utils/getExistingContracts";
18
+ import { defaultModuleContracts } from "../utils/modules/constants";
19
+ import { getContractData } from "../utils/utils/getContractData";
20
+ import { configToTables } from "./configToTables";
21
+ import { deployer } from "./ensureDeployer";
22
+ import { resourceLabel } from "./resourceLabel";
23
+
24
+ // TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668
25
+
26
+ export function resolveConfig<config extends ConfigInput>({
27
+ config,
28
+ forgeSourceDir,
29
+ forgeOutDir,
30
+ }: {
31
+ config: config;
32
+ forgeSourceDir: string;
33
+ forgeOutDir: string;
34
+ }): Config<config> {
35
+ const tables = configToTables(config);
36
+
37
+ // TODO: should the config parser/loader help with resolving systems?
38
+ const contractNames = getExistingContracts(forgeSourceDir).map(({ basename }) => basename);
39
+ const resolvedConfig = resolveWorldConfig(config, contractNames);
40
+ const baseSystemContractData = getContractData("System", forgeOutDir);
41
+ const baseSystemFunctions = baseSystemContractData.abi
42
+ .filter((item): item is typeof item & { type: "function" } => item.type === "function")
43
+ .map(getFunctionSignature);
44
+
45
+ const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]) => {
46
+ const namespace = config.namespace;
47
+ const name = system.name;
48
+ const systemId = resourceToHex({ type: "system", namespace, name });
49
+ const contractData = getContractData(systemName, forgeOutDir);
50
+
51
+ const systemFunctions = contractData.abi
52
+ .filter((item): item is typeof item & { type: "function" } => item.type === "function")
53
+ .map(getFunctionSignature)
54
+ .filter((sig) => !baseSystemFunctions.includes(sig))
55
+ .map((sig): WorldFunction => {
56
+ // TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
57
+ const worldSignature = namespace === "" ? sig : `${namespace}__${sig}`;
58
+ return {
59
+ signature: worldSignature,
60
+ selector: getFunctionSelector(worldSignature),
61
+ systemId,
62
+ systemFunctionSignature: sig,
63
+ systemFunctionSelector: getFunctionSelector(sig),
64
+ };
65
+ });
66
+
67
+ return {
68
+ namespace,
69
+ name,
70
+ systemId,
71
+ allowAll: system.openAccess,
72
+ allowedAddresses: system.accessListAddresses as Hex[],
73
+ allowedSystemIds: system.accessListSystems.map((name) =>
74
+ resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name })
75
+ ),
76
+ address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
77
+ bytecode: contractData.bytecode,
78
+ deployedBytecodeSize: contractData.deployedBytecodeSize,
79
+ abi: contractData.abi,
80
+ functions: systemFunctions,
81
+ };
82
+ });
83
+
84
+ // resolve allowedSystemIds
85
+ // TODO: resolve this at deploy time so we can allow for arbitrary system IDs registered in the world as the source-of-truth rather than config
86
+ const systemsWithAccess = systems.map(({ allowedAddresses, allowedSystemIds, ...system }) => {
87
+ const allowedSystemAddresses = allowedSystemIds.map((systemId) => {
88
+ const targetSystem = systems.find((s) => s.systemId === systemId);
89
+ if (!targetSystem) {
90
+ throw new Error(
91
+ `System ${resourceLabel(system)} wanted access to ${resourceLabel(
92
+ hexToResource(systemId)
93
+ )}, but it wasn't found in the config.`
94
+ );
95
+ }
96
+ return targetSystem.address;
97
+ });
98
+ return {
99
+ ...system,
100
+ allowedAddresses: Array.from(
101
+ new Set([...allowedAddresses, ...allowedSystemAddresses].map((addr) => getAddress(addr)))
102
+ ),
103
+ };
104
+ });
105
+
106
+ // ugh (https://github.com/latticexyz/mud/issues/1668)
107
+ const resolveContext = {
108
+ tableIds: Object.fromEntries(
109
+ Object.entries(config.tables).map(([tableName, table]) => [
110
+ tableName,
111
+ hexToBytes(
112
+ resourceToHex({
113
+ type: table.offchainOnly ? "offchainTable" : "table",
114
+ namespace: config.namespace,
115
+ name: table.name,
116
+ })
117
+ ),
118
+ ])
119
+ ),
120
+ };
121
+
122
+ const modules = config.modules.map((mod) => {
123
+ const contractData =
124
+ defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ??
125
+ getContractData(mod.name, forgeOutDir);
126
+ const installArgs = mod.args
127
+ .map((arg) => resolveWithContext(arg, resolveContext))
128
+ .map((arg) => {
129
+ const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
130
+ return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
131
+ });
132
+ if (installArgs.length > 1) {
133
+ throw new Error(`${mod.name} module should only have 0-1 args, but had ${installArgs.length} args.`);
134
+ }
135
+ return {
136
+ name: mod.name,
137
+ installAsRoot: mod.root,
138
+ installData: installArgs.length === 0 ? "0x" : installArgs[0],
139
+ address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
140
+ bytecode: contractData.bytecode,
141
+ deployedBytecodeSize: contractData.deployedBytecodeSize,
142
+ abi: contractData.abi,
143
+ };
144
+ });
145
+
146
+ return {
147
+ tables,
148
+ systems: systemsWithAccess,
149
+ modules,
150
+ };
151
+ }
@@ -0,0 +1,3 @@
1
+ export function resourceLabel({ namespace, name }: { readonly namespace: string; readonly name: string }): string {
2
+ return `${namespace}:${name}`;
3
+ }
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 { _errors, ...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,131 @@
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 { StoreConfig } from "@latticexyz/store";
9
+ import { WorldConfig } from "@latticexyz/world";
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/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
+ worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" },
26
+ srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." },
27
+ skipBuild: { type: "boolean", desc: "Skip rebuilding the contracts before deploying" },
28
+ alwaysRunPostDeploy: {
29
+ type: "boolean",
30
+ 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.",
31
+ },
32
+ salt: {
33
+ type: "string",
34
+ desc: "The deployment salt to use. Defaults to a random salt.",
35
+ },
36
+ } as const satisfies Record<string, Options>;
37
+
38
+ export type DeployOptions = InferredOptionTypes<typeof deployOptions>;
39
+
40
+ /**
41
+ * Given some CLI arguments, finds and resolves a MUD config, foundry profile, and runs a deploy.
42
+ * This is used by the deploy, test, and dev-contracts CLI commands.
43
+ */
44
+ export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
45
+ const salt = opts.salt;
46
+ if (salt != null && !isHex(salt)) {
47
+ throw new MUDError("Expected hex string for salt");
48
+ }
49
+
50
+ const profile = opts.profile ?? process.env.FOUNDRY_PROFILE;
51
+
52
+ const config = (await loadConfig(opts.configPath)) as StoreConfig & WorldConfig;
53
+ if (opts.printConfig) {
54
+ console.log(chalk.green("\nResolved config:\n"), JSON.stringify(config, null, 2));
55
+ }
56
+
57
+ const srcDir = opts.srcDir ?? (await getSrcDirectory(profile));
58
+ const outDir = await getOutDirectory(profile);
59
+
60
+ const rpc = opts.rpc ?? (await getRpcUrl(profile));
61
+ console.log(
62
+ chalk.bgBlue(
63
+ chalk.whiteBright(`\n Deploying MUD contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
64
+ )
65
+ );
66
+
67
+ // Run build
68
+ if (!opts.skipBuild) {
69
+ await build({ config, srcDir, foundryProfile: profile });
70
+ }
71
+
72
+ const privateKey = process.env.PRIVATE_KEY as Hex;
73
+ if (!privateKey) {
74
+ throw new MUDError(
75
+ `Missing PRIVATE_KEY environment variable.
76
+ Run 'echo "PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" > .env'
77
+ in your contracts directory to use the default anvil private key.`
78
+ );
79
+ }
80
+
81
+ const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir });
82
+
83
+ const client = createWalletClient({
84
+ transport: http(rpc),
85
+ account: privateKeyToAccount(privateKey),
86
+ });
87
+ console.log("Deploying from", client.account.address);
88
+
89
+ const startTime = Date.now();
90
+ const worldDeploy = await deploy({
91
+ salt,
92
+ worldAddress: opts.worldAddress as Hex | undefined,
93
+ client,
94
+ config: resolvedConfig,
95
+ });
96
+ if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {
97
+ await postDeploy(config.postDeployScript, worldDeploy.address, rpc, profile);
98
+ }
99
+ console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
100
+
101
+ const deploymentInfo = {
102
+ worldAddress: worldDeploy.address,
103
+ blockNumber: Number(worldDeploy.deployBlock),
104
+ };
105
+
106
+ if (opts.saveDeployment) {
107
+ const chainId = await getChainId(client);
108
+ const deploysDir = path.join(config.deploysDirectory, chainId.toString());
109
+ mkdirSync(deploysDir, { recursive: true });
110
+ writeFileSync(path.join(deploysDir, "latest.json"), JSON.stringify(deploymentInfo, null, 2));
111
+ writeFileSync(path.join(deploysDir, Date.now() + ".json"), JSON.stringify(deploymentInfo, null, 2));
112
+
113
+ const localChains = [1337, 31337];
114
+ const deploys = existsSync(config.worldsFile) ? JSON.parse(readFileSync(config.worldsFile, "utf-8")) : {};
115
+ deploys[chainId] = {
116
+ address: deploymentInfo.worldAddress,
117
+ // We expect the worlds file to be committed and since local deployments are often
118
+ // a consistent address but different block number, we'll ignore the block number.
119
+ blockNumber: localChains.includes(chainId) ? undefined : deploymentInfo.blockNumber,
120
+ };
121
+ writeFileSync(config.worldsFile, JSON.stringify(deploys, null, 2));
122
+
123
+ console.log(
124
+ chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${config.worldsFile} and ${deploysDir}): \n`))
125
+ );
126
+ }
127
+
128
+ console.log(deploymentInfo);
129
+
130
+ return worldDeploy;
131
+ }
@@ -0,0 +1,26 @@
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
+
6
+ // These modules are always deployed
7
+ export const defaultModuleContracts = [
8
+ {
9
+ name: "KeysWithValueModule",
10
+ abi: KeysWithValueModuleData.abi as Abi,
11
+ bytecode: KeysWithValueModuleData.bytecode.object as Hex,
12
+ deployedBytecodeSize: size(KeysWithValueModuleData.deployedBytecode.object as Hex),
13
+ },
14
+ {
15
+ name: "KeysInTableModule",
16
+ abi: KeysInTableModuleData.abi as Abi,
17
+ bytecode: KeysInTableModuleData.bytecode.object as Hex,
18
+ deployedBytecodeSize: size(KeysInTableModuleData.deployedBytecode.object as Hex),
19
+ },
20
+ {
21
+ name: "UniqueEntityModule",
22
+ abi: UniqueEntityModuleData.abi as Abi,
23
+ bytecode: UniqueEntityModuleData.bytecode.object as Hex,
24
+ deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex),
25
+ },
26
+ ];
@@ -0,0 +1,32 @@
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
+
6
+ /**
7
+ * Load the contract's abi and bytecode from the file system
8
+ * @param contractName: Name of the contract to load
9
+ */
10
+ export function getContractData(
11
+ contractName: string,
12
+ forgeOutDirectory: string
13
+ ): { bytecode: Hex; abi: Abi; deployedBytecodeSize: number } {
14
+ let data: any;
15
+ const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json");
16
+ try {
17
+ data = JSON.parse(readFileSync(contractDataPath, "utf8"));
18
+ } catch (error: any) {
19
+ throw new MUDError(`Error reading file at ${contractDataPath}`);
20
+ }
21
+
22
+ const bytecode = data?.bytecode?.object;
23
+ if (!bytecode) throw new MUDError(`No bytecode found in ${contractDataPath}`);
24
+
25
+ const deployedBytecode = data?.deployedBytecode?.object;
26
+ if (!deployedBytecode) throw new MUDError(`No deployed bytecode found in ${contractDataPath}`);
27
+
28
+ const abi = data?.abi;
29
+ if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`);
30
+
31
+ return { abi, bytecode, deployedBytecodeSize: size(deployedBytecode as Hex) };
32
+ }
@@ -0,0 +1,25 @@
1
+ import { existsSync } from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { getScriptDirectory, forge } from "@latticexyz/common/foundry";
5
+
6
+ export async function postDeploy(
7
+ postDeployScript: string,
8
+ worldAddress: string,
9
+ rpc: string,
10
+ profile: string | undefined
11
+ ): Promise<void> {
12
+ // Execute postDeploy forge script
13
+ const postDeployPath = path.join(await getScriptDirectory(), postDeployScript + ".s.sol");
14
+ if (existsSync(postDeployPath)) {
15
+ console.log(chalk.blue(`Executing post deploy script at ${postDeployPath}`));
16
+ await forge(
17
+ ["script", postDeployScript, "--sig", "run(address)", worldAddress, "--broadcast", "--rpc-url", rpc, "-vvv"],
18
+ {
19
+ profile: profile,
20
+ }
21
+ );
22
+ } else {
23
+ console.log(`No script at ${postDeployPath}, skipping post deploy hook`);
24
+ }
25
+ }