@latticexyz/cli 2.0.0-next.11 → 2.0.0-next.13

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 (68) hide show
  1. package/dist/chunk-22IIKR4S.js +4 -0
  2. package/dist/chunk-22IIKR4S.js.map +1 -0
  3. package/dist/commands-AAHOIIJW.js +23 -0
  4. package/dist/commands-AAHOIIJW.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 -18
  9. package/dist/mud.js.map +1 -1
  10. package/package.json +16 -12
  11. package/src/commands/deploy.ts +7 -30
  12. package/src/commands/dev-contracts.ts +74 -138
  13. package/src/commands/test.ts +30 -36
  14. package/src/commands/trace.ts +7 -5
  15. package/src/debug.ts +3 -0
  16. package/src/deploy/assertNamespaceOwner.ts +42 -0
  17. package/src/deploy/common.ts +72 -0
  18. package/src/deploy/configToTables.ts +68 -0
  19. package/src/deploy/create2/README.md +9 -0
  20. package/src/deploy/create2/deployment.json +7 -0
  21. package/src/deploy/debug.ts +3 -0
  22. package/src/deploy/deploy.ts +108 -0
  23. package/src/deploy/deployWorld.ts +33 -0
  24. package/src/deploy/ensureContract.ts +49 -0
  25. package/src/deploy/ensureContractsDeployed.ts +25 -0
  26. package/src/deploy/ensureDeployer.ts +36 -0
  27. package/src/deploy/ensureFunctions.ts +86 -0
  28. package/src/deploy/ensureModules.ts +72 -0
  29. package/src/deploy/ensureSystems.ts +161 -0
  30. package/src/deploy/ensureTables.ts +65 -0
  31. package/src/deploy/ensureWorldFactory.ts +34 -0
  32. package/src/deploy/getFunctions.ts +58 -0
  33. package/src/deploy/getResourceAccess.ts +51 -0
  34. package/src/deploy/getResourceIds.ts +31 -0
  35. package/src/deploy/getSystems.ts +48 -0
  36. package/src/deploy/getTableValue.ts +30 -0
  37. package/src/deploy/getTables.ts +59 -0
  38. package/src/deploy/getWorldDeploy.ts +39 -0
  39. package/src/deploy/logsToWorldDeploy.ts +49 -0
  40. package/src/deploy/resolveConfig.ts +154 -0
  41. package/src/deploy/resourceLabel.ts +3 -0
  42. package/src/index.ts +1 -1
  43. package/src/mud.ts +37 -31
  44. package/src/runDeploy.ts +128 -0
  45. package/src/utils/modules/constants.ts +1 -2
  46. package/src/utils/utils/getContractData.ts +2 -5
  47. package/dist/chunk-TW3YGZ4D.js +0 -11
  48. package/dist/chunk-TW3YGZ4D.js.map +0 -1
  49. package/src/utils/deploy.ts +0 -254
  50. package/src/utils/deployHandler.ts +0 -93
  51. package/src/utils/modules/getInstallModuleCallData.ts +0 -27
  52. package/src/utils/modules/getUserModules.ts +0 -5
  53. package/src/utils/modules/types.ts +0 -14
  54. package/src/utils/systems/getGrantAccessCallData.ts +0 -29
  55. package/src/utils/systems/getRegisterFunctionSelectorsCallData.ts +0 -57
  56. package/src/utils/systems/getRegisterSystemCallData.ts +0 -17
  57. package/src/utils/systems/types.ts +0 -9
  58. package/src/utils/systems/utils.ts +0 -42
  59. package/src/utils/tables/getRegisterTableCallData.ts +0 -49
  60. package/src/utils/tables/getTableIds.ts +0 -18
  61. package/src/utils/tables/types.ts +0 -12
  62. package/src/utils/utils/confirmNonce.ts +0 -24
  63. package/src/utils/utils/deployContract.ts +0 -33
  64. package/src/utils/utils/fastTxExecute.ts +0 -56
  65. package/src/utils/utils/getChainId.ts +0 -10
  66. package/src/utils/utils/setInternalFeePerGas.ts +0 -49
  67. package/src/utils/utils/types.ts +0 -21
  68. package/src/utils/world.ts +0 -28
@@ -1,70 +1,64 @@
1
- import type { CommandModule } from "yargs";
1
+ import type { CommandModule, InferredOptionTypes, Options } from "yargs";
2
2
  import { anvil, forge, getRpcUrl } from "@latticexyz/common/foundry";
3
3
  import chalk from "chalk";
4
- import { rmSync, writeFileSync } from "fs";
5
- import { yDeployOptions } from "./deploy";
6
- import { DeployOptions, deployHandler } from "../utils/deployHandler";
4
+ import { deployOptions, runDeploy } from "../runDeploy";
7
5
 
8
- type Options = DeployOptions & { port?: number; worldAddress?: string; forgeOptions?: string };
6
+ const testOptions = {
7
+ ...deployOptions,
8
+ port: { type: "number", description: "Port to run internal node for fork testing on", default: 4242 },
9
+ worldAddress: {
10
+ type: "string",
11
+ description:
12
+ "Address of an existing world contract. If provided, deployment is skipped and the RPC provided in the foundry.toml is used for fork testing.",
13
+ },
14
+ forgeOptions: { type: "string", description: "Options to pass to forge test" },
15
+ } as const satisfies Record<string, Options>;
9
16
 
10
- const WORLD_ADDRESS_FILE = ".mudtest";
17
+ type TestOptions = InferredOptionTypes<typeof testOptions>;
11
18
 
12
- const commandModule: CommandModule<Options, Options> = {
19
+ const commandModule: CommandModule<typeof testOptions, TestOptions> = {
13
20
  command: "test",
14
21
 
15
22
  describe: "Run tests in MUD contracts",
16
23
 
17
24
  builder(yargs) {
18
- return yargs.options({
19
- ...yDeployOptions,
20
- port: { type: "number", description: "Port to run internal node for fork testing on", default: 4242 },
21
- worldAddress: {
22
- type: "string",
23
- description:
24
- "Address of an existing world contract. If provided, deployment is skipped and the RPC provided in the foundry.toml is used for fork testing.",
25
- },
26
- forgeOptions: { type: "string", description: "Options to pass to forge test" },
27
- });
25
+ return yargs.options(testOptions);
28
26
  },
29
27
 
30
- async handler(args) {
28
+ async handler(opts) {
31
29
  // Start an internal anvil process if no world address is provided
32
- if (!args.worldAddress) {
33
- const anvilArgs = ["--block-base-fee-per-gas", "0", "--port", String(args.port)];
30
+ if (!opts.worldAddress) {
31
+ const anvilArgs = ["--block-base-fee-per-gas", "0", "--port", String(opts.port)];
34
32
  anvil(anvilArgs);
35
33
  }
36
34
 
37
- const forkRpc = args.worldAddress ? await getRpcUrl(args.profile) : `http://127.0.0.1:${args.port}`;
35
+ const forkRpc = opts.worldAddress ? await getRpcUrl(opts.profile) : `http://127.0.0.1:${opts.port}`;
38
36
 
39
37
  const worldAddress =
40
- args.worldAddress ??
38
+ opts.worldAddress ??
41
39
  (
42
- await deployHandler({
43
- ...args,
40
+ await runDeploy({
41
+ ...opts,
44
42
  saveDeployment: false,
45
43
  rpc: forkRpc,
46
44
  })
47
- ).worldAddress;
45
+ ).address;
48
46
 
49
47
  console.log(chalk.blue("World address", worldAddress));
50
48
 
51
- // Create a temporary file to pass the world address to the tests
52
- writeFileSync(WORLD_ADDRESS_FILE, worldAddress);
53
-
54
- const userOptions = args.forgeOptions?.replaceAll("\\", "").split(" ") ?? [];
49
+ const userOptions = opts.forgeOptions?.replaceAll("\\", "").split(" ") ?? [];
55
50
  try {
56
- const testResult = await forge(["test", "--fork-url", forkRpc, ...userOptions], {
57
- profile: args.profile,
51
+ await forge(["test", "--fork-url", forkRpc, ...userOptions], {
52
+ profile: opts.profile,
53
+ env: {
54
+ WORLD_ADDRESS: worldAddress,
55
+ },
58
56
  });
59
- console.log(testResult);
57
+ process.exit(0);
60
58
  } catch (e) {
61
59
  console.error(e);
62
- rmSync(WORLD_ADDRESS_FILE);
63
60
  process.exit(1);
64
61
  }
65
-
66
- rmSync(WORLD_ADDRESS_FILE);
67
- process.exit(0);
68
62
  },
69
63
  };
70
64
 
@@ -9,12 +9,13 @@ import { StoreConfig } from "@latticexyz/store";
9
9
  import { resolveWorldConfig, WorldConfig } from "@latticexyz/world";
10
10
  import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" };
11
11
  import worldConfig from "@latticexyz/world/mud.config.js";
12
- import { resourceIdToHex } from "@latticexyz/common";
12
+ import { resourceToHex } from "@latticexyz/common";
13
13
  import { getExistingContracts } from "../utils/getExistingContracts";
14
- import { getChainId } from "../utils/utils/getChainId";
14
+ import { createClient, http } from "viem";
15
+ import { getChainId } from "viem/actions";
15
16
 
16
17
  // TODO account for multiple namespaces (https://github.com/latticexyz/mud/issues/994)
17
- const systemsTableId = resourceIdToHex({
18
+ const systemsTableId = resourceToHex({
18
19
  type: "system",
19
20
  namespace: worldConfig.namespace,
20
21
  name: worldConfig.tables.Systems.name,
@@ -80,7 +81,7 @@ const commandModule: CommandModule<Options, Options> = {
80
81
  const systemTableFieldLayout = await WorldContract.getFieldLayout(systemsTableId);
81
82
  const labels: { name: string; address: string }[] = [];
82
83
  for (const name of names) {
83
- const systemSelector = resourceIdToHex({ type: "system", namespace, name });
84
+ const systemSelector = resourceToHex({ type: "system", namespace, name });
84
85
  // Get the first field of `Systems` table (the table maps system name to its address and other data)
85
86
  const address = await WorldContract.getField(systemsTableId, [systemSelector], 0, systemTableFieldLayout);
86
87
  labels.push({ name, address });
@@ -103,7 +104,8 @@ export default commandModule;
103
104
 
104
105
  async function getWorldAddress(worldsFile: string, rpc: string) {
105
106
  if (existsSync(worldsFile)) {
106
- const chainId = await getChainId(rpc);
107
+ const client = createClient({ transport: http(rpc) });
108
+ const chainId = await getChainId(client);
107
109
  const deploys = JSON.parse(readFileSync(worldsFile, "utf-8"));
108
110
 
109
111
  if (!deploys[chainId]) {
package/src/debug.ts ADDED
@@ -0,0 +1,3 @@
1
+ import createDebug from "debug";
2
+
3
+ export const debug = createDebug("mud:cli");
@@ -0,0 +1,42 @@
1
+ import { Account, Chain, Client, Hex, Transport, getAddress } from "viem";
2
+ import { WorldDeploy, worldTables } from "./common";
3
+ import { hexToResource, resourceToHex } from "@latticexyz/common";
4
+ import { getResourceIds } from "./getResourceIds";
5
+ import { getTableValue } from "./getTableValue";
6
+
7
+ export async function assertNamespaceOwner({
8
+ client,
9
+ worldDeploy,
10
+ resourceIds,
11
+ }: {
12
+ readonly client: Client<Transport, Chain | undefined, Account>;
13
+ readonly worldDeploy: WorldDeploy;
14
+ readonly resourceIds: readonly Hex[];
15
+ }): Promise<void> {
16
+ const desiredNamespaces = Array.from(new Set(resourceIds.map((resourceId) => hexToResource(resourceId).namespace)));
17
+ const existingResourceIds = await getResourceIds({ client, worldDeploy });
18
+ const existingNamespaces = Array.from(
19
+ new Set(existingResourceIds.map((resourceId) => hexToResource(resourceId).namespace))
20
+ );
21
+
22
+ const namespaces = desiredNamespaces.filter((namespace) => existingNamespaces.includes(namespace));
23
+ const namespaceOwners = await Promise.all(
24
+ namespaces.map(async (namespace) => {
25
+ const { owner } = await getTableValue({
26
+ client,
27
+ worldDeploy,
28
+ table: worldTables.world_NamespaceOwner,
29
+ key: { namespaceId: resourceToHex({ type: "namespace", namespace, name: "" }) },
30
+ });
31
+ return [namespace, owner];
32
+ })
33
+ );
34
+
35
+ const unauthorizedNamespaces = namespaceOwners
36
+ .filter(([, owner]) => getAddress(owner) !== getAddress(client.account.address))
37
+ .map(([namespace]) => namespace);
38
+
39
+ if (unauthorizedNamespaces.length) {
40
+ throw new Error(`You are attempting to deploy to namespaces you do not own: ${unauthorizedNamespaces.join(", ")}`);
41
+ }
42
+ }
@@ -0,0 +1,72 @@
1
+ import { Abi, Address, Hex, padHex } from "viem";
2
+ import storeConfig from "@latticexyz/store/mud.config.js";
3
+ import worldConfig from "@latticexyz/world/mud.config.js";
4
+ import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" };
5
+ import IModuleAbi from "@latticexyz/world-modules/out/IModule.sol/IModule.abi.json" assert { type: "json" };
6
+ import { Tables, configToTables } from "./configToTables";
7
+ import { StoreConfig, helloStoreEvent } from "@latticexyz/store";
8
+ import { WorldConfig, helloWorldEvent } from "@latticexyz/world";
9
+
10
+ export const salt = padHex("0x", { size: 32 });
11
+
12
+ // TODO: add `as const` to mud config so these get more strongly typed (blocked by current config parsing not using readonly)
13
+ export const storeTables = configToTables(storeConfig);
14
+ export const worldTables = configToTables(worldConfig);
15
+
16
+ export const worldDeployEvents = [helloStoreEvent, helloWorldEvent] as const;
17
+
18
+ export const worldAbi = [...IBaseWorldAbi, ...IModuleAbi] as const;
19
+
20
+ // Ideally, this should be an append-only list. Before adding more versions here, be sure to add backwards-compatible support for old Store/World versions.
21
+ export const supportedStoreVersions = ["1.0.0-unaudited"];
22
+ export const supportedWorldVersions = ["1.0.0-unaudited"];
23
+
24
+ export type WorldDeploy = {
25
+ readonly address: Address;
26
+ readonly worldVersion: string;
27
+ readonly storeVersion: string;
28
+ /** Block number where the world was deployed */
29
+ readonly deployBlock: bigint;
30
+ /**
31
+ * Block number at the time of fetching world deploy.
32
+ * We use this block number when requesting data from the chain to align chain state
33
+ * with the same block during the introspection steps of the deploy.
34
+ */
35
+ readonly stateBlock: bigint;
36
+ };
37
+
38
+ export type WorldFunction = {
39
+ readonly signature: string;
40
+ readonly selector: Hex;
41
+ readonly systemId: Hex;
42
+ readonly systemFunctionSignature: string;
43
+ readonly systemFunctionSelector: Hex;
44
+ };
45
+
46
+ export type DeterministicContract = {
47
+ readonly address: Address;
48
+ readonly bytecode: Hex;
49
+ readonly abi: Abi;
50
+ };
51
+
52
+ export type System = DeterministicContract & {
53
+ readonly namespace: string;
54
+ readonly name: string;
55
+ readonly systemId: Hex;
56
+ readonly allowAll: boolean;
57
+ readonly allowedAddresses: readonly Hex[];
58
+ readonly functions: readonly WorldFunction[];
59
+ };
60
+
61
+ export type Module = DeterministicContract & {
62
+ readonly name: string;
63
+ readonly installAsRoot: boolean;
64
+ readonly installData: Hex; // TODO: figure out better naming for this
65
+ };
66
+
67
+ export type ConfigInput = StoreConfig & WorldConfig;
68
+ export type Config<config extends ConfigInput> = {
69
+ readonly tables: Tables<config>;
70
+ readonly systems: readonly System[];
71
+ readonly modules: readonly Module[];
72
+ };
@@ -0,0 +1,68 @@
1
+ import { resourceToHex } from "@latticexyz/common";
2
+ import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser";
3
+ import { SchemaAbiType, StaticAbiType } from "@latticexyz/schema-type";
4
+ import { StoreConfig, resolveUserTypes } from "@latticexyz/store";
5
+ import { Hex } from "viem";
6
+
7
+ // TODO: we shouldn't need this file once our config parsing returns nicely formed tables
8
+
9
+ type UserTypes<config extends StoreConfig = StoreConfig> = config["userTypes"];
10
+ // TODO: fix strong enum types and avoid every schema getting `{ [k: string]: "uint8" }`
11
+ // type UserTypes<config extends StoreConfig = StoreConfig> = config["userTypes"] & {
12
+ // [k in keyof config["enums"]]: { internalType: "uint8" };
13
+ // };
14
+
15
+ export type TableKey<
16
+ config extends StoreConfig = StoreConfig,
17
+ table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]]
18
+ > = `${config["namespace"]}_${table["name"]}`;
19
+
20
+ export type Table<
21
+ config extends StoreConfig = StoreConfig,
22
+ table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]]
23
+ > = {
24
+ readonly namespace: config["namespace"];
25
+ readonly name: table["name"];
26
+ readonly tableId: Hex;
27
+ readonly keySchema: table["keySchema"] extends KeySchema<UserTypes<config>>
28
+ ? KeySchema & {
29
+ readonly [k in keyof table["keySchema"]]: UserTypes<config>[table["keySchema"][k]]["internalType"] extends StaticAbiType
30
+ ? UserTypes<config>[table["keySchema"][k]]["internalType"]
31
+ : table["keySchema"][k];
32
+ }
33
+ : KeySchema;
34
+ readonly valueSchema: table["valueSchema"] extends ValueSchema<UserTypes<config>>
35
+ ? {
36
+ readonly [k in keyof table["valueSchema"]]: UserTypes<config>[table["valueSchema"][k]]["internalType"] extends SchemaAbiType
37
+ ? UserTypes<config>[table["valueSchema"][k]]["internalType"]
38
+ : table["valueSchema"][k];
39
+ }
40
+ : ValueSchema;
41
+ };
42
+
43
+ export type Tables<config extends StoreConfig = StoreConfig> = {
44
+ readonly [k in keyof config["tables"] as TableKey<config, config["tables"][k]>]: Table<config, config["tables"][k]>;
45
+ };
46
+
47
+ export function configToTables<config extends StoreConfig>(config: config): Tables<config> {
48
+ const userTypes = {
49
+ ...config.userTypes,
50
+ ...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)),
51
+ };
52
+ return Object.fromEntries(
53
+ Object.entries(config.tables).map(([tableName, table]) => [
54
+ `${config.namespace}_${tableName}` satisfies TableKey<config, config["tables"][keyof config["tables"]]>,
55
+ {
56
+ namespace: config.namespace,
57
+ name: table.name,
58
+ tableId: resourceToHex({
59
+ type: table.offchainOnly ? "offchainTable" : "table",
60
+ namespace: config.namespace,
61
+ name: table.name,
62
+ }),
63
+ keySchema: resolveUserTypes(table.keySchema, userTypes) as any,
64
+ valueSchema: resolveUserTypes(table.valueSchema, userTypes) as any,
65
+ } satisfies Table<config, config["tables"][keyof config["tables"]]>,
66
+ ])
67
+ ) as Tables<config>;
68
+ }
@@ -0,0 +1,9 @@
1
+ Files generated by
2
+
3
+ ```
4
+ git clone https://github.com/Arachnid/deterministic-deployment-proxy.git
5
+ cd deterministic-deployment-proxy
6
+ git checkout b3bb19c
7
+ npm install
8
+ npm run build
9
+ ```
@@ -0,0 +1,7 @@
1
+ {
2
+ "gasPrice": 100000000000,
3
+ "gasLimit": 100000,
4
+ "signerAddress": "3fab184622dc19b6109349b94811493bf2a45362",
5
+ "transaction": "f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222",
6
+ "address": "4e59b44847b379578588920ca78fbf26c0b4956c"
7
+ }
@@ -0,0 +1,3 @@
1
+ import { debug as parentDebug } from "../debug";
2
+
3
+ export const debug = parentDebug.extend("deploy");
@@ -0,0 +1,108 @@
1
+ import { Account, Address, Chain, Client, Transport, getAddress } from "viem";
2
+ import { ensureDeployer } from "./ensureDeployer";
3
+ import { deployWorld } from "./deployWorld";
4
+ import { ensureTables } from "./ensureTables";
5
+ import { Config, ConfigInput, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common";
6
+ import { ensureSystems } from "./ensureSystems";
7
+ import { waitForTransactionReceipt } from "viem/actions";
8
+ import { getWorldDeploy } from "./getWorldDeploy";
9
+ import { ensureFunctions } from "./ensureFunctions";
10
+ import { ensureModules } from "./ensureModules";
11
+ import { Table } from "./configToTables";
12
+ import { assertNamespaceOwner } from "./assertNamespaceOwner";
13
+ import { debug } from "./debug";
14
+ import { resourceLabel } from "./resourceLabel";
15
+ import { ensureContract } from "./ensureContract";
16
+ import { uniqueBy } from "@latticexyz/common/utils";
17
+ import { ensureContractsDeployed } from "./ensureContractsDeployed";
18
+ import { coreModuleBytecode, worldFactoryBytecode } from "./ensureWorldFactory";
19
+
20
+ type DeployOptions<configInput extends ConfigInput> = {
21
+ client: Client<Transport, Chain | undefined, Account>;
22
+ config: Config<configInput>;
23
+ worldAddress?: Address;
24
+ };
25
+
26
+ /**
27
+ * Given a viem client and MUD config, we attempt to introspect the world
28
+ * (or deploy a new one if no world address is provided) and do the minimal
29
+ * amount of work to make the world match the config (e.g. deploy new tables,
30
+ * replace systems, etc.)
31
+ */
32
+ export async function deploy<configInput extends ConfigInput>({
33
+ client,
34
+ config,
35
+ worldAddress: existingWorldAddress,
36
+ }: DeployOptions<configInput>): Promise<WorldDeploy> {
37
+ const tables = Object.values(config.tables) as Table[];
38
+ const systems = Object.values(config.systems);
39
+
40
+ await ensureDeployer(client);
41
+
42
+ // deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable.
43
+ await ensureContractsDeployed({
44
+ client,
45
+ contracts: [
46
+ { bytecode: coreModuleBytecode, label: "core module" },
47
+ { bytecode: worldFactoryBytecode, label: "world factory" },
48
+ ...uniqueBy(systems, (system) => getAddress(system.address)).map((system) => ({
49
+ bytecode: system.bytecode,
50
+ label: `${resourceLabel(system)} system`,
51
+ })),
52
+ ...uniqueBy(config.modules, (mod) => getAddress(mod.address)).map((mod) => ({
53
+ bytecode: mod.bytecode,
54
+ label: `${mod.name} module`,
55
+ })),
56
+ ],
57
+ });
58
+
59
+ const worldDeploy = existingWorldAddress
60
+ ? await getWorldDeploy(client, existingWorldAddress)
61
+ : await deployWorld(client);
62
+
63
+ if (!supportedStoreVersions.includes(worldDeploy.storeVersion)) {
64
+ throw new Error(`Unsupported Store version: ${worldDeploy.storeVersion}`);
65
+ }
66
+ if (!supportedWorldVersions.includes(worldDeploy.worldVersion)) {
67
+ throw new Error(`Unsupported World version: ${worldDeploy.worldVersion}`);
68
+ }
69
+
70
+ await assertNamespaceOwner({
71
+ client,
72
+ worldDeploy,
73
+ resourceIds: [...tables.map((table) => table.tableId), ...systems.map((system) => system.systemId)],
74
+ });
75
+
76
+ const tableTxs = await ensureTables({
77
+ client,
78
+ worldDeploy,
79
+ tables,
80
+ });
81
+ const systemTxs = await ensureSystems({
82
+ client,
83
+ worldDeploy,
84
+ systems,
85
+ });
86
+ const functionTxs = await ensureFunctions({
87
+ client,
88
+ worldDeploy,
89
+ functions: systems.flatMap((system) => system.functions),
90
+ });
91
+ const moduleTxs = await ensureModules({
92
+ client,
93
+ worldDeploy,
94
+ modules: config.modules,
95
+ });
96
+
97
+ const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs];
98
+
99
+ // wait for each tx separately/serially, because parallelizing results in RPC errors
100
+ debug("waiting for all transactions to confirm");
101
+ for (const tx of txs) {
102
+ await waitForTransactionReceipt(client, { hash: tx });
103
+ // TODO: throw if there was a revert?
104
+ }
105
+
106
+ debug("deploy complete");
107
+ return worldDeploy;
108
+ }
@@ -0,0 +1,33 @@
1
+ import { Account, Chain, Client, Log, Transport } from "viem";
2
+ import { waitForTransactionReceipt } from "viem/actions";
3
+ import { ensureWorldFactory, worldFactory } from "./ensureWorldFactory";
4
+ import WorldFactoryAbi from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.abi.json" assert { type: "json" };
5
+ import { writeContract } from "@latticexyz/common";
6
+ import { debug } from "./debug";
7
+ import { logsToWorldDeploy } from "./logsToWorldDeploy";
8
+ import { WorldDeploy } from "./common";
9
+
10
+ export async function deployWorld(client: Client<Transport, Chain | undefined, Account>): Promise<WorldDeploy> {
11
+ await ensureWorldFactory(client);
12
+
13
+ debug("deploying world");
14
+ const tx = await writeContract(client, {
15
+ chain: client.chain ?? null,
16
+ address: worldFactory,
17
+ abi: WorldFactoryAbi,
18
+ functionName: "deployWorld",
19
+ });
20
+
21
+ debug("waiting for world deploy");
22
+ const receipt = await waitForTransactionReceipt(client, { hash: tx });
23
+ if (receipt.status !== "success") {
24
+ console.error("world deploy failed", receipt);
25
+ throw new Error("world deploy failed");
26
+ }
27
+
28
+ // TODO: remove type casting once https://github.com/wagmi-dev/viem/pull/1330 is merged
29
+ const deploy = logsToWorldDeploy(receipt.logs.map((log) => log as Log<bigint, number, false>));
30
+ debug("deployed world to", deploy.address, "at block", deploy.deployBlock);
31
+
32
+ return { ...deploy, stateBlock: deploy.deployBlock };
33
+ }
@@ -0,0 +1,49 @@
1
+ import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex } from "viem";
2
+ import { getBytecode } from "viem/actions";
3
+ import { deployer } from "./ensureDeployer";
4
+ import { salt } from "./common";
5
+ import { sendTransaction } from "@latticexyz/common";
6
+ import { debug } from "./debug";
7
+ import pRetry from "p-retry";
8
+ import { wait } from "@latticexyz/common/utils";
9
+
10
+ export type Contract = {
11
+ bytecode: Hex;
12
+ label?: string;
13
+ };
14
+
15
+ export async function ensureContract({
16
+ client,
17
+ bytecode,
18
+ label = "contract",
19
+ }: {
20
+ readonly client: Client<Transport, Chain | undefined, Account>;
21
+ } & Contract): Promise<readonly Hex[]> {
22
+ const address = getCreate2Address({ from: deployer, salt, bytecode });
23
+
24
+ const contractCode = await getBytecode(client, { address, blockTag: "pending" });
25
+ if (contractCode) {
26
+ debug("found", label, "at", address);
27
+ return [];
28
+ }
29
+
30
+ debug("deploying", label, "at", address);
31
+ return [
32
+ await pRetry(
33
+ () =>
34
+ sendTransaction(client, {
35
+ chain: client.chain ?? null,
36
+ to: deployer,
37
+ data: concatHex([salt, bytecode]),
38
+ }),
39
+ {
40
+ retries: 3,
41
+ onFailedAttempt: async (error) => {
42
+ const delay = error.attemptNumber * 500;
43
+ debug(`failed to deploy ${label}, retrying in ${delay}ms...`);
44
+ await wait(delay);
45
+ },
46
+ }
47
+ ),
48
+ ];
49
+ }
@@ -0,0 +1,25 @@
1
+ import { Client, Transport, Chain, Account, Hex } from "viem";
2
+ import { waitForTransactionReceipt } from "viem/actions";
3
+ import { debug } from "./debug";
4
+ import { Contract, ensureContract } from "./ensureContract";
5
+
6
+ export async function ensureContractsDeployed({
7
+ client,
8
+ contracts,
9
+ }: {
10
+ readonly client: Client<Transport, Chain | undefined, Account>;
11
+ readonly contracts: readonly Contract[];
12
+ }): Promise<readonly Hex[]> {
13
+ const txs = (await Promise.all(contracts.map((contract) => ensureContract({ client, ...contract })))).flat();
14
+
15
+ if (txs.length) {
16
+ debug("waiting for contracts");
17
+ // wait for each tx separately/serially, because parallelizing results in RPC errors
18
+ for (const tx of txs) {
19
+ await waitForTransactionReceipt(client, { hash: tx });
20
+ // TODO: throw if there was a revert?
21
+ }
22
+ }
23
+
24
+ return txs;
25
+ }
@@ -0,0 +1,36 @@
1
+ import { Account, Chain, Client, Transport } from "viem";
2
+ import { getBytecode, sendRawTransaction, sendTransaction, waitForTransactionReceipt } from "viem/actions";
3
+ import deployment from "./create2/deployment.json";
4
+ import { debug } from "./debug";
5
+
6
+ export const deployer = `0x${deployment.address}` as const;
7
+
8
+ export async function ensureDeployer(client: Client<Transport, Chain | undefined, Account>): Promise<void> {
9
+ const bytecode = await getBytecode(client, { address: deployer });
10
+ if (bytecode) {
11
+ debug("found create2 deployer at", deployer);
12
+ return;
13
+ }
14
+
15
+ // send gas to signer
16
+ debug("sending gas for create2 deployer to signer at", deployment.signerAddress);
17
+ const gasTx = await sendTransaction(client, {
18
+ chain: client.chain ?? null,
19
+ to: `0x${deployment.signerAddress}`,
20
+ value: BigInt(deployment.gasLimit) * BigInt(deployment.gasPrice),
21
+ });
22
+ const gasReceipt = await waitForTransactionReceipt(client, { hash: gasTx });
23
+ if (gasReceipt.status !== "success") {
24
+ console.error("failed to send gas to deployer signer", gasReceipt);
25
+ throw new Error("failed to send gas to deployer signer");
26
+ }
27
+
28
+ // deploy the deployer
29
+ debug("deploying create2 deployer at", deployer);
30
+ const deployTx = await sendRawTransaction(client, { serializedTransaction: `0x${deployment.transaction}` });
31
+ const deployReceipt = await waitForTransactionReceipt(client, { hash: deployTx });
32
+ if (deployReceipt.contractAddress !== deployer) {
33
+ console.error("unexpected contract address for deployer", deployReceipt);
34
+ throw new Error("unexpected contract address for deployer");
35
+ }
36
+ }