@latticexyz/cli 2.0.0-next.1 → 2.0.0-next.10

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 (49) hide show
  1. package/dist/chunk-TW3YGZ4D.js +11 -0
  2. package/dist/chunk-TW3YGZ4D.js.map +1 -0
  3. package/dist/index.js +1 -1
  4. package/dist/mud.js +7 -7
  5. package/dist/mud.js.map +1 -1
  6. package/package.json +17 -15
  7. package/src/commands/deploy.ts +1 -1
  8. package/src/commands/dev-contracts.ts +17 -16
  9. package/src/commands/index.ts +2 -2
  10. package/src/commands/tablegen.ts +3 -2
  11. package/src/commands/test.ts +3 -2
  12. package/src/commands/trace.ts +17 -9
  13. package/src/commands/worldgen.ts +1 -1
  14. package/src/index.ts +0 -1
  15. package/src/mud.ts +5 -2
  16. package/src/utils/deploy.ts +189 -554
  17. package/src/utils/deployHandler.ts +9 -3
  18. package/src/utils/modules/constants.ts +23 -0
  19. package/src/utils/modules/getInstallModuleCallData.ts +27 -0
  20. package/src/utils/modules/getUserModules.ts +5 -0
  21. package/src/utils/modules/types.ts +14 -0
  22. package/src/utils/systems/getGrantAccessCallData.ts +29 -0
  23. package/src/utils/systems/getRegisterFunctionSelectorsCallData.ts +57 -0
  24. package/src/utils/systems/getRegisterSystemCallData.ts +17 -0
  25. package/src/utils/systems/types.ts +9 -0
  26. package/src/utils/systems/utils.ts +42 -0
  27. package/src/utils/tables/getRegisterTableCallData.ts +49 -0
  28. package/src/utils/tables/getTableIds.ts +18 -0
  29. package/src/utils/tables/types.ts +12 -0
  30. package/src/utils/utils/confirmNonce.ts +24 -0
  31. package/src/utils/utils/deployContract.ts +33 -0
  32. package/src/utils/utils/fastTxExecute.ts +56 -0
  33. package/src/utils/utils/getContractData.ts +29 -0
  34. package/src/utils/utils/postDeploy.ts +25 -0
  35. package/src/utils/utils/setInternalFeePerGas.ts +49 -0
  36. package/src/utils/utils/types.ts +21 -0
  37. package/src/utils/world.ts +28 -0
  38. package/dist/chunk-P7JIR52V.js +0 -32
  39. package/dist/chunk-P7JIR52V.js.map +0 -1
  40. package/src/commands/tsgen.ts +0 -34
  41. package/src/render-ts/index.ts +0 -5
  42. package/src/render-ts/recsV1TableOptions.ts +0 -57
  43. package/src/render-ts/renderRecsV1Tables.ts +0 -35
  44. package/src/render-ts/schemaTypesToRecsTypeStrings.ts +0 -202
  45. package/src/render-ts/tsgen.ts +0 -12
  46. package/src/render-ts/types.ts +0 -17
  47. package/src/utils/index.ts +0 -7
  48. package/src/utils/worldtypes.ts +0 -21
  49. /package/src/utils/{getChainId.ts → utils/getChainId.ts} +0 -0
@@ -1,24 +1,28 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import path from "path";
3
1
  import chalk from "chalk";
4
- import { BigNumber, ContractInterface, ethers } from "ethers";
5
- import { defaultAbiCoder as abi, Fragment, ParamType } from "ethers/lib/utils.js";
6
-
7
- import { getOutDirectory, getScriptDirectory, cast, forge } from "@latticexyz/common/foundry";
8
- import { resolveWithContext } from "@latticexyz/config";
9
- import { MUDError } from "@latticexyz/common/errors";
10
- import { encodeSchema } from "@latticexyz/schema-type/deprecated";
2
+ import path from "path";
3
+ import { ethers } from "ethers";
4
+ import { getOutDirectory, cast, getSrcDirectory, getRemappings } from "@latticexyz/common/foundry";
11
5
  import { StoreConfig } from "@latticexyz/store";
12
- import { resolveAbiOrUserType } from "@latticexyz/store/codegen";
13
6
  import { WorldConfig, resolveWorldConfig } from "@latticexyz/world";
14
- import { IBaseWorld } from "@latticexyz/world/types/ethers-contracts/IBaseWorld";
15
- import WorldData from "@latticexyz/world/abi/World.sol/World.json" assert { type: "json" };
16
- import IBaseWorldData from "@latticexyz/world/abi/IBaseWorld.sol/IBaseWorld.json" assert { type: "json" };
17
- import CoreModuleData from "@latticexyz/world/abi/CoreModule.sol/CoreModule.json" assert { type: "json" };
18
- import KeysWithValueModuleData from "@latticexyz/world/abi/KeysWithValueModule.sol/KeysWithValueModule.json" assert { type: "json" };
19
- import KeysInTableModuleData from "@latticexyz/world/abi/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" };
20
- import UniqueEntityModuleData from "@latticexyz/world/abi/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" };
21
- import SnapSyncModuleData from "@latticexyz/world/abi/SnapSyncModule.sol/SnapSyncModule.json" assert { type: "json" };
7
+ import { deployWorldContract } from "./world";
8
+ import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" };
9
+ import CoreModuleData from "@latticexyz/world/out/CoreModule.sol/CoreModule.json" assert { type: "json" };
10
+ import { defaultModuleContracts } from "./modules/constants";
11
+ import { getInstallModuleCallData } from "./modules/getInstallModuleCallData";
12
+ import { getUserModules } from "./modules/getUserModules";
13
+ import { getGrantAccessCallData } from "./systems/getGrantAccessCallData";
14
+ import { getRegisterFunctionSelectorsCallData } from "./systems/getRegisterFunctionSelectorsCallData";
15
+ import { getRegisterSystemCallData } from "./systems/getRegisterSystemCallData";
16
+ import { getRegisterTableCallData } from "./tables/getRegisterTableCallData";
17
+ import { getTableIds } from "./tables/getTableIds";
18
+ import { confirmNonce } from "./utils/confirmNonce";
19
+ import { deployContract } from "./utils/deployContract";
20
+ import { fastTxExecute } from "./utils/fastTxExecute";
21
+ import { getContractData } from "./utils/getContractData";
22
+ import { postDeploy } from "./utils/postDeploy";
23
+ import { setInternalFeePerGas } from "./utils/setInternalFeePerGas";
24
+ import { ContractCode } from "./utils/types";
25
+ import { resourceIdToHex } from "@latticexyz/common";
22
26
 
23
27
  export interface DeployConfig {
24
28
  profile?: string;
@@ -41,13 +45,13 @@ export async function deploy(
41
45
  existingContractNames: string[],
42
46
  deployConfig: DeployConfig
43
47
  ): Promise<DeploymentInfo> {
44
- const resolvedConfig = resolveWorldConfig(mudConfig, existingContractNames);
45
-
46
48
  const startTime = Date.now();
47
- const { worldContractName, namespace, postDeployScript } = mudConfig;
48
49
  const { profile, rpc, privateKey, priorityFeeMultiplier, debug, worldAddress, disableTxWait, pollInterval } =
49
50
  deployConfig;
51
+ const resolvedConfig = resolveWorldConfig(mudConfig, existingContractNames);
50
52
  const forgeOutDirectory = await getOutDirectory(profile);
53
+ const remappings = await getRemappings(profile);
54
+ const outputBaseDirectory = path.join(await getSrcDirectory(profile), mudConfig.codegenDirectory);
51
55
 
52
56
  // Set up signer for deployment
53
57
  const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
@@ -55,565 +59,196 @@ export async function deploy(
55
59
  const signer = new ethers.Wallet(privateKey, provider);
56
60
  console.log("Deploying from", signer.address);
57
61
 
58
- // Manual nonce handling to allow for faster sending of transactions without waiting for previous transactions
59
62
  let nonce = await signer.getTransactionCount();
60
63
  console.log("Initial nonce", nonce);
61
64
 
62
- // Compute maxFeePerGas and maxPriorityFeePerGas like ethers, but allow for a multiplier to allow replacing pending transactions
63
- let maxPriorityFeePerGas: number | undefined;
64
- let maxFeePerGas: BigNumber | undefined;
65
- let gasPrice: BigNumber | undefined;
65
+ const txParams = await setInternalFeePerGas(signer, priorityFeeMultiplier);
66
66
 
67
- await setInternalFeePerGas(priorityFeeMultiplier);
68
-
69
- // Catch all to await any promises before exiting the script
70
- let promises: Promise<unknown>[] = [];
67
+ const txConfig = {
68
+ ...txParams,
69
+ signer,
70
+ debug: Boolean(debug),
71
+ disableTxWait,
72
+ confirmations: disableTxWait ? 0 : 1,
73
+ };
71
74
 
72
75
  // Get block number before deploying
73
76
  const blockNumber = Number(await cast(["block-number", "--rpc-url", rpc], { profile }));
74
77
  console.log("Start deployment at block", blockNumber);
75
78
 
76
- // Deploy World
77
- const worldPromise = {
78
- World: worldAddress
79
- ? Promise.resolve(worldAddress)
80
- : worldContractName
81
- ? deployContractByName(worldContractName, disableTxWait)
82
- : deployContract(IBaseWorldData.abi, WorldData.bytecode, disableTxWait, "World"),
83
- };
84
-
85
- // Deploy Systems
86
- const systemPromises = Object.keys(resolvedConfig.systems).reduce<Record<string, Promise<string>>>(
87
- (acc, systemName) => {
88
- acc[systemName] = deployContractByName(systemName, disableTxWait);
89
- return acc;
90
- },
91
- {}
92
- );
93
-
94
- // Deploy default World modules
95
- const defaultModules: Record<string, Promise<string>> = {
96
- // TODO: these only need to be deployed once per chain, add a check if they exist already
97
- CoreModule: deployContract(CoreModuleData.abi, CoreModuleData.bytecode, disableTxWait, "CoreModule"),
98
- KeysWithValueModule: deployContract(
99
- KeysWithValueModuleData.abi,
100
- KeysWithValueModuleData.bytecode,
101
- disableTxWait,
102
- "KeysWithValueModule"
103
- ),
104
- KeysInTableModule: deployContract(
105
- KeysInTableModuleData.abi,
106
- KeysInTableModuleData.bytecode,
107
- disableTxWait,
108
- "KeysInTableModule"
109
- ),
110
- UniqueEntityModule: deployContract(
111
- UniqueEntityModuleData.abi,
112
- UniqueEntityModuleData.bytecode,
113
- disableTxWait,
114
- "UniqueEntityModule"
115
- ),
116
- SnapSyncModule: deployContract(
117
- SnapSyncModuleData.abi,
118
- SnapSyncModuleData.bytecode,
119
- disableTxWait,
120
- "SnapSyncModule"
121
- ),
122
- };
123
-
124
- // Deploy user Modules
125
- const modulePromises = mudConfig.modules
126
- .filter((module) => !defaultModules[module.name]) // Only deploy user modules here, not default modules
127
- .reduce<Record<string, Promise<string>>>((acc, module) => {
128
- acc[module.name] = deployContractByName(module.name, disableTxWait);
129
- return acc;
130
- }, defaultModules);
131
-
132
- // Combine all contracts into one object
133
- const contractPromises: Record<string, Promise<string>> = { ...worldPromise, ...systemPromises, ...modulePromises };
134
-
135
- // Create World contract instance from deployed address
136
- const WorldContract = new ethers.Contract(await contractPromises.World, IBaseWorldData.abi, signer) as IBaseWorld;
137
-
138
- const confirmations = disableTxWait ? 0 : 1;
139
-
140
- // Install core Modules
141
- if (!worldAddress) {
142
- console.log(chalk.blue("Installing core World modules"));
143
- await fastTxExecute(WorldContract, "installRootModule", [await modulePromises.CoreModule, "0x"], confirmations);
144
- console.log(chalk.green("Installed core World modules"));
145
- }
146
-
147
- // Register namespace
148
- if (namespace) await fastTxExecute(WorldContract, "registerNamespace", [toBytes16(namespace)], confirmations);
149
-
150
- // Register tables
151
- const tableIds: { [tableName: string]: Uint8Array } = {};
152
- promises = [
153
- ...promises,
154
- ...Object.entries(mudConfig.tables).map(async ([tableName, { name, schema, keySchema }]) => {
155
- console.log(chalk.blue(`Registering table ${tableName} at ${namespace}/${name}`));
156
-
157
- // Store the tableId for later use
158
- tableIds[tableName] = toResourceSelector(namespace, name);
159
-
160
- // Register table
161
- const schemaTypes = Object.values(schema).map((abiOrUserType) => {
162
- const { schemaType } = resolveAbiOrUserType(abiOrUserType, mudConfig);
163
- return schemaType;
164
- });
165
-
166
- const keyTypes = Object.values(keySchema).map((abiOrUserType) => {
167
- const { schemaType } = resolveAbiOrUserType(abiOrUserType, mudConfig);
168
- return schemaType;
79
+ // Deploy the World contract. Non-blocking.
80
+ const worldPromise: Promise<string> = worldAddress
81
+ ? Promise.resolve(worldAddress)
82
+ : deployWorldContract({
83
+ ...txConfig,
84
+ nonce: nonce++,
85
+ worldContractName: mudConfig.worldContractName,
86
+ forgeOutDirectory,
169
87
  });
170
88
 
171
- await fastTxExecute(
172
- WorldContract,
173
- "registerTable",
174
- [toBytes16(namespace), toBytes16(name), encodeSchema(schemaTypes), encodeSchema(keyTypes)],
175
- confirmations
176
- );
177
-
178
- // Register table metadata
179
- await fastTxExecute(
180
- WorldContract,
181
- "setMetadata(bytes16,bytes16,string,string[])",
182
- [toBytes16(namespace), toBytes16(name), tableName, Object.keys(schema)],
183
- confirmations
184
- );
185
-
186
- console.log(chalk.green(`Registered table ${tableName} at ${name}`));
187
- }),
188
- ];
189
-
190
- // Register systems (using forEach instead of for..of to avoid blocking on async calls)
191
- promises = [
192
- ...promises,
193
- ...Object.entries(resolvedConfig.systems).map(
194
- async ([systemName, { name, openAccess, registerFunctionSelectors }]) => {
195
- // Register system at route
196
- console.log(chalk.blue(`Registering system ${systemName} at ${namespace}/${name}`));
197
- await fastTxExecute(
198
- WorldContract,
199
- "registerSystem",
200
- [toBytes16(namespace), toBytes16(name), await contractPromises[systemName], openAccess],
201
- confirmations
202
- );
203
- console.log(chalk.green(`Registered system ${systemName} at ${namespace}/${name}`));
204
-
205
- // Register function selectors for the system
206
- if (registerFunctionSelectors) {
207
- const functionSignatures: FunctionSignature[] = await loadFunctionSignatures(systemName);
208
- const isRoot = namespace === "";
209
- // Using Promise.all to avoid blocking on async calls
210
- await Promise.all(
211
- functionSignatures.map(async ({ functionName, functionArgs }) => {
212
- const functionSignature = isRoot
213
- ? functionName + functionArgs
214
- : `${namespace}_${name}_${functionName}${functionArgs}`;
215
-
216
- console.log(chalk.blue(`Registering function "${functionSignature}"`));
217
- if (isRoot) {
218
- const worldFunctionSelector = toFunctionSelector(
219
- functionSignature === ""
220
- ? { functionName: systemName, functionArgs } // Register the system's fallback function as `<systemName>(<args>)`
221
- : { functionName, functionArgs }
222
- );
223
- const systemFunctionSelector = toFunctionSelector({ functionName, functionArgs });
224
- await fastTxExecute(
225
- WorldContract,
226
- "registerRootFunctionSelector",
227
- [toBytes16(namespace), toBytes16(name), worldFunctionSelector, systemFunctionSelector],
228
- confirmations
229
- );
230
- } else {
231
- await fastTxExecute(
232
- WorldContract,
233
- "registerFunctionSelector",
234
- [toBytes16(namespace), toBytes16(name), functionName, functionArgs],
235
- confirmations
236
- );
237
- }
238
- console.log(chalk.green(`Registered function "${functionSignature}"`));
239
- })
240
- );
241
- }
242
- }
243
- ),
244
- ];
245
-
246
- // Wait for resources to be registered before granting access to them
247
- await Promise.all(promises); // ----------------------------------------------------------------------------------------------
248
- promises = [];
249
-
250
- // Grant access to systems
251
- for (const [systemName, { name, accessListAddresses, accessListSystems }] of Object.entries(resolvedConfig.systems)) {
252
- const resourceSelector = `${namespace}/${name}`;
253
-
254
- // Grant access to addresses
255
- promises = [
256
- ...promises,
257
- ...accessListAddresses.map(async (address) => {
258
- console.log(chalk.blue(`Grant ${address} access to ${systemName} (${resourceSelector})`));
259
- await fastTxExecute(
260
- WorldContract,
261
- "grantAccess",
262
- [toBytes16(namespace), toBytes16(name), address],
263
- confirmations
264
- );
265
- console.log(chalk.green(`Granted ${address} access to ${systemName} (${namespace}/${name})`));
266
- }),
267
- ];
268
-
269
- // Grant access to other systems
270
- promises = [
271
- ...promises,
272
- ...accessListSystems.map(async (granteeSystem) => {
273
- console.log(chalk.blue(`Grant ${granteeSystem} access to ${systemName} (${resourceSelector})`));
274
- await fastTxExecute(
275
- WorldContract,
276
- "grantAccess",
277
- [toBytes16(namespace), toBytes16(name), await contractPromises[granteeSystem]],
278
- confirmations
279
- );
280
- console.log(chalk.green(`Granted ${granteeSystem} access to ${systemName} (${resourceSelector})`));
281
- }),
282
- ];
283
- }
284
-
285
- // Wait for access to be granted before installing modules
286
- await Promise.all(promises); // ----------------------------------------------------------------------------------------------
287
- promises = [];
288
-
289
- // Install modules
290
- promises = [
291
- ...promises,
292
- ...mudConfig.modules.map(async (module) => {
293
- console.log(chalk.blue(`Installing${module.root ? " root " : " "}module ${module.name}`));
294
- // Resolve arguments
295
- const resolvedArgs = await Promise.all(
296
- module.args.map((arg) => resolveWithContext(arg, { tableIds, systemAddresses: contractPromises }))
297
- );
298
- const values = resolvedArgs.map((arg) => arg.value);
299
- const types = resolvedArgs.map((arg) => arg.type);
300
- const moduleAddress = await contractPromises[module.name];
301
- if (!moduleAddress) throw new Error(`Module ${module.name} not found`);
302
-
303
- // Send transaction to install module
304
- await fastTxExecute(
305
- WorldContract,
306
- module.root ? "installRootModule" : "installModule",
307
- [moduleAddress, abi.encode(types, values)],
308
- confirmations
309
- );
310
-
311
- console.log(chalk.green(`Installed${module.root ? " root " : " "}module ${module.name}`));
312
- }),
89
+ // Filters any default modules from config
90
+ const userModules = getUserModules(defaultModuleContracts, mudConfig.modules);
91
+ const userModuleContracts = Object.keys(userModules).map((name) => {
92
+ const { abi, bytecode } = getContractData(name, forgeOutDirectory);
93
+ return {
94
+ name,
95
+ abi,
96
+ bytecode,
97
+ } as ContractCode;
98
+ });
99
+
100
+ const systemContracts = Object.keys(resolvedConfig.systems).map((name) => {
101
+ const { abi, bytecode } = getContractData(name, forgeOutDirectory);
102
+ return {
103
+ name,
104
+ abi,
105
+ bytecode,
106
+ } as ContractCode;
107
+ });
108
+
109
+ const contracts: ContractCode[] = [
110
+ {
111
+ name: "CoreModule",
112
+ abi: CoreModuleData.abi,
113
+ bytecode: CoreModuleData.bytecode,
114
+ },
115
+ ...defaultModuleContracts,
116
+ ...userModuleContracts,
117
+ ...systemContracts,
313
118
  ];
314
119
 
315
- // Await all promises before executing PostDeploy script
316
- await Promise.all(promises); // ----------------------------------------------------------------------------------------------
317
-
318
- // Confirm the current nonce is the expected nonce to make sure all transactions have been included
319
- let remoteNonce = await signer.getTransactionCount();
320
- let retryCount = 0;
321
- const maxRetries = 100;
322
- while (remoteNonce !== nonce && retryCount < maxRetries) {
323
- console.log(
324
- chalk.gray(
325
- `Waiting for transactions to be included before executing ${postDeployScript} (local nonce: ${nonce}, remote nonce: ${remoteNonce}, retry number ${retryCount}/${maxRetries})`
326
- )
327
- );
328
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
329
- retryCount++;
330
- remoteNonce = await signer.getTransactionCount();
331
- }
332
- if (remoteNonce !== nonce) {
333
- throw new MUDError(
334
- "Remote nonce doesn't match local nonce, indicating that not all deploy transactions were included."
335
- );
336
- }
337
-
338
- promises = [];
339
-
340
- // Execute postDeploy forge script
341
- const postDeployPath = path.join(await getScriptDirectory(), postDeployScript + ".s.sol");
342
- if (existsSync(postDeployPath)) {
343
- console.log(chalk.blue(`Executing post deploy script at ${postDeployPath}`));
344
- await forge(
345
- [
346
- "script",
347
- postDeployScript,
348
- "--sig",
349
- "run(address)",
350
- await contractPromises.World,
351
- "--broadcast",
352
- "--rpc-url",
353
- rpc,
354
- "-vvv",
355
- ],
356
- {
357
- profile,
358
- }
359
- );
360
- } else {
361
- console.log(`No script at ${postDeployPath}, skipping post deploy hook`);
362
- }
363
-
364
- console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
365
-
366
- return { worldAddress: await contractPromises.World, blockNumber };
367
-
368
- // ------------------- INTERNAL FUNCTIONS -------------------
369
- // (Inlined to avoid having to pass around nonce, signer and forgeOutDir)
370
-
371
- /**
372
- * Deploy a contract and return the address
373
- * @param contractName Name of the contract to deploy (must exist in the file system)
374
- * @param disableTxWait Disable waiting for contract deployment
375
- * @returns Address of the deployed contract
376
- */
377
- async function deployContractByName(contractName: string, disableTxWait: boolean): Promise<string> {
378
- console.log(chalk.blue("Deploying", contractName));
379
-
380
- const { abi, bytecode } = await getContractData(contractName);
381
- return deployContract(abi, bytecode, disableTxWait, contractName);
382
- }
383
-
384
- /**
385
- * Deploy a contract and return the address
386
- * @param abi The contract interface
387
- * @param bytecode The contract bytecode
388
- * @param disableTxWait Disable waiting for contract deployment
389
- * @param contractName The contract name (optional, used for logs)
390
- * @param retryCount
391
- * @returns Address of the deployed contract
392
- */
393
- async function deployContract(
394
- abi: ContractInterface,
395
- bytecode: string | { object: string },
396
- disableTxWait: boolean,
397
- contractName?: string,
398
- retryCount = 0
399
- ): Promise<string> {
400
- try {
401
- const factory = new ethers.ContractFactory(abi, bytecode, signer);
402
- console.log(chalk.gray(`executing deployment of ${contractName} with nonce ${nonce}`));
403
- const deployPromise = factory
404
- .deploy({
405
- nonce: nonce++,
406
- maxPriorityFeePerGas,
407
- maxFeePerGas,
408
- gasPrice,
409
- })
410
- .then((c) => (disableTxWait ? c : c.deployed()));
411
-
412
- promises.push(deployPromise);
413
- const { address } = await deployPromise;
414
-
415
- console.log(chalk.green("Deployed", contractName, "to", address));
416
- return address;
417
- } catch (error: any) {
418
- if (debug) console.error(error);
419
- if (retryCount === 0 && error?.message.includes("transaction already imported")) {
420
- // If the deployment failed because the transaction was already imported,
421
- // retry with a higher priority fee
422
- setInternalFeePerGas(priorityFeeMultiplier * 1.1);
423
- return deployContract(abi, bytecode, disableTxWait, contractName, retryCount++);
424
- } else if (error?.message.includes("invalid bytecode")) {
425
- throw new MUDError(
426
- `Error deploying ${contractName}: invalid bytecode. Note that linking of public libraries is not supported yet, make sure none of your libraries use "external" functions.`
427
- );
428
- } else if (error?.message.includes("CreateContractLimit")) {
429
- throw new MUDError(`Error deploying ${contractName}: CreateContractLimit exceeded.`);
430
- } else throw error;
431
- }
432
- }
433
-
434
- /**
435
- * Deploy a contract and return the address
436
- * @param contractName Name of the contract to deploy (must exist in the file system)
437
- * @returns Address of the deployed contract
438
- *
439
- * NOTE: Forge deploy seems to be slightly slower than ethers
440
- * (probably due to the extra overhead spawning a child process to run forge),
441
- * so we mostly use ethersDeployContract here.
442
- * However, for contracts not in the user directory (eg. the vanilla World contract),
443
- * using forge is more convenient because it automatically finds the contract in the @latticexyz/world package.
444
- */
445
- // async function forgeDeployContract(contractName: string): Promise<string> {
446
- // console.log(chalk.blue("Deploying", contractName));
447
-
448
- // const { deployedTo } = JSON.parse(
449
- // await forge(
450
- // ["create", contractName, "--rpc-url", rpc, "--private-key", privateKey, "--json", "--nonce", String(nonce++)],
451
- // { profile, silent: true }
452
- // )
453
- // );
454
- // return deployedTo;
455
- // }
456
-
457
- async function loadFunctionSignatures(contractName: string): Promise<FunctionSignature[]> {
458
- const { abi } = await getContractData(contractName);
459
-
460
- return abi
461
- .filter((item) => ["fallback", "function"].includes(item.type))
462
- .map((item) => {
463
- if (item.type === "fallback") return { functionName: "", functionArgs: "" };
120
+ // Deploy the System and Module contracts
121
+ const deployedContracts = contracts.reduce<Record<string, Promise<string>>>((acc, contract) => {
122
+ acc[contract.name] = deployContract({
123
+ ...txConfig,
124
+ nonce: nonce++,
125
+ contract,
126
+ });
127
+ return acc;
128
+ }, {});
464
129
 
465
- return {
466
- functionName: item.name,
467
- functionArgs: parseComponents(item.inputs),
468
- };
469
- });
470
- }
130
+ // Wait for world to be deployed
131
+ const deployedWorldAddress = await worldPromise;
132
+ const worldContract = new ethers.Contract(deployedWorldAddress, IBaseWorldAbi);
471
133
 
472
- /**
473
- * Recursively turn (nested) structs in signatures into tuples
474
- */
475
- function parseComponents(params: ParamType[]): string {
476
- const components = params.map((param) => {
477
- const tupleMatch = param.type.match(/tuple(.*)/);
478
- if (tupleMatch) {
479
- // there can be arrays of tuples,
480
- // `tupleMatch[1]` preserves the array brackets (or is empty string for non-arrays)
481
- return parseComponents(param.components) + tupleMatch[1];
482
- } else {
483
- return param.type;
484
- }
134
+ // If an existing World is passed assume its coreModule is already installed - blocking to install if not
135
+ if (!worldAddress) {
136
+ console.log(chalk.blue("Installing CoreModule"));
137
+ await fastTxExecute({
138
+ ...txConfig,
139
+ nonce: nonce++,
140
+ contract: worldContract,
141
+ func: "initialize",
142
+ args: [await deployedContracts["CoreModule"]],
485
143
  });
486
- return `(${components})`;
487
- }
488
-
489
- /**
490
- * Only await gas estimation (for speed), only execute if gas estimation succeeds (for safety)
491
- */
492
- async function fastTxExecute<C extends { estimateGas: any; [key: string]: any }, F extends keyof C>(
493
- contract: C,
494
- func: F,
495
- args: Parameters<C[F]>,
496
- confirmations = 1,
497
- retryCount = 0
498
- ): Promise<Awaited<ReturnType<Awaited<ReturnType<C[F]>>["wait"]>>> {
499
- const functionName = `${func as string}(${args.map((arg) => `'${arg}'`).join(",")})`;
500
- try {
501
- const gasLimit = await contract.estimateGas[func].apply(null, args);
502
- console.log(chalk.gray(`executing transaction: ${functionName} with nonce ${nonce}`));
503
- const txPromise = contract[func]
504
- .apply(null, [...args, { gasLimit, nonce: nonce++, maxPriorityFeePerGas, maxFeePerGas, gasPrice }])
505
- .then((tx: any) => (confirmations === 0 ? tx : tx.wait(confirmations)));
506
- promises.push(txPromise);
507
- return txPromise;
508
- } catch (error: any) {
509
- if (debug) console.error(error);
510
- if (retryCount === 0 && error?.message.includes("transaction already imported")) {
511
- // If the deployment failed because the transaction was already imported,
512
- // retry with a higher priority fee
513
- setInternalFeePerGas(priorityFeeMultiplier * 1.1);
514
- return fastTxExecute(contract, func, args, confirmations, retryCount++);
515
- } else throw new MUDError(`Gas estimation error for ${functionName}: ${error?.reason}`);
516
- }
144
+ console.log(chalk.green("Installed CoreModule"));
517
145
  }
518
146
 
519
- /**
520
- * Load the contract's abi and bytecode from the file system
521
- * @param contractName: Name of the contract to load
522
- */
523
- async function getContractData(contractName: string): Promise<{ bytecode: string; abi: Fragment[] }> {
524
- let data: any;
525
- const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json");
526
- try {
527
- data = JSON.parse(readFileSync(contractDataPath, "utf8"));
528
- } catch (error: any) {
529
- throw new MUDError(`Error reading file at ${contractDataPath}`);
530
- }
531
-
532
- const bytecode = data?.bytecode?.object;
533
- if (!bytecode) throw new MUDError(`No bytecode found in ${contractDataPath}`);
534
-
535
- const abi = data?.abi;
536
- if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`);
537
-
538
- return { abi, bytecode };
147
+ if (mudConfig.namespace) {
148
+ console.log(chalk.blue("Registering Namespace"));
149
+ await fastTxExecute({
150
+ ...txConfig,
151
+ nonce: nonce++,
152
+ contract: worldContract,
153
+ func: "registerNamespace",
154
+ args: [resourceIdToHex({ type: "namespace", namespace: mudConfig.namespace, name: "" })],
155
+ });
156
+ console.log(chalk.green("Namespace registered"));
539
157
  }
540
158
 
541
- /**
542
- * Set the maxFeePerGas and maxPriorityFeePerGas based on the current base fee and the given multiplier.
543
- * The multiplier is used to allow replacing pending transactions.
544
- * @param multiplier Multiplier to apply to the base fee
545
- */
546
- async function setInternalFeePerGas(multiplier: number) {
547
- // Compute maxFeePerGas and maxPriorityFeePerGas like ethers, but allow for a multiplier to allow replacing pending transactions
548
- const feeData = await provider.getFeeData();
549
-
550
- if (feeData.lastBaseFeePerGas) {
551
- if (!feeData.lastBaseFeePerGas.eq(0) && (await signer.getBalance()).eq(0)) {
552
- throw new MUDError(`Attempting to deploy to a chain with non-zero base fee with an account that has no balance.
553
- If you're deploying to the Lattice testnet, you can fund your account by running 'pnpm mud faucet --address ${await signer.getAddress()}'`);
554
- }
159
+ const tableIds = getTableIds(mudConfig);
555
160
 
556
- // Set the priority fee to 0 for development chains with no base fee, to allow transactions from unfunded wallets
557
- maxPriorityFeePerGas = feeData.lastBaseFeePerGas.eq(0) ? 0 : Math.floor(1_500_000_000 * multiplier);
558
- maxFeePerGas = feeData.lastBaseFeePerGas.mul(2).add(maxPriorityFeePerGas);
559
- } else if (feeData.gasPrice) {
560
- // Legacy chains with gasPrice instead of maxFeePerGas
561
- if (!feeData.gasPrice.eq(0) && (await signer.getBalance()).eq(0)) {
562
- throw new MUDError(
563
- `Attempting to deploy to a chain with non-zero gas price with an account that has no balance.`
564
- );
565
- }
161
+ const registerTableCalls = Object.values(mudConfig.tables).map((table) =>
162
+ getRegisterTableCallData(table, mudConfig, outputBaseDirectory, remappings)
163
+ );
566
164
 
567
- gasPrice = feeData.gasPrice;
568
- } else {
569
- throw new MUDError("Can not fetch fee data from RPC");
570
- }
571
- }
572
- }
165
+ console.log(chalk.blue("Registering tables"));
166
+ await Promise.all(
167
+ registerTableCalls.map((call) =>
168
+ fastTxExecute({
169
+ ...txConfig,
170
+ nonce: nonce++,
171
+ contract: worldContract,
172
+ ...call,
173
+ })
174
+ )
175
+ );
176
+ console.log(chalk.green(`Tables registered`));
177
+
178
+ console.log(chalk.blue("Registering Systems and Functions"));
179
+ const systemCalls = await Promise.all(
180
+ Object.entries(resolvedConfig.systems).map(([systemKey, system]) =>
181
+ getRegisterSystemCallData({
182
+ systemContracts: deployedContracts,
183
+ systemKey,
184
+ system,
185
+ namespace: mudConfig.namespace,
186
+ })
187
+ )
188
+ );
189
+ const functionCalls = Object.entries(resolvedConfig.systems).flatMap(([systemKey, system]) =>
190
+ getRegisterFunctionSelectorsCallData({
191
+ systemContractName: systemKey,
192
+ system,
193
+ namespace: mudConfig.namespace,
194
+ forgeOutDirectory,
195
+ })
196
+ );
197
+ await Promise.all(
198
+ [...systemCalls, ...functionCalls].map((call) =>
199
+ fastTxExecute({
200
+ ...txConfig,
201
+ nonce: nonce++,
202
+ contract: worldContract,
203
+ ...call,
204
+ })
205
+ )
206
+ );
207
+ console.log(chalk.green(`Systems and Functions registered`));
208
+
209
+ // Wait for System access to be granted before installing modules
210
+ const grantCalls = await getGrantAccessCallData({
211
+ systems: Object.values(resolvedConfig.systems),
212
+ systemContracts: deployedContracts,
213
+ namespace: mudConfig.namespace,
214
+ });
215
+
216
+ console.log(chalk.blue("Granting Access"));
217
+ await Promise.all(
218
+ grantCalls.map((call) =>
219
+ fastTxExecute({
220
+ ...txConfig,
221
+ nonce: nonce++,
222
+ contract: worldContract,
223
+ ...call,
224
+ })
225
+ )
226
+ );
227
+ console.log(chalk.green(`Access granted`));
573
228
 
574
- // TODO: use stringToBytes16 from utils as soon as utils are usable inside cli
575
- // (see https://github.com/latticexyz/mud/issues/499)
576
- function toBytes16(input: string) {
577
- if (input.length > 16) throw new Error("String does not fit into 16 bytes");
229
+ const moduleCalls = await Promise.all(
230
+ mudConfig.modules.map((m) => getInstallModuleCallData(deployedContracts, m, tableIds))
231
+ );
578
232
 
579
- const result = new Uint8Array(16);
580
- // Set ascii bytes
581
- for (let i = 0; i < input.length; i++) {
582
- result[i] = input.charCodeAt(i);
583
- }
584
- // Set the remaining bytes to 0
585
- for (let i = input.length; i < 16; i++) {
586
- result[i] = 0;
587
- }
588
- return result;
589
- }
233
+ console.log(chalk.blue("Installing User Modules"));
234
+ await Promise.all(
235
+ moduleCalls.map((call) =>
236
+ fastTxExecute({
237
+ ...txConfig,
238
+ nonce: nonce++,
239
+ contract: worldContract,
240
+ ...call,
241
+ })
242
+ )
243
+ );
244
+ console.log(chalk.green(`User Modules Installed`));
590
245
 
591
- // TODO: use TableId from utils as soon as utils are usable inside cli
592
- // (see https://github.com/latticexyz/mud/issues/499)
593
- function toResourceSelector(namespace: string, file: string): Uint8Array {
594
- const namespaceBytes = toBytes16(namespace);
595
- const fileBytes = toBytes16(file);
596
- const result = new Uint8Array(32);
597
- result.set(namespaceBytes);
598
- result.set(fileBytes, 16);
599
- return result;
600
- }
246
+ // Double check that all transactions have been included by confirming the current nonce is the expected nonce
247
+ await confirmNonce(signer, nonce, pollInterval);
601
248
 
602
- interface FunctionSignature {
603
- functionName: string;
604
- functionArgs: string;
605
- }
249
+ await postDeploy(mudConfig.postDeployScript, deployedWorldAddress, rpc, profile);
606
250
 
607
- // TODO: move this to utils as soon as utils are usable inside cli
608
- // (see https://github.com/latticexyz/mud/issues/499)
609
- function toFunctionSelector({ functionName, functionArgs }: FunctionSignature): string {
610
- const functionSignature = functionName + functionArgs;
611
- if (functionSignature === "") return "0x";
612
- return sigHash(functionSignature);
613
- }
251
+ console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
614
252
 
615
- // TODO: move this to utils as soon as utils are usable inside cli
616
- // (see https://github.com/latticexyz/mud/issues/499)
617
- function sigHash(signature: string) {
618
- return ethers.utils.hexDataSlice(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(signature)), 0, 4);
253
+ return { worldAddress: deployedWorldAddress, blockNumber };
619
254
  }