@latticexyz/cli 0.0.0-main-20230704121617

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