@latticexyz/cli 1.40.0 → 1.41.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-ATAWDHWC.js +67 -0
  3. package/dist/{chunk-6AQ6LFVZ.js → chunk-J4DJQNIC.js} +743 -103
  4. package/dist/chunk-KD354QKC.js +23039 -0
  5. package/dist/{chunk-S3V3XX7N.js → chunk-SLIMIO4Z.js} +1 -1
  6. package/dist/config/index.d.ts +746 -8
  7. package/dist/config/index.js +63 -17
  8. package/dist/index.d.ts +1 -2
  9. package/dist/index.js +14 -10
  10. package/dist/mud.js +1055 -49
  11. package/dist/utils/deprecated/index.js +2 -2
  12. package/dist/utils/index.d.ts +56 -7
  13. package/dist/utils/index.js +17 -3
  14. package/package.json +16 -11
  15. package/src/commands/deploy-v2.ts +96 -0
  16. package/src/commands/deprecated/call-system.ts +1 -1
  17. package/src/commands/deprecated/deploy-contracts.ts +1 -1
  18. package/src/commands/deprecated/test.ts +9 -6
  19. package/src/commands/deprecated/trace.ts +1 -1
  20. package/src/commands/gas-report.ts +1 -1
  21. package/src/commands/index.ts +4 -0
  22. package/src/commands/tablegen.ts +4 -18
  23. package/src/commands/worldgen.ts +55 -0
  24. package/src/config/commonSchemas.ts +19 -5
  25. package/src/config/dynamicResolution.ts +49 -0
  26. package/src/config/index.ts +20 -0
  27. package/src/config/loadStoreConfig.ts +3 -89
  28. package/src/config/parseStoreConfig.test-d.ts +40 -0
  29. package/src/config/parseStoreConfig.ts +314 -0
  30. package/src/config/validation.ts +71 -0
  31. package/src/config/world/index.ts +4 -0
  32. package/src/config/world/loadWorldConfig.test-d.ts +11 -0
  33. package/src/config/world/loadWorldConfig.ts +26 -0
  34. package/src/config/world/parseWorldConfig.ts +55 -0
  35. package/src/config/world/resolveWorldConfig.ts +80 -0
  36. package/src/config/world/userTypes.ts +72 -0
  37. package/src/index.ts +13 -5
  38. package/src/mud.ts +4 -0
  39. package/src/render-solidity/common.ts +138 -0
  40. package/src/render-solidity/field.ts +137 -0
  41. package/src/render-solidity/index.ts +10 -0
  42. package/src/render-solidity/record.ts +154 -0
  43. package/src/render-solidity/renderSystemInterface.ts +31 -0
  44. package/src/render-solidity/renderTable.ts +164 -0
  45. package/src/render-solidity/renderTypeHelpers.ts +99 -0
  46. package/src/render-solidity/renderTypes.ts +19 -0
  47. package/src/render-solidity/renderTypesFromConfig.ts +13 -0
  48. package/src/render-solidity/renderWorld.ts +24 -0
  49. package/src/{render-table/renderTablesFromConfig.ts → render-solidity/tableOptions.ts} +45 -37
  50. package/src/render-solidity/tablegen.ts +33 -0
  51. package/src/render-solidity/types.ts +110 -0
  52. package/src/render-solidity/userType.ts +132 -0
  53. package/src/render-solidity/worldgen.ts +60 -0
  54. package/src/utils/contractToInterface.ts +130 -0
  55. package/src/utils/deploy-v2.ts +512 -0
  56. package/src/utils/deprecated/build.ts +1 -1
  57. package/src/utils/deprecated/typegen.ts +1 -1
  58. package/src/utils/errors.ts +12 -2
  59. package/src/utils/execLog.ts +22 -0
  60. package/src/utils/formatAndWrite.ts +12 -0
  61. package/src/utils/foundry.ts +94 -0
  62. package/src/utils/getChainId.ts +10 -0
  63. package/src/utils/index.ts +2 -1
  64. package/src/utils/typeUtils.ts +17 -0
  65. package/dist/chunk-B6VWCGHZ.js +0 -199
  66. package/dist/chunk-JKAA3WMC.js +0 -55
  67. package/dist/chunk-JNGSW4AP.js +0 -493
  68. package/dist/chunk-PJ6GS2R4.js +0 -22
  69. package/dist/chunk-UC3QPOON.js +0 -35
  70. package/dist/loadStoreConfig-37f99136.d.ts +0 -164
  71. package/dist/render-table/index.d.ts +0 -29
  72. package/dist/render-table/index.js +0 -24
  73. package/dist/renderTable-9e6410c5.d.ts +0 -72
  74. package/src/config/loadStoreConfig.test-d.ts +0 -11
  75. package/src/render-table/common.ts +0 -67
  76. package/src/render-table/field.ts +0 -132
  77. package/src/render-table/index.ts +0 -6
  78. package/src/render-table/record.ts +0 -176
  79. package/src/render-table/renderTable.ts +0 -109
  80. package/src/render-table/types.ts +0 -51
  81. package/src/utils/forgeConfig.ts +0 -45
@@ -0,0 +1,512 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import path from "path";
3
+ import { MUDConfig, resolveWithContext } from "../config/index.js";
4
+ import { MUDError } from "./errors.js";
5
+ import { getOutDirectory, getScriptDirectory, cast, forge } from "./foundry.js";
6
+ import { BigNumber, ContractInterface, ethers } from "ethers";
7
+ import { IWorld } from "@latticexyz/world/types/ethers-contracts/IWorld.js";
8
+ import { ArgumentsType } from "vitest";
9
+ import chalk from "chalk";
10
+ import { encodeSchema } from "@latticexyz/schema-type";
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" };
19
+
20
+ export interface DeployConfig {
21
+ profile?: string;
22
+ rpc: string;
23
+ privateKey: string;
24
+ priorityFeeMultiplier: number;
25
+ debug?: boolean;
26
+ }
27
+
28
+ export interface DeploymentInfo {
29
+ blockNumber: number;
30
+ worldAddress: string;
31
+ }
32
+
33
+ export async function deploy(mudConfig: MUDConfig, deployConfig: DeployConfig): Promise<DeploymentInfo> {
34
+ const startTime = Date.now();
35
+ const { worldContractName, namespace, postDeployScript } = mudConfig;
36
+ const { profile, rpc, privateKey, priorityFeeMultiplier, debug } = deployConfig;
37
+ const forgeOutDirectory = await getOutDirectory(profile);
38
+
39
+ // Set up signer for deployment
40
+ const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
41
+ const signer = new ethers.Wallet(privateKey, provider);
42
+
43
+ // Manual nonce handling to allow for faster sending of transactions without waiting for previous transactions
44
+ let nonce = await signer.getTransactionCount();
45
+ console.log("Initial nonce", nonce);
46
+
47
+ // Compute maxFeePerGas and maxPriorityFeePerGas like ethers, but allow for a multiplier to allow replacing pending transactions
48
+ let maxPriorityFeePerGas: number;
49
+ let maxFeePerGas: BigNumber;
50
+ setInternalFeePerGas(priorityFeeMultiplier);
51
+
52
+ // Catch all to await any promises before exiting the script
53
+ let promises: Promise<unknown>[] = [];
54
+
55
+ // Get block number before deploying
56
+ const blockNumber = Number(await cast(["block-number", "--rpc-url", rpc], { profile }));
57
+ console.log("Start deployment at block", blockNumber);
58
+
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);
93
+ return acc;
94
+ }, defaultModules);
95
+
96
+ // Combine all contracts into one object
97
+ const contractPromises: Record<string, Promise<string>> = { ...worldPromise, ...systemPromises, ...modulePromises };
98
+
99
+ // Create World contract instance from deployed address
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"));
107
+
108
+ // Register namespace
109
+ if (namespace) await fastTxExecute(WorldContract, "registerNamespace", [toBytes16(namespace)]);
110
+
111
+ // Register tables
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}`));
117
+
118
+ // Store the tableId for later use
119
+ tableIds[tableName] = toResourceSelector(namespace, fileSelector);
120
+
121
+ // Register table
122
+ const schemaTypes = Object.values(schema).map((abiOrUserType) => {
123
+ const { schemaType } = resolveAbiOrUserType(abiOrUserType, mudConfig);
124
+ return schemaType;
125
+ });
126
+
127
+ const keyTypes = Object.values(primaryKeys).map((abiOrUserType) => {
128
+ const { schemaType } = resolveAbiOrUserType(abiOrUserType, mudConfig);
129
+ return schemaType;
130
+ });
131
+
132
+ await fastTxExecute(WorldContract, "registerTable", [
133
+ toBytes16(namespace),
134
+ toBytes16(fileSelector),
135
+ encodeSchema(schemaTypes),
136
+ encodeSchema(keyTypes),
137
+ ]);
138
+
139
+ // Register table metadata
140
+ await fastTxExecute(WorldContract, "setMetadata(bytes16,bytes16,string,string[])", [
141
+ toBytes16(namespace),
142
+ toBytes16(fileSelector),
143
+ tableName,
144
+ Object.keys(schema),
145
+ ]);
146
+
147
+ console.log(chalk.green(`Registered table ${tableName} at ${fileSelector}`));
148
+ }),
149
+ ];
150
+
151
+ // Register systems (using forEach instead of for..of to avoid blocking on async calls)
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
+ ];
206
+
207
+ // Wait for resources to be registered before granting access to them
208
+ await Promise.all(promises); // ----------------------------------------------------------------------------------------------
209
+ promises = [];
210
+
211
+ // Grant access to systems
212
+ for (const [systemName, { fileSelector, accessListAddresses, accessListSystems }] of Object.entries(
213
+ mudConfig.systems
214
+ )) {
215
+ const resourceSelector = `${namespace}/${fileSelector}`;
216
+
217
+ // Grant access to addresses
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
+ ];
230
+
231
+ // Grant access to other systems
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
+ ];
244
+ }
245
+
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 = [];
277
+
278
+ // Execute postDeploy forge script
279
+ const postDeployPath = path.join(await getScriptDirectory(), postDeployScript + ".s.sol");
280
+ if (existsSync(postDeployPath)) {
281
+ console.log(chalk.blue(`Executing post deploy script at ${postDeployPath}`));
282
+ await forge(
283
+ [
284
+ "script",
285
+ postDeployScript,
286
+ "--sig",
287
+ "run(address)",
288
+ await contractPromises.World,
289
+ "--broadcast",
290
+ "--rpc-url",
291
+ rpc,
292
+ "-vvv",
293
+ ],
294
+ {
295
+ profile,
296
+ }
297
+ );
298
+ } else {
299
+ console.log(`No script at ${postDeployPath}, skipping post deploy hook`);
300
+ }
301
+
302
+ console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
303
+
304
+ return { worldAddress: await contractPromises.World, blockNumber };
305
+
306
+ // ------------------- INTERNAL FUNCTIONS -------------------
307
+ // (Inlined to avoid having to pass around nonce, signer and forgeOutDir)
308
+
309
+ /**
310
+ * Deploy a contract and return the address
311
+ * @param contractName Name of the contract to deploy (must exist in the file system)
312
+ * @returns Address of the deployed contract
313
+ */
314
+ async function deployContractByName(contractName: string): Promise<string> {
315
+ console.log(chalk.blue("Deploying", contractName));
316
+
317
+ const { abi, bytecode } = await getContractData(contractName);
318
+ return deployContract(abi, bytecode, contractName);
319
+ }
320
+
321
+ /**
322
+ * Deploy a contract and return the address
323
+ * @param abi The contract interface
324
+ * @param bytecode The contract bytecode
325
+ * @param contractName The contract name (optional, used for logs)
326
+ * @returns Address of the deployed contract
327
+ */
328
+ async function deployContract(
329
+ abi: ContractInterface,
330
+ bytecode: string | { object: string },
331
+ contractName?: string,
332
+ retryCount = 0
333
+ ): Promise<string> {
334
+ try {
335
+ const factory = new ethers.ContractFactory(abi, bytecode, signer);
336
+ console.log(chalk.gray(`executing deployment of ${contractName} with nonce ${nonce}`));
337
+ const deployPromise = factory.deploy({
338
+ nonce: nonce++,
339
+ maxPriorityFeePerGas,
340
+ maxFeePerGas,
341
+ });
342
+ promises.push(deployPromise);
343
+ const { address } = await deployPromise;
344
+
345
+ console.log(chalk.green("Deployed", contractName, "to", address));
346
+ return address;
347
+ } catch (error: any) {
348
+ if (debug) console.error(error);
349
+ if (retryCount === 0 && error?.message.includes("transaction already imported")) {
350
+ // If the deployment failed because the transaction was already imported,
351
+ // retry with a higher priority fee
352
+ setInternalFeePerGas(priorityFeeMultiplier * 1.1);
353
+ return deployContract(abi, bytecode, contractName, retryCount++);
354
+ } else if (error?.message.includes("invalid bytecode")) {
355
+ throw new MUDError(
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.`
357
+ );
358
+ } else if (error?.message.includes("CreateContractLimit")) {
359
+ throw new MUDError(`Error deploying ${contractName}: CreateContractLimit exceeded.`);
360
+ } else throw error;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Deploy a contract and return the address
366
+ * @param contractName Name of the contract to deploy (must exist in the file system)
367
+ * @returns Address of the deployed contract
368
+ *
369
+ * NOTE: Forge deploy seems to be slightly slower than ethers
370
+ * (probably due to the extra overhead spawning a child process to run forge),
371
+ * so we mostly use ethersDeployContract here.
372
+ * However, for contracts not in the user directory (eg. the vanilla World contract),
373
+ * using forge is more convenient because it automatically finds the contract in the @latticexyz/world package.
374
+ */
375
+ // async function forgeDeployContract(contractName: string): Promise<string> {
376
+ // console.log(chalk.blue("Deploying", contractName));
377
+
378
+ // const { deployedTo } = JSON.parse(
379
+ // await forge(
380
+ // ["create", contractName, "--rpc-url", rpc, "--private-key", privateKey, "--json", "--nonce", String(nonce++)],
381
+ // { profile, silent: true }
382
+ // )
383
+ // );
384
+ // return deployedTo;
385
+ // }
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
+
399
+ /**
400
+ * Only await gas estimation (for speed), only execute if gas estimation succeeds (for safety)
401
+ */
402
+ async function fastTxExecute<C extends { estimateGas: any; [key: string]: any }, F extends keyof C>(
403
+ contract: C,
404
+ func: F,
405
+ args: ArgumentsType<C[F]>,
406
+ retryCount = 0
407
+ ): Promise<Awaited<ReturnType<C[F]>>> {
408
+ const functionName = `${func as string}(${args.map((arg) => `'${arg}'`).join(",")})`;
409
+ try {
410
+ const gasLimit = await contract.estimateGas[func].apply(null, args);
411
+ console.log(chalk.gray(`executing transaction: ${functionName} with nonce ${nonce}`));
412
+ const txPromise = contract[func].apply(null, [
413
+ ...args,
414
+ { gasLimit, nonce: nonce++, maxPriorityFeePerGas, maxFeePerGas },
415
+ ]);
416
+ promises.push(txPromise);
417
+ return txPromise;
418
+ } catch (error: any) {
419
+ if (debug) console.error(error);
420
+ if (retryCount === 0 && error?.message.includes("transaction already imported")) {
421
+ // If the deployment failed because the transaction was already imported,
422
+ // retry with a higher priority fee
423
+ setInternalFeePerGas(priorityFeeMultiplier * 1.1);
424
+ return fastTxExecute(contract, func, args, retryCount++);
425
+ } else throw new MUDError(`Gas estimation error for ${functionName}: ${error?.reason}`);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Load the contract's abi and bytecode from the file system
431
+ * @param contractName: Name of the contract to load
432
+ */
433
+ async function getContractData(contractName: string): Promise<{ bytecode: string; abi: Fragment[] }> {
434
+ let data: any;
435
+ const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json");
436
+ try {
437
+ data = JSON.parse(readFileSync(contractDataPath, "utf8"));
438
+ } catch (error: any) {
439
+ throw new MUDError(`Error reading file at ${contractDataPath}`);
440
+ }
441
+
442
+ const bytecode = data?.bytecode?.object;
443
+ if (!bytecode) throw new MUDError(`No bytecode found in ${contractDataPath}`);
444
+
445
+ const abi = data?.abi;
446
+ if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`);
447
+
448
+ return { abi, bytecode };
449
+ }
450
+
451
+ /**
452
+ * Set the maxFeePerGas and maxPriorityFeePerGas based on the current base fee and the given multiplier.
453
+ * The multiplier is used to allow replacing pending transactions.
454
+ * @param multiplier Multiplier to apply to the base fee
455
+ */
456
+ async function setInternalFeePerGas(multiplier: number) {
457
+ // Compute maxFeePerGas and maxPriorityFeePerGas like ethers, but allow for a multiplier to allow replacing pending transactions
458
+ const feeData = await provider.getFeeData();
459
+ if (!feeData.lastBaseFeePerGas) throw new MUDError("Can not fetch lastBaseFeePerGas from RPC");
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);
463
+ maxFeePerGas = feeData.lastBaseFeePerGas.mul(2).add(maxPriorityFeePerGas);
464
+ }
465
+ }
466
+
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);
512
+ }
@@ -1,7 +1,7 @@
1
1
  import { execa } from "execa";
2
2
  import { copyFileSync, mkdirSync, readdirSync, rmSync } from "fs";
3
3
  import path from "path";
4
- import { getOutDirectory } from "../forgeConfig.js";
4
+ import { getOutDirectory } from "../foundry.js";
5
5
 
6
6
  export async function forgeBuild(options?: { clear?: boolean }) {
7
7
  if (options?.clear) {
@@ -4,7 +4,7 @@ import { extractIdFromFile } from "./ids.js";
4
4
  import { rmSync, writeFileSync } from "fs";
5
5
  import path from "path";
6
6
  import { filterAbi, forgeBuild } from "./build.js";
7
- import { getOutDirectory, getSrcDirectory } from "../forgeConfig.js";
7
+ import { getOutDirectory, getSrcDirectory } from "../foundry.js";
8
8
  import { systemsDir } from "./constants.js";
9
9
 
10
10
  export async function generateAbiTypes(
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { ZodError } from "zod";
2
+ import { z, ZodError, ZodIssueCode } from "zod";
3
3
  import { fromZodError, ValidationError } from "zod-validation-error";
4
4
 
5
5
  // Wrapper with preset styles, only requires a `prefix`
@@ -21,7 +21,15 @@ export class NotESMConfigError extends Error {
21
21
  message = "MUD config must be an ES module";
22
22
  }
23
23
 
24
- export function logError(error: Error) {
24
+ export class MUDError extends Error {
25
+ name = "MUDError";
26
+ }
27
+
28
+ export function UnrecognizedSystemErrorFactory(path: string[], systemName: string) {
29
+ return new z.ZodError([{ code: ZodIssueCode.custom, path: path, message: `Unrecognized system: "${systemName}"` }]);
30
+ }
31
+
32
+ export function logError(error: unknown) {
25
33
  if (error instanceof ValidationError) {
26
34
  console.log(chalk.redBright(error.message));
27
35
  } else if (error instanceof ZodError) {
@@ -44,6 +52,8 @@ export function logError(error: Error) {
44
52
  console.log(
45
53
  chalk.blue(`Please name your config file \`mud.config.mts\`, or use \`type: "module"\` in package.json`)
46
54
  );
55
+ } else if (error instanceof MUDError) {
56
+ console.log(chalk.red(error));
47
57
  } else {
48
58
  console.log(error);
49
59
  }
@@ -0,0 +1,22 @@
1
+ import chalk from "chalk";
2
+ import { execa, Options } from "execa";
3
+
4
+ /**
5
+ * Executes the given command, returns the stdout, and logs the command to the console.
6
+ * Throws an error if the command fails.
7
+ * @param command The command to execute
8
+ * @param args The arguments to pass to the command
9
+ * @returns The stdout of the command
10
+ */
11
+ export async function execLog(command: string, args: string[], options?: Options<string>): Promise<string> {
12
+ const commandString = `${command} ${args.join(" ")}`;
13
+ try {
14
+ console.log(chalk.gray(`running "${commandString}"`));
15
+ const { stdout } = await execa(command, args, { stdout: "pipe", stderr: "pipe", ...options });
16
+ return stdout;
17
+ } catch (error: any) {
18
+ let errorMessage = error?.stderr || error?.message || "";
19
+ errorMessage += chalk.red(`\nError running "${commandString}"`);
20
+ throw new Error(errorMessage);
21
+ }
22
+ }
@@ -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
+ }