@latticexyz/cli 1.41.0 → 1.41.1-alpha.41

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 (53) hide show
  1. package/dist/{chunk-GR245KYP.js → chunk-J4DJQNIC.js} +679 -133
  2. package/dist/chunk-O57QENJ6.js +23039 -0
  3. package/dist/config/index.d.ts +610 -296
  4. package/dist/config/index.js +49 -26
  5. package/dist/index.d.ts +2 -110
  6. package/dist/index.js +9 -50
  7. package/dist/mud.js +937 -57
  8. package/dist/utils/index.d.ts +92 -4
  9. package/dist/utils/index.js +2 -3
  10. package/package.json +10 -9
  11. package/src/commands/deploy-v2.ts +11 -15
  12. package/src/commands/index.ts +2 -0
  13. package/src/commands/worldgen.ts +55 -0
  14. package/src/config/commonSchemas.ts +11 -13
  15. package/src/config/dynamicResolution.ts +49 -0
  16. package/src/config/index.ts +15 -3
  17. package/src/config/loadStoreConfig.ts +1 -1
  18. package/src/config/parseStoreConfig.test-d.ts +31 -5
  19. package/src/config/parseStoreConfig.ts +218 -78
  20. package/src/config/validation.ts +25 -0
  21. package/src/config/world/index.ts +4 -0
  22. package/src/config/{loadWorldConfig.test-d.ts → world/loadWorldConfig.test-d.ts} +3 -3
  23. package/src/config/world/loadWorldConfig.ts +26 -0
  24. package/src/config/world/parseWorldConfig.ts +55 -0
  25. package/src/config/world/resolveWorldConfig.ts +80 -0
  26. package/src/config/world/userTypes.ts +72 -0
  27. package/src/index.ts +4 -6
  28. package/src/render-solidity/common.ts +51 -6
  29. package/src/render-solidity/field.ts +40 -44
  30. package/src/render-solidity/index.ts +5 -1
  31. package/src/render-solidity/record.ts +56 -73
  32. package/src/render-solidity/renderSystemInterface.ts +31 -0
  33. package/src/render-solidity/renderTable.ts +98 -70
  34. package/src/render-solidity/renderTypeHelpers.ts +99 -0
  35. package/src/render-solidity/renderTypesFromConfig.ts +2 -2
  36. package/src/render-solidity/renderWorld.ts +24 -0
  37. package/src/render-solidity/{renderTablesFromConfig.ts → tableOptions.ts} +28 -30
  38. package/src/render-solidity/tablegen.ts +20 -22
  39. package/src/render-solidity/types.ts +39 -5
  40. package/src/render-solidity/userType.ts +80 -48
  41. package/src/render-solidity/worldgen.ts +60 -0
  42. package/src/utils/contractToInterface.ts +130 -0
  43. package/src/utils/deploy-v2.ts +268 -101
  44. package/src/utils/formatAndWrite.ts +12 -0
  45. package/src/utils/getChainId.ts +10 -0
  46. package/src/utils/typeUtils.ts +17 -0
  47. package/dist/chunk-AER7UDD4.js +0 -0
  48. package/dist/chunk-XRS7KWBZ.js +0 -547
  49. package/dist/chunk-YZATC2M3.js +0 -397
  50. package/dist/chunk-ZYDMYSTH.js +0 -1178
  51. package/dist/deploy-v2-b7b3207d.d.ts +0 -92
  52. package/src/config/loadWorldConfig.ts +0 -178
  53. package/src/constants.ts +0 -1
@@ -0,0 +1,130 @@
1
+ import { parse, visit } from "@solidity-parser/parser";
2
+ import { TypeName, VariableDeclaration } from "@solidity-parser/parser/dist/src/ast-types.js";
3
+ import { RenderSystemInterfaceFunction } from "../render-solidity/types.js";
4
+ import { MUDError } from "./errors.js";
5
+
6
+ /**
7
+ * Parse the contract data to get the functions necessary to generate an interface,
8
+ * and symbols to import from the original contract.
9
+ * @param data contents of a file with the solidity contract
10
+ * @param contractName name of the contract
11
+ * @returns interface data
12
+ */
13
+ export function contractToInterface(data: string, contractName: string) {
14
+ const ast = parse(data);
15
+
16
+ let withContract = false;
17
+ let symbols: string[] = [];
18
+ const functions: RenderSystemInterfaceFunction[] = [];
19
+
20
+ visit(ast, {
21
+ ContractDefinition({ name }) {
22
+ if (name === contractName) {
23
+ withContract = true;
24
+ }
25
+ },
26
+ FunctionDefinition(
27
+ { name, visibility, parameters, returnParameters, isConstructor, isFallback, isReceiveEther },
28
+ parent
29
+ ) {
30
+ if (parent !== undefined && parent.type === "ContractDefinition" && parent.name === contractName) {
31
+ try {
32
+ // skip constructor and fallbacks
33
+ if (isConstructor || isFallback || isReceiveEther) return;
34
+ // forbid default visibility (this check might be unnecessary, modern solidity already disallows this)
35
+ if (visibility === "default") throw new MUDError(`Visibility is not specified`);
36
+
37
+ if (visibility === "external" || visibility === "public") {
38
+ functions.push({
39
+ name: name === null ? "" : name,
40
+ parameters: parameters.map(parseParameter),
41
+ returnParameters: returnParameters === null ? [] : returnParameters.map(parseParameter),
42
+ });
43
+
44
+ for (const { typeName } of parameters.concat(returnParameters ?? [])) {
45
+ symbols = symbols.concat(typeNameToExternalSymbols(typeName));
46
+ }
47
+ }
48
+ } catch (error: unknown) {
49
+ if (error instanceof MUDError) {
50
+ error.message = `Function "${name}" in contract "${contractName}": ${error.message}`;
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+ },
56
+ });
57
+
58
+ if (!withContract) {
59
+ throw new MUDError(`Contract not found: ${contractName}`);
60
+ }
61
+
62
+ return {
63
+ functions,
64
+ symbols,
65
+ };
66
+ }
67
+
68
+ function parseParameter({ name, typeName, storageLocation }: VariableDeclaration): string {
69
+ let typedNameWithLocation = "";
70
+
71
+ const { name: flattenedTypeName, stateMutability } = flattenTypeName(typeName);
72
+ // type name (e.g. uint256)
73
+ typedNameWithLocation += flattenedTypeName;
74
+ // optional mutability (e.g. address payable)
75
+ if (stateMutability !== null) {
76
+ typedNameWithLocation += ` ${stateMutability}`;
77
+ }
78
+ // location, when relevant (e.g. string memory)
79
+ if (storageLocation !== null) {
80
+ typedNameWithLocation += ` ${storageLocation}`;
81
+ }
82
+ // optional variable name
83
+ if (name !== null) {
84
+ typedNameWithLocation += ` ${name}`;
85
+ }
86
+
87
+ return typedNameWithLocation;
88
+ }
89
+
90
+ function flattenTypeName(typeName: TypeName | null): { name: string; stateMutability: string | null } {
91
+ if (typeName === null) {
92
+ return {
93
+ name: "",
94
+ stateMutability: null,
95
+ };
96
+ }
97
+ if (typeName.type === "ElementaryTypeName") {
98
+ return {
99
+ name: typeName.name,
100
+ stateMutability: typeName.stateMutability,
101
+ };
102
+ } else if (typeName.type === "UserDefinedTypeName") {
103
+ return {
104
+ name: typeName.namePath,
105
+ stateMutability: null,
106
+ };
107
+ } else if (typeName.type === "ArrayTypeName") {
108
+ const { name, stateMutability } = flattenTypeName(typeName.baseTypeName);
109
+ return {
110
+ name: `${name}[]`,
111
+ stateMutability,
112
+ };
113
+ } else {
114
+ // TODO function types are unsupported but could be useful
115
+ throw new MUDError(`Invalid typeName.type ${typeName.type}`);
116
+ }
117
+ }
118
+
119
+ // Get symbols that need to be imported for given typeName
120
+ function typeNameToExternalSymbols(typeName: TypeName | null): string[] {
121
+ if (typeName?.type === "UserDefinedTypeName") {
122
+ // split is needed to get a library, if types are internal to it
123
+ const symbol = typeName.namePath.split(".")[0];
124
+ return [symbol];
125
+ } else if (typeName?.type === "ArrayTypeName") {
126
+ return typeNameToExternalSymbols(typeName.baseTypeName);
127
+ } else {
128
+ return [];
129
+ }
130
+ }
@@ -1,33 +1,39 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import path from "path";
3
- import { MUDConfig } from "../config/index.js";
3
+ import { MUDConfig, resolveWithContext } from "../config/index.js";
4
4
  import { MUDError } from "./errors.js";
5
5
  import { getOutDirectory, getScriptDirectory, cast, forge } from "./foundry.js";
6
6
  import { BigNumber, ContractInterface, ethers } from "ethers";
7
- import { World } from "@latticexyz/world/types/ethers-contracts/World.js";
8
- import { abi as WorldABI, bytecode as WorldBytecode } from "@latticexyz/world/abi/World.json";
7
+ import { IWorld } from "@latticexyz/world/types/ethers-contracts/IWorld.js";
9
8
  import { ArgumentsType } from "vitest";
10
9
  import chalk from "chalk";
11
10
  import { encodeSchema } from "@latticexyz/schema-type";
12
- import { resolveSchemaOrUserTypeSimple } from "../render-solidity/userType.js";
11
+ import { resolveAbiOrUserType } from "../render-solidity/userType.js";
12
+ import { defaultAbiCoder as abi, Fragment } from "ethers/lib/utils.js";
13
+
14
+ import WorldData from "@latticexyz/world/abi/World.json" assert { type: "json" };
15
+ import IWorldData from "@latticexyz/world/abi/IWorld.json" assert { type: "json" };
16
+ import CoreModuleData from "@latticexyz/world/abi/CoreModule.json" assert { type: "json" };
17
+ import RegistrationModuleData from "@latticexyz/world/abi/RegistrationModule.json" assert { type: "json" };
18
+ import KeysWithValueModuleData from "@latticexyz/world/abi/KeysWithValueModule.json" assert { type: "json" };
13
19
 
14
20
  export interface DeployConfig {
15
21
  profile?: string;
16
22
  rpc: string;
17
23
  privateKey: string;
18
24
  priorityFeeMultiplier: number;
25
+ debug?: boolean;
19
26
  }
20
27
 
21
28
  export interface DeploymentInfo {
22
29
  blockNumber: number;
23
30
  worldAddress: string;
24
- rpc: string;
25
31
  }
26
32
 
27
33
  export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig): Promise<DeploymentInfo> {
28
34
  const startTime = Date.now();
29
- const { worldContractName, baseRoute, postDeployScript } = mudConfig;
30
- const { profile, rpc, privateKey, priorityFeeMultiplier } = deployConfig;
35
+ const { worldContractName, namespace, postDeployScript } = mudConfig;
36
+ const { profile, rpc, privateKey, priorityFeeMultiplier, debug } = deployConfig;
31
37
  const forgeOutDirectory = await getOutDirectory(profile);
32
38
 
33
39
  // Set up signer for deployment
@@ -44,116 +50,230 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
44
50
  setInternalFeePerGas(priorityFeeMultiplier);
45
51
 
46
52
  // Catch all to await any promises before exiting the script
47
- const promises: Promise<unknown>[] = [];
53
+ let promises: Promise<unknown>[] = [];
48
54
 
49
55
  // Get block number before deploying
50
56
  const blockNumber = Number(await cast(["block-number", "--rpc-url", rpc], { profile }));
51
57
  console.log("Start deployment at block", blockNumber);
52
58
 
53
- // Deploy all contracts (World and systems)
54
- const contractPromises = Object.keys(mudConfig.systems).reduce<Record<string, Promise<string>>>(
55
- (acc, systemName) => {
56
- acc[systemName] = deployContractByName(systemName);
59
+ // Deploy World
60
+ const worldPromise = {
61
+ World: worldContractName
62
+ ? deployContractByName(worldContractName)
63
+ : deployContract(IWorldData.abi, WorldData.bytecode, "World"),
64
+ };
65
+
66
+ // Deploy Systems
67
+ const systemPromises = Object.keys(mudConfig.systems).reduce<Record<string, Promise<string>>>((acc, systemName) => {
68
+ acc[systemName] = deployContractByName(systemName);
69
+ return acc;
70
+ }, {});
71
+
72
+ // Deploy default World modules
73
+ const defaultModules: Record<string, Promise<string>> = {
74
+ // TODO: these only need to be deployed once per chain, add a check if they exist already
75
+ CoreModule: deployContract(CoreModuleData.abi, CoreModuleData.bytecode, "CoreModule"),
76
+ RegistrationModule: deployContract(
77
+ RegistrationModuleData.abi,
78
+ RegistrationModuleData.bytecode,
79
+ "RegistrationModule"
80
+ ),
81
+ KeysWithValueModule: deployContract(
82
+ KeysWithValueModuleData.abi,
83
+ KeysWithValueModuleData.bytecode,
84
+ "KeysWithValueModule"
85
+ ),
86
+ };
87
+
88
+ // Deploy user Modules
89
+ const modulePromises = mudConfig.modules
90
+ .filter((module) => !defaultModules[module.name]) // Only deploy user modules here, not default modules
91
+ .reduce<Record<string, Promise<string>>>((acc, module) => {
92
+ acc[module.name] = deployContractByName(module.name);
57
93
  return acc;
58
- },
59
- {
60
- World: worldContractName
61
- ? deployContractByName(worldContractName)
62
- : deployContract(WorldABI, WorldBytecode, "World"),
63
- }
64
- );
94
+ }, defaultModules);
95
+
96
+ // Combine all contracts into one object
97
+ const contractPromises: Record<string, Promise<string>> = { ...worldPromise, ...systemPromises, ...modulePromises };
65
98
 
66
99
  // Create World contract instance from deployed address
67
- const WorldContract = new ethers.Contract(await contractPromises.World, WorldABI, signer) as World;
100
+ const WorldContract = new ethers.Contract(await contractPromises.World, IWorldData.abi, signer) as IWorld;
101
+
102
+ // Install core Modules
103
+ console.log(chalk.blue("Installing core World modules"));
104
+ await fastTxExecute(WorldContract, "installRootModule", [await modulePromises.CoreModule, "0x"]);
105
+ await fastTxExecute(WorldContract, "installRootModule", [await modulePromises.RegistrationModule, "0x"]);
106
+ console.log(chalk.green("Installed core World modules"));
68
107
 
69
- // Register baseRoute
70
- if (baseRoute) await fastTxExecute(WorldContract, "registerRoute", ["", baseRoute]);
108
+ // Register namespace
109
+ if (namespace) await fastTxExecute(WorldContract, "registerNamespace", [toBytes16(namespace)]);
71
110
 
72
111
  // Register tables
73
- promises.push(
74
- ...Object.entries(mudConfig.tables).map(async ([tableName, tableConfig]) => {
75
- console.log(chalk.blue("Registering table", tableName, "at", baseRoute + tableConfig.route));
112
+ const tableIds: { [tableName: string]: Uint8Array } = {};
113
+ promises = [
114
+ ...promises,
115
+ ...Object.entries(mudConfig.tables).map(async ([tableName, { fileSelector, schema, primaryKeys }]) => {
116
+ console.log(chalk.blue(`Registering table ${tableName} at ${namespace}/${fileSelector}`));
76
117
 
77
- // Register nested route
78
- const routeFragments = tableConfig.route.substring(1).split("/"); // Split route into fragments (skip leading slash)
79
- const lastRouteFragment = toRoute(routeFragments.pop()); // Register last fragment separately as part of call to registerTable
80
- await registerNestedRoute(WorldContract, baseRoute, routeFragments);
118
+ // Store the tableId for later use
119
+ tableIds[tableName] = toResourceSelector(namespace, fileSelector);
81
120
 
82
121
  // Register table
83
- const tableBaseRoute = toRoute(baseRoute, ...routeFragments);
122
+ const schemaTypes = Object.values(schema).map((abiOrUserType) => {
123
+ const { schemaType } = resolveAbiOrUserType(abiOrUserType, mudConfig);
124
+ return schemaType;
125
+ });
84
126
 
85
- const schemaTypes = Object.values(tableConfig.schema).map((schemaOrUserType) => {
86
- return resolveSchemaOrUserTypeSimple(schemaOrUserType, mudConfig.userTypes);
127
+ const keyTypes = Object.values(primaryKeys).map((abiOrUserType) => {
128
+ const { schemaType } = resolveAbiOrUserType(abiOrUserType, mudConfig);
129
+ return schemaType;
87
130
  });
88
131
 
89
132
  await fastTxExecute(WorldContract, "registerTable", [
90
- tableBaseRoute,
91
- lastRouteFragment,
133
+ toBytes16(namespace),
134
+ toBytes16(fileSelector),
92
135
  encodeSchema(schemaTypes),
136
+ encodeSchema(keyTypes),
93
137
  ]);
94
138
 
95
139
  // Register table metadata
96
- await fastTxExecute(WorldContract, "setMetadata(string,string,string[])", [
97
- baseRoute + tableConfig.route,
140
+ await fastTxExecute(WorldContract, "setMetadata(bytes16,bytes16,string,string[])", [
141
+ toBytes16(namespace),
142
+ toBytes16(fileSelector),
98
143
  tableName,
99
- Object.keys(tableConfig.schema),
144
+ Object.keys(schema),
100
145
  ]);
101
146
 
102
- console.log(chalk.green("Registered table", tableName, "at", baseRoute + tableConfig.route));
103
- })
104
- );
147
+ console.log(chalk.green(`Registered table ${tableName} at ${fileSelector}`));
148
+ }),
149
+ ];
105
150
 
106
151
  // Register systems (using forEach instead of for..of to avoid blocking on async calls)
107
- promises.push(
108
- ...Object.entries(mudConfig.systems).map(async ([systemName, systemConfig]) => {
109
- console.log(chalk.blue("Registering system", systemName, "at", baseRoute + systemConfig.route));
110
-
111
- // Register system route
112
- const routeFragments = systemConfig.route.substring(1).split("/"); // Split route into fragments (skip leading slash)
113
- const lastRouteFragment = toRoute(routeFragments.pop()); // Register last fragment as part of call to registerSystem
114
- const systemBaseRoute = toRoute(baseRoute, ...routeFragments);
115
- await registerNestedRoute(WorldContract, baseRoute, routeFragments);
116
-
117
- // Register system at route
118
- await fastTxExecute(WorldContract, "registerSystem", [
119
- systemBaseRoute,
120
- lastRouteFragment,
121
- await contractPromises[systemName],
122
- systemConfig.openAccess,
123
- ]);
124
-
125
- console.log(chalk.green("Registered system", systemName, "at", baseRoute + systemConfig.route));
126
- })
127
- );
152
+ promises = [
153
+ ...promises,
154
+ ...Object.entries(mudConfig.systems).map(
155
+ async ([systemName, { fileSelector, openAccess, registerFunctionSelectors }]) => {
156
+ // Register system at route
157
+ console.log(chalk.blue(`Registering system ${systemName} at ${namespace}/${fileSelector}`));
158
+ await fastTxExecute(WorldContract, "registerSystem", [
159
+ toBytes16(namespace),
160
+ toBytes16(fileSelector),
161
+ await contractPromises[systemName],
162
+ openAccess,
163
+ ]);
164
+ console.log(chalk.green(`Registered system ${systemName} at ${namespace}/${fileSelector}`));
165
+
166
+ // Register function selectors for the system
167
+ if (registerFunctionSelectors) {
168
+ const functionSignatures: FunctionSignature[] = await loadFunctionSignatures(systemName);
169
+ const isRoot = namespace === "";
170
+ // Using Promise.all to avoid blocking on async calls
171
+ await Promise.all(
172
+ functionSignatures.map(async ({ functionName, functionArgs }) => {
173
+ const functionSignature = isRoot
174
+ ? functionName + functionArgs
175
+ : `${namespace}_${fileSelector}_${functionName}${functionArgs}`;
176
+
177
+ console.log(chalk.blue(`Registering function "${functionSignature}"`));
178
+ if (isRoot) {
179
+ const worldFunctionSelector = toFunctionSelector(
180
+ functionSignature === ""
181
+ ? { functionName: systemName, functionArgs } // Register the system's fallback function as `<systemName>(<args>)`
182
+ : { functionName, functionArgs }
183
+ );
184
+ const systemFunctionSelector = toFunctionSelector({ functionName, functionArgs });
185
+ await fastTxExecute(WorldContract, "registerRootFunctionSelector", [
186
+ toBytes16(namespace),
187
+ toBytes16(fileSelector),
188
+ worldFunctionSelector,
189
+ systemFunctionSelector,
190
+ ]);
191
+ } else {
192
+ await fastTxExecute(WorldContract, "registerFunctionSelector", [
193
+ toBytes16(namespace),
194
+ toBytes16(fileSelector),
195
+ functionName,
196
+ functionArgs,
197
+ ]);
198
+ }
199
+ console.log(chalk.green(`Registered function "${functionSignature}"`));
200
+ })
201
+ );
202
+ }
203
+ }
204
+ ),
205
+ ];
128
206
 
129
- // Wait for routes to be registered before granting access to them
130
- await Promise.all(promises);
207
+ // Wait for resources to be registered before granting access to them
208
+ await Promise.all(promises); // ----------------------------------------------------------------------------------------------
209
+ promises = [];
131
210
 
132
211
  // Grant access to systems
133
- for (const [systemName, systemConfig] of Object.entries(mudConfig.systems)) {
134
- const systemRoute = baseRoute + systemConfig.route;
212
+ for (const [systemName, { fileSelector, accessListAddresses, accessListSystems }] of Object.entries(
213
+ mudConfig.systems
214
+ )) {
215
+ const resourceSelector = `${namespace}/${fileSelector}`;
135
216
 
136
217
  // Grant access to addresses
137
- promises.push(
138
- ...systemConfig.accessListAddresses.map(async (address) => {
139
- console.log(chalk.blue(`Grant ${address} access to ${systemName} (${systemRoute})`));
140
- await fastTxExecute(WorldContract, "grantAccess", [systemRoute, address]);
141
- console.log(chalk.green(`Granted ${address} access to ${systemName} (${systemRoute})`));
142
- })
143
- );
218
+ promises = [
219
+ ...promises,
220
+ ...accessListAddresses.map(async (address) => {
221
+ console.log(chalk.blue(`Grant ${address} access to ${systemName} (${resourceSelector})`));
222
+ await fastTxExecute(WorldContract, "grantAccess(bytes16,bytes16,address)", [
223
+ toBytes16(namespace),
224
+ toBytes16(fileSelector),
225
+ address,
226
+ ]);
227
+ console.log(chalk.green(`Granted ${address} access to ${systemName} (${namespace}/${fileSelector})`));
228
+ }),
229
+ ];
144
230
 
145
231
  // Grant access to other systems
146
- promises.push(
147
- ...systemConfig.accessListSystems.map(async (granteeSystem) => {
148
- console.log(chalk.blue(`Grant ${granteeSystem} access to ${systemName} (${systemRoute})`));
149
- await fastTxExecute(WorldContract, "grantAccess", [systemRoute, await contractPromises[granteeSystem]]);
150
- console.log(chalk.green(`Granted ${granteeSystem} access to ${systemName} (${systemRoute})`));
151
- })
152
- );
232
+ promises = [
233
+ ...promises,
234
+ ...accessListSystems.map(async (granteeSystem) => {
235
+ console.log(chalk.blue(`Grant ${granteeSystem} access to ${systemName} (${resourceSelector})`));
236
+ await fastTxExecute(WorldContract, "grantAccess(bytes16,bytes16,address)", [
237
+ toBytes16(namespace),
238
+ toBytes16(fileSelector),
239
+ await contractPromises[granteeSystem],
240
+ ]);
241
+ console.log(chalk.green(`Granted ${granteeSystem} access to ${systemName} (${resourceSelector})`));
242
+ }),
243
+ ];
153
244
  }
154
245
 
155
- // Await all promises
156
- await Promise.all(promises);
246
+ // Wait for access to be granted before installing modules
247
+ await Promise.all(promises); // ----------------------------------------------------------------------------------------------
248
+ promises = [];
249
+
250
+ // Install modules
251
+ promises = [
252
+ ...promises,
253
+ ...mudConfig.modules.map(async (module) => {
254
+ console.log(chalk.blue(`Installing${module.root ? " root " : " "}module ${module.name}`));
255
+ // Resolve arguments
256
+ const resolvedArgs = await Promise.all(
257
+ module.args.map((arg) => resolveWithContext(arg, { tableIds, systemAddresses: contractPromises }))
258
+ );
259
+ const values = resolvedArgs.map((arg) => arg.value);
260
+ const types = resolvedArgs.map((arg) => arg.type);
261
+ const moduleAddress = await contractPromises[module.name];
262
+ if (!moduleAddress) throw new Error(`Module ${module.name} not found`);
263
+
264
+ // Send transaction to install module
265
+ await fastTxExecute(WorldContract, module.root ? "installRootModule" : "installModule", [
266
+ moduleAddress,
267
+ abi.encode(types, values),
268
+ ]);
269
+
270
+ console.log(chalk.green(`Installed${module.root ? " root " : " "}module ${module.name}`));
271
+ }),
272
+ ];
273
+
274
+ // Await all promises before executing PostDeploy script
275
+ await Promise.all(promises); // ----------------------------------------------------------------------------------------------
276
+ promises = [];
157
277
 
158
278
  // Execute postDeploy forge script
159
279
  const postDeployPath = path.join(await getScriptDirectory(), postDeployScript + ".s.sol");
@@ -181,7 +301,7 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
181
301
 
182
302
  console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
183
303
 
184
- return { worldAddress: await contractPromises.World, blockNumber, rpc };
304
+ return { worldAddress: await contractPromises.World, blockNumber };
185
305
 
186
306
  // ------------------- INTERNAL FUNCTIONS -------------------
187
307
  // (Inlined to avoid having to pass around nonce, signer and forgeOutDir)
@@ -225,6 +345,7 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
225
345
  console.log(chalk.green("Deployed", contractName, "to", address));
226
346
  return address;
227
347
  } catch (error: any) {
348
+ if (debug) console.error(error);
228
349
  if (retryCount === 0 && error?.message.includes("transaction already imported")) {
229
350
  // If the deployment failed because the transaction was already imported,
230
351
  // retry with a higher priority fee
@@ -234,6 +355,8 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
234
355
  throw new MUDError(
235
356
  `Error deploying ${contractName}: invalid bytecode. Note that linking of public libraries is not supported yet, make sure none of your libraries use "external" functions.`
236
357
  );
358
+ } else if (error?.message.includes("CreateContractLimit")) {
359
+ throw new MUDError(`Error deploying ${contractName}: CreateContractLimit exceeded.`);
237
360
  } else throw error;
238
361
  }
239
362
  }
@@ -261,6 +384,18 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
261
384
  // return deployedTo;
262
385
  // }
263
386
 
387
+ async function loadFunctionSignatures(contractName: string): Promise<FunctionSignature[]> {
388
+ const { abi } = await getContractData(contractName);
389
+
390
+ return abi
391
+ .filter((item) => ["fallback", "function"].includes(item.type))
392
+ .map((item) => {
393
+ if (item.type === "fallback") return { functionName: "", functionArgs: "" };
394
+
395
+ return { functionName: item.name, functionArgs: `(${item.inputs.map((arg) => arg.type).join(",")})` };
396
+ });
397
+ }
398
+
264
399
  /**
265
400
  * Only await gas estimation (for speed), only execute if gas estimation succeeds (for safety)
266
401
  */
@@ -281,6 +416,7 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
281
416
  promises.push(txPromise);
282
417
  return txPromise;
283
418
  } catch (error: any) {
419
+ if (debug) console.error(error);
284
420
  if (retryCount === 0 && error?.message.includes("transaction already imported")) {
285
421
  // If the deployment failed because the transaction was already imported,
286
422
  // retry with a higher priority fee
@@ -290,30 +426,17 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
290
426
  }
291
427
  }
292
428
 
293
- async function registerNestedRoute(WorldContract: World, baseRoute: string, registerRouteFragments: string[]) {
294
- // Register nested routes
295
- for (let i = 0; i < registerRouteFragments.length; i++) {
296
- const subRoute = toRoute(registerRouteFragments[i]);
297
- try {
298
- await fastTxExecute(WorldContract, "registerRoute", [baseRoute, subRoute]);
299
- } catch (e) {
300
- // TODO: check if the gas estimation error is due to the route already being registered and ignore it
301
- }
302
- baseRoute += subRoute;
303
- }
304
- }
305
-
306
429
  /**
307
430
  * Load the contract's abi and bytecode from the file system
308
431
  * @param contractName: Name of the contract to load
309
432
  */
310
- async function getContractData(contractName: string): Promise<{ bytecode: string; abi: ContractInterface }> {
433
+ async function getContractData(contractName: string): Promise<{ bytecode: string; abi: Fragment[] }> {
311
434
  let data: any;
312
435
  const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json");
313
436
  try {
314
437
  data = JSON.parse(readFileSync(contractDataPath, "utf8"));
315
438
  } catch (error: any) {
316
- throw new MUDError(`Error reading file at ${contractDataPath}: ${error?.message}`);
439
+ throw new MUDError(`Error reading file at ${contractDataPath}`);
317
440
  }
318
441
 
319
442
  const bytecode = data?.bytecode?.object;
@@ -334,12 +457,56 @@ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig):
334
457
  // Compute maxFeePerGas and maxPriorityFeePerGas like ethers, but allow for a multiplier to allow replacing pending transactions
335
458
  const feeData = await provider.getFeeData();
336
459
  if (!feeData.lastBaseFeePerGas) throw new MUDError("Can not fetch lastBaseFeePerGas from RPC");
337
- maxPriorityFeePerGas = Math.floor(1_500_000_000 * multiplier);
460
+
461
+ // Set the priority fee to 0 for development chains with no base fee, to allow transactions from unfunded wallets
462
+ maxPriorityFeePerGas = feeData.lastBaseFeePerGas.eq(0) ? 0 : Math.floor(1_500_000_000 * multiplier);
338
463
  maxFeePerGas = feeData.lastBaseFeePerGas.mul(2).add(maxPriorityFeePerGas);
339
464
  }
340
465
  }
341
466
 
342
- function toRoute(...routeFragments: (string | undefined)[]): string {
343
- const route = routeFragments.filter((e) => Boolean(e)).join("/");
344
- return route ? `/${route}` : "";
467
+ // TODO: use stringToBytes16 from utils as soon as utils are usable inside cli
468
+ // (see https://github.com/latticexyz/mud/issues/499)
469
+ function toBytes16(input: string) {
470
+ if (input.length > 16) throw new Error("String does not fit into 16 bytes");
471
+
472
+ const result = new Uint8Array(16);
473
+ // Set ascii bytes
474
+ for (let i = 0; i < input.length; i++) {
475
+ result[i] = input.charCodeAt(i);
476
+ }
477
+ // Set the remaining bytes to 0
478
+ for (let i = input.length; i < 16; i++) {
479
+ result[i] = 0;
480
+ }
481
+ return result;
482
+ }
483
+
484
+ // TODO: use TableId from utils as soon as utils are usable inside cli
485
+ // (see https://github.com/latticexyz/mud/issues/499)
486
+ function toResourceSelector(namespace: string, file: string): Uint8Array {
487
+ const namespaceBytes = toBytes16(namespace);
488
+ const fileBytes = toBytes16(file);
489
+ const result = new Uint8Array(32);
490
+ result.set(namespaceBytes);
491
+ result.set(fileBytes, 16);
492
+ return result;
493
+ }
494
+
495
+ interface FunctionSignature {
496
+ functionName: string;
497
+ functionArgs: string;
498
+ }
499
+
500
+ // TODO: move this to utils as soon as utils are usable inside cli
501
+ // (see https://github.com/latticexyz/mud/issues/499)
502
+ function toFunctionSelector({ functionName, functionArgs }: FunctionSignature): string {
503
+ const functionSignature = functionName + functionArgs;
504
+ if (functionSignature === "") return "0x";
505
+ return sigHash(functionSignature);
506
+ }
507
+
508
+ // TODO: move this to utils as soon as utils are usable inside cli
509
+ // (see https://github.com/latticexyz/mud/issues/499)
510
+ function sigHash(signature: string) {
511
+ return ethers.utils.hexDataSlice(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(signature)), 0, 4);
345
512
  }
@@ -0,0 +1,12 @@
1
+ import { mkdirSync, writeFileSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { formatSolidity } from "./format.js";
4
+
5
+ export async function formatAndWrite(output: string, fullOutputPath: string, logPrefix: string) {
6
+ const formattedOutput = await formatSolidity(output);
7
+
8
+ mkdirSync(dirname(fullOutputPath), { recursive: true });
9
+
10
+ writeFileSync(fullOutputPath, formattedOutput);
11
+ console.log(`${logPrefix}: ${fullOutputPath}`);
12
+ }
@@ -0,0 +1,10 @@
1
+ import { ethers } from "ethers";
2
+
3
+ // TODO: Use viem's getChainId
4
+ export async function getChainId(rpc: string) {
5
+ const { result: chainId } = await ethers.utils.fetchJson(
6
+ rpc,
7
+ '{ "id": 42, "jsonrpc": "2.0", "method": "eth_chainId", "params": [ ] }'
8
+ );
9
+ return Number(chainId);
10
+ }