@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/chunk-LFIOIY67.js +30 -0
- package/dist/chunk-LFIOIY67.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mud.js +20 -0
- package/dist/mud.js.map +1 -0
- package/package.json +77 -0
- package/src/commands/deploy.ts +49 -0
- package/src/commands/dev-contracts.ts +176 -0
- package/src/commands/devnode.ts +42 -0
- package/src/commands/faucet.ts +60 -0
- package/src/commands/gas-report.ts +193 -0
- package/src/commands/hello.ts +28 -0
- package/src/commands/index.ts +30 -0
- package/src/commands/set-version.ts +200 -0
- package/src/commands/tablegen.ts +33 -0
- package/src/commands/test.ts +70 -0
- package/src/commands/trace.ts +108 -0
- package/src/commands/tsgen.ts +34 -0
- package/src/commands/worldgen.ts +57 -0
- package/src/index.ts +2 -0
- package/src/modules.d.ts +11 -0
- package/src/mud.ts +36 -0
- package/src/render-ts/index.ts +5 -0
- package/src/render-ts/recsV1TableOptions.ts +39 -0
- package/src/render-ts/renderRecsV1Tables.ts +31 -0
- package/src/render-ts/schemaTypesToRecsTypeStrings.ts +202 -0
- package/src/render-ts/tsgen.ts +12 -0
- package/src/render-ts/types.ts +13 -0
- package/src/utils/deploy.ts +602 -0
- package/src/utils/deployHandler.ts +87 -0
- package/src/utils/errors.ts +29 -0
- package/src/utils/getChainId.ts +10 -0
- package/src/utils/getExistingContracts.ts +12 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/printMUD.ts +14 -0
- package/src/utils/worldtypes.ts +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@latticexyz/cli",
|
|
3
|
+
"version": "0.0.0-main-20230704121617",
|
|
4
|
+
"description": "Command line interface for mud",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/latticexyz/mud.git",
|
|
8
|
+
"directory": "packages/cli"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"types": "src/index.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"mud": "./dist/mud.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@ethersproject/abi": "^5.7.0",
|
|
21
|
+
"@ethersproject/providers": "^5.7.2",
|
|
22
|
+
"@improbable-eng/grpc-web": "^0.15.0",
|
|
23
|
+
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
|
|
24
|
+
"@typechain/ethers-v5": "^10.2.0",
|
|
25
|
+
"chalk": "^5.0.1",
|
|
26
|
+
"chokidar": "^3.5.3",
|
|
27
|
+
"dotenv": "^16.0.3",
|
|
28
|
+
"ejs": "^3.1.8",
|
|
29
|
+
"ethers": "^5.7.2",
|
|
30
|
+
"execa": "^7.0.0",
|
|
31
|
+
"glob": "^8.0.3",
|
|
32
|
+
"nice-grpc-web": "^2.0.1",
|
|
33
|
+
"openurl": "^1.1.1",
|
|
34
|
+
"path": "^0.12.7",
|
|
35
|
+
"strip-ansi": "^7.1.0",
|
|
36
|
+
"table": "^6.8.1",
|
|
37
|
+
"throttle-debounce": "^5.0.0",
|
|
38
|
+
"typechain": "^8.1.1",
|
|
39
|
+
"typescript": "^4.9.5",
|
|
40
|
+
"yargs": "^17.7.1",
|
|
41
|
+
"zod": "^3.21.4",
|
|
42
|
+
"zod-validation-error": "^1.3.0",
|
|
43
|
+
"@latticexyz/common": "1.42.0",
|
|
44
|
+
"@latticexyz/config": "1.42.0",
|
|
45
|
+
"@latticexyz/schema-type": "1.42.0",
|
|
46
|
+
"@latticexyz/services": "0.0.0-main-20230704121617",
|
|
47
|
+
"@latticexyz/solecs": "1.42.0",
|
|
48
|
+
"@latticexyz/std-contracts": "1.42.0",
|
|
49
|
+
"@latticexyz/store": "1.42.0",
|
|
50
|
+
"@latticexyz/utils": "1.42.0",
|
|
51
|
+
"@latticexyz/world": "1.42.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/ejs": "^3.1.1",
|
|
55
|
+
"@types/glob": "^7.2.0",
|
|
56
|
+
"@types/node": "^18.15.11",
|
|
57
|
+
"@types/openurl": "^1.0.0",
|
|
58
|
+
"@types/throttle-debounce": "^5.0.0",
|
|
59
|
+
"@types/yargs": "^17.0.10",
|
|
60
|
+
"ds-test": "https://github.com/dapphub/ds-test.git#c9ce3f25bde29fc5eb9901842bf02850dfd2d084",
|
|
61
|
+
"forge-std": "https://github.com/foundry-rs/forge-std.git#b4f121555729b3afb3c5ffccb62ff4b6e2818fd3",
|
|
62
|
+
"tsup": "^6.7.0",
|
|
63
|
+
"tsx": "^3.12.6",
|
|
64
|
+
"vitest": "0.31.4"
|
|
65
|
+
},
|
|
66
|
+
"gitHead": "914a1e0ae4a573d685841ca2ea921435057deb8f",
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "pnpm run build:js",
|
|
69
|
+
"build:js": "tsup && chmod +x ./dist/mud.js",
|
|
70
|
+
"clean": "pnpm run clean:js",
|
|
71
|
+
"clean:js": "rimraf dist",
|
|
72
|
+
"dev": "tsup --watch",
|
|
73
|
+
"generate-test-tables": "tsx ./scripts/generate-test-tables.ts",
|
|
74
|
+
"lint": "eslint . --ext .ts",
|
|
75
|
+
"test": "tsc --noEmit && pnpm run generate-test-tables && forge test"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CommandModule, Options } from "yargs";
|
|
2
|
+
import { logError } from "../utils/errors";
|
|
3
|
+
import { deployHandler, DeployOptions } from "../utils";
|
|
4
|
+
|
|
5
|
+
export const yDeployOptions = {
|
|
6
|
+
configPath: { type: "string", desc: "Path to the config file" },
|
|
7
|
+
clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
|
|
8
|
+
printConfig: { type: "boolean", desc: "Print the resolved config" },
|
|
9
|
+
profile: { type: "string", desc: "The foundry profile to use" },
|
|
10
|
+
debug: { type: "boolean", desc: "Print debug logs, like full error messages" },
|
|
11
|
+
priorityFeeMultiplier: {
|
|
12
|
+
type: "number",
|
|
13
|
+
desc: "Multiply the estimated priority fee by the provided factor",
|
|
14
|
+
default: 1,
|
|
15
|
+
},
|
|
16
|
+
saveDeployment: { type: "boolean", desc: "Save the deployment info to a file", default: true },
|
|
17
|
+
rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" },
|
|
18
|
+
worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" },
|
|
19
|
+
srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." },
|
|
20
|
+
disableTxWait: { type: "boolean", desc: "Disable waiting for transactions to be confirmed.", default: false },
|
|
21
|
+
pollInterval: {
|
|
22
|
+
type: "number",
|
|
23
|
+
desc: "Interval in miliseconds to use to poll for transaction receipts / block inclusion",
|
|
24
|
+
default: 1000,
|
|
25
|
+
},
|
|
26
|
+
skipBuild: { type: "boolean", desc: "Skip rebuilding the contracts before deploying" },
|
|
27
|
+
} satisfies Record<keyof DeployOptions, Options>;
|
|
28
|
+
|
|
29
|
+
const commandModule: CommandModule<DeployOptions, DeployOptions> = {
|
|
30
|
+
command: "deploy",
|
|
31
|
+
|
|
32
|
+
describe: "Deploy MUD contracts",
|
|
33
|
+
|
|
34
|
+
builder(yargs) {
|
|
35
|
+
return yargs.options(yDeployOptions);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async handler(args) {
|
|
39
|
+
try {
|
|
40
|
+
await deployHandler(args);
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
logError(error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
process.exit(0);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default commandModule;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
import { anvil, forge, getRpcUrl, getScriptDirectory, getSrcDirectory } from "@latticexyz/common/foundry";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import chokidar from "chokidar";
|
|
5
|
+
import { loadConfig, resolveConfigPath } from "@latticexyz/config/node";
|
|
6
|
+
import { StoreConfig } from "@latticexyz/store";
|
|
7
|
+
import { tablegen } from "@latticexyz/store/codegen";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { tsgen } from "../render-ts";
|
|
10
|
+
import { debounce } from "throttle-debounce";
|
|
11
|
+
import { worldgenHandler } from "./worldgen";
|
|
12
|
+
import { WorldConfig } from "@latticexyz/world";
|
|
13
|
+
import { deployHandler, logError, printMUD, worldtypes } from "../utils";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { rmSync } from "fs";
|
|
16
|
+
|
|
17
|
+
type Options = {
|
|
18
|
+
rpc?: string;
|
|
19
|
+
configPath?: string;
|
|
20
|
+
tsgenOutput: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const commandModule: CommandModule<Options, Options> = {
|
|
24
|
+
command: "dev-contracts",
|
|
25
|
+
|
|
26
|
+
describe: "Start a development server for MUD contracts",
|
|
27
|
+
|
|
28
|
+
builder(yargs) {
|
|
29
|
+
return yargs.options({
|
|
30
|
+
rpc: {
|
|
31
|
+
type: "string",
|
|
32
|
+
decs: "RPC endpoint of the development node. If none is provided, an anvil instance is spawned in the background on port 8545.",
|
|
33
|
+
},
|
|
34
|
+
configPath: {
|
|
35
|
+
type: "string",
|
|
36
|
+
decs: "Path to MUD config",
|
|
37
|
+
},
|
|
38
|
+
tsgenOutput: { type: "string", demandOption: true, desc: "Directory to output MUD typescript definition files" },
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async handler(args) {
|
|
43
|
+
if (!args.tsgenOutputDir) {
|
|
44
|
+
console.error("No output provided");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Initial cleanup
|
|
48
|
+
await forge(["clean"]);
|
|
49
|
+
|
|
50
|
+
const rpc = args.rpc ?? (await getRpcUrl());
|
|
51
|
+
const configPath = args.configPath ?? (await resolveConfigPath(args.configPath));
|
|
52
|
+
const srcDirectory = await getSrcDirectory();
|
|
53
|
+
const scriptDirectory = await getScriptDirectory();
|
|
54
|
+
const initialConfig = (await loadConfig(configPath)) as StoreConfig & WorldConfig;
|
|
55
|
+
|
|
56
|
+
// Initial run of all codegen steps before starting anvil
|
|
57
|
+
// (so clients can wait for everything to be ready before starting)
|
|
58
|
+
await handleConfigChange(initialConfig);
|
|
59
|
+
await handleContractsChange(initialConfig);
|
|
60
|
+
|
|
61
|
+
// Start an anvil instance in the background if no RPC url is provided
|
|
62
|
+
if (!args.rpc) {
|
|
63
|
+
console.log(chalk.gray("Cleaning devnode cache"));
|
|
64
|
+
const userHomeDir = homedir();
|
|
65
|
+
rmSync(path.join(userHomeDir, ".foundry", "anvil", "tmp"), { recursive: true, force: true });
|
|
66
|
+
|
|
67
|
+
const anvilArgs = ["--block-time", "1", "--block-base-fee-per-gas", "0"];
|
|
68
|
+
anvil(anvilArgs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const changedSinceLastHandled = {
|
|
72
|
+
config: false,
|
|
73
|
+
contracts: false,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const changeInProgress = {
|
|
77
|
+
current: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Watch for changes
|
|
81
|
+
const configWatcher = chokidar.watch([configPath, srcDirectory]);
|
|
82
|
+
configWatcher.on("all", async (_, updatePath) => {
|
|
83
|
+
if (updatePath.includes(configPath)) {
|
|
84
|
+
changedSinceLastHandled.config = true;
|
|
85
|
+
// We trigger contract changes if the config changed here instead of
|
|
86
|
+
// listening to changes in the codegen directory to avoid an infinite loop
|
|
87
|
+
changedSinceLastHandled.contracts = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (updatePath.includes(srcDirectory) || updatePath.includes(scriptDirectory)) {
|
|
91
|
+
// Ignore changes to codegen files to avoid an infinite loop
|
|
92
|
+
if (updatePath.includes(initialConfig.codegenDirectory)) return;
|
|
93
|
+
changedSinceLastHandled.contracts = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Trigger debounced onChange
|
|
97
|
+
handleChange();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const handleChange = debounce(100, async () => {
|
|
101
|
+
// Avoid handling changes multiple times in parallel
|
|
102
|
+
if (changeInProgress.current) return;
|
|
103
|
+
changeInProgress.current = true;
|
|
104
|
+
|
|
105
|
+
// Reset dirty flags
|
|
106
|
+
const { config, contracts } = changedSinceLastHandled;
|
|
107
|
+
changedSinceLastHandled.config = false;
|
|
108
|
+
changedSinceLastHandled.contracts = false;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Load latest config
|
|
112
|
+
const mudConfig = (await loadConfig(configPath)) as StoreConfig & WorldConfig;
|
|
113
|
+
|
|
114
|
+
// Handle changes
|
|
115
|
+
if (config) await handleConfigChange(mudConfig);
|
|
116
|
+
if (contracts) await handleContractsChange(mudConfig);
|
|
117
|
+
|
|
118
|
+
await deploy();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(chalk.red("MUD dev-contracts watcher failed to deploy config or contracts changes\n"));
|
|
121
|
+
logError(error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
changeInProgress.current = false;
|
|
125
|
+
if (changedSinceLastHandled.config || changedSinceLastHandled.contracts) {
|
|
126
|
+
console.log("Detected change while handling the previous change");
|
|
127
|
+
handleChange();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
printMUD();
|
|
131
|
+
console.log("MUD watching for changes...");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/** Codegen to run if config changes */
|
|
135
|
+
async function handleConfigChange(config: StoreConfig & WorldConfig) {
|
|
136
|
+
console.log(chalk.blue("mud.config.ts changed - regenerating tables and recs types"));
|
|
137
|
+
// Run tablegen to generate tables based on the config
|
|
138
|
+
const outPath = path.join(srcDirectory, config.codegenDirectory);
|
|
139
|
+
await tablegen(config, outPath);
|
|
140
|
+
|
|
141
|
+
// Run tsgen to regenerate recs types based on the mud config
|
|
142
|
+
await tsgen(config, args.tsgenOutput);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Codegen to run if contracts changed */
|
|
146
|
+
async function handleContractsChange(config: StoreConfig & WorldConfig) {
|
|
147
|
+
console.log(chalk.blue("contracts changed - regenerating interfaces and contract types"));
|
|
148
|
+
|
|
149
|
+
// Run worldgen to generate interfaces based on the systems
|
|
150
|
+
await worldgenHandler({ config, clean: true, srcDir: srcDirectory });
|
|
151
|
+
|
|
152
|
+
// Build the contracts
|
|
153
|
+
await forge(["build"]);
|
|
154
|
+
|
|
155
|
+
// Run typechain to rebuild typescript types for the contracts
|
|
156
|
+
await worldtypes();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Run after codegen if either mud config or contracts changed */
|
|
160
|
+
async function deploy() {
|
|
161
|
+
console.log(chalk.blue("redeploying World"));
|
|
162
|
+
await deployHandler({
|
|
163
|
+
configPath,
|
|
164
|
+
skipBuild: true,
|
|
165
|
+
priorityFeeMultiplier: 1,
|
|
166
|
+
disableTxWait: true,
|
|
167
|
+
pollInterval: 1000,
|
|
168
|
+
saveDeployment: true,
|
|
169
|
+
srcDir: srcDirectory,
|
|
170
|
+
rpc,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export default commandModule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { rmSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import type { CommandModule } from "yargs";
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
|
|
7
|
+
type Options = {
|
|
8
|
+
blocktime: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const commandModule: CommandModule<Options, Options> = {
|
|
12
|
+
command: "devnode",
|
|
13
|
+
|
|
14
|
+
describe: "Start a local Ethereum node for development",
|
|
15
|
+
|
|
16
|
+
builder(yargs) {
|
|
17
|
+
return yargs.options({
|
|
18
|
+
blocktime: { type: "number", default: 1, decs: "Interval in which new blocks are produced" },
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
async handler({ blocktime }) {
|
|
23
|
+
console.log("Clearing devnode history");
|
|
24
|
+
const userHomeDir = homedir();
|
|
25
|
+
rmSync(path.join(userHomeDir, ".foundry", "anvil", "tmp"), { recursive: true, force: true });
|
|
26
|
+
|
|
27
|
+
const anvilArgs = ["-b", String(blocktime), "--block-base-fee-per-gas", "0"];
|
|
28
|
+
console.log(`Running: anvil ${anvilArgs.join(" ")}`);
|
|
29
|
+
const child = execa("anvil", anvilArgs, {
|
|
30
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
process.on("SIGINT", () => {
|
|
34
|
+
console.log("\ngracefully shutting down from SIGINT (Crtl-C)");
|
|
35
|
+
child.kill();
|
|
36
|
+
process.exit();
|
|
37
|
+
});
|
|
38
|
+
await child;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default commandModule;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
import { FaucetServiceDefinition } from "@latticexyz/services/faucet";
|
|
3
|
+
import { createChannel, createClient } from "nice-grpc-web";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
|
|
6
|
+
|
|
7
|
+
type Options = {
|
|
8
|
+
dripDev?: boolean;
|
|
9
|
+
faucetUrl: string;
|
|
10
|
+
address: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a FaucetServiceClient
|
|
15
|
+
* @param url FaucetService URL
|
|
16
|
+
* @returns FaucetServiceClient
|
|
17
|
+
*/
|
|
18
|
+
function createFaucetService(url: string) {
|
|
19
|
+
return createClient(FaucetServiceDefinition, createChannel(url, NodeHttpTransport()));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const commandModule: CommandModule<Options, Options> = {
|
|
23
|
+
command: "faucet",
|
|
24
|
+
|
|
25
|
+
describe: "Interact with a MUD faucet",
|
|
26
|
+
|
|
27
|
+
builder(yargs) {
|
|
28
|
+
return yargs.options({
|
|
29
|
+
dripDev: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
desc: "Request a drip from the dev endpoint (requires faucet to have dev mode enabled)",
|
|
32
|
+
default: true,
|
|
33
|
+
},
|
|
34
|
+
faucetUrl: {
|
|
35
|
+
type: "string",
|
|
36
|
+
desc: "URL of the MUD faucet",
|
|
37
|
+
default: "https://faucet.testnet-mud-services.linfra.xyz",
|
|
38
|
+
},
|
|
39
|
+
address: {
|
|
40
|
+
type: "string",
|
|
41
|
+
desc: "Ethereum address to fund",
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async handler({ dripDev, faucetUrl, address }) {
|
|
48
|
+
const faucet = createFaucetService(faucetUrl);
|
|
49
|
+
|
|
50
|
+
if (dripDev) {
|
|
51
|
+
console.log(chalk.yellow("Dripping to", address));
|
|
52
|
+
await faucet.dripDev({ address });
|
|
53
|
+
console.log(chalk.yellow("Success"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.exit(0);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default commandModule;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { table, getBorderCharacters } from "table";
|
|
6
|
+
import stripAnsi from "strip-ansi";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Print the gas report to the console, save it to a file and compare it to a previous gas report if provided.
|
|
10
|
+
*
|
|
11
|
+
* Requires foundry to be installed. Inherit from GasReporter and use startGasReport/endGasReport to measure gas used.
|
|
12
|
+
*
|
|
13
|
+
* ```solidity
|
|
14
|
+
* contract MyContractTest is Test, GasReporter {
|
|
15
|
+
* function testBuffer() public pure {
|
|
16
|
+
* startGasReport("allocate a buffer");
|
|
17
|
+
* Buffer buffer = Buffer_.allocate(32);
|
|
18
|
+
* endGasReport();
|
|
19
|
+
*
|
|
20
|
+
* bytes32 value = keccak256("some data");
|
|
21
|
+
*
|
|
22
|
+
* startGasReport("append 32 bytes to a buffer");
|
|
23
|
+
* buffer.append(value);
|
|
24
|
+
* endGasReport();
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
type Options = {
|
|
31
|
+
path: string[];
|
|
32
|
+
save?: string;
|
|
33
|
+
compare?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type GasReportEntry = {
|
|
37
|
+
file: string;
|
|
38
|
+
test: string;
|
|
39
|
+
name: string;
|
|
40
|
+
gasUsed: number;
|
|
41
|
+
prevGasUsed?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type GasReport = GasReportEntry[];
|
|
45
|
+
|
|
46
|
+
const commandModule: CommandModule<Options, Options> = {
|
|
47
|
+
command: "gas-report",
|
|
48
|
+
|
|
49
|
+
describe: "Create a gas report",
|
|
50
|
+
|
|
51
|
+
builder(yargs) {
|
|
52
|
+
return yargs.options({
|
|
53
|
+
save: { type: "string", desc: "Save the gas report to a file" },
|
|
54
|
+
compare: { type: "string", desc: "Compare to an existing gas report" },
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async handler({ save, compare }) {
|
|
59
|
+
let gasReport: GasReport;
|
|
60
|
+
try {
|
|
61
|
+
gasReport = await runGasReport();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(error);
|
|
64
|
+
setTimeout(() => process.exit());
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If this gas report should be compared to an existing one, load the existing one
|
|
69
|
+
if (compare) {
|
|
70
|
+
try {
|
|
71
|
+
const compareGasReport: GasReport = JSON.parse(readFileSync(compare, "utf8"));
|
|
72
|
+
// Merge the previous gas report with the new one
|
|
73
|
+
gasReport = gasReport.map((entry) => {
|
|
74
|
+
const prevEntry = compareGasReport.find((e) => e.file === entry.file && e.name === entry.name);
|
|
75
|
+
return { ...entry, prevGasUsed: prevEntry?.gasUsed };
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
console.log(chalk.red(`Gas report to compare not found: ${compare}`));
|
|
79
|
+
compare = undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Print gas report
|
|
84
|
+
printGasReport(gasReport, compare);
|
|
85
|
+
|
|
86
|
+
// Save gas report to file if requested
|
|
87
|
+
if (save) saveGasReport(gasReport, save);
|
|
88
|
+
|
|
89
|
+
process.exit(0);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default commandModule;
|
|
94
|
+
|
|
95
|
+
async function runGasReport(): Promise<GasReport> {
|
|
96
|
+
console.log("Running gas report");
|
|
97
|
+
const gasReport: GasReport = [];
|
|
98
|
+
|
|
99
|
+
// Extract the logs from the child process
|
|
100
|
+
let stdout: string;
|
|
101
|
+
try {
|
|
102
|
+
// Run the generated file using forge
|
|
103
|
+
const child = execa("forge", ["test", "-vvv"], {
|
|
104
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
105
|
+
env: { GAS_REPORTER_ENABLED: "true" },
|
|
106
|
+
});
|
|
107
|
+
stdout = (await child).stdout;
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
console.log(error.stdout ?? error);
|
|
110
|
+
console.log(chalk.red("\n-----------\nError while running the gas report (see above)"));
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract the gas reports from the logs
|
|
115
|
+
const lines = stdout.split("\n").map(stripAnsi);
|
|
116
|
+
const gasReportPattern = /^\s*GAS REPORT: (\d+) (.*)$/;
|
|
117
|
+
const testFunctionPattern = /^\[(?:PASS|FAIL).*\] (\w+)\(\)/;
|
|
118
|
+
const testFilePattern = /^Running \d+ tests? for (.*):(.*)$/;
|
|
119
|
+
|
|
120
|
+
function nearestLine(pattern: RegExp, startIndex: number = lines.length - 1): number {
|
|
121
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
122
|
+
const line = lines[i];
|
|
123
|
+
if (pattern.test(line)) {
|
|
124
|
+
return i;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const matches = lines[i].match(gasReportPattern);
|
|
132
|
+
if (!matches) continue;
|
|
133
|
+
|
|
134
|
+
const gasUsed = parseInt(matches[1]);
|
|
135
|
+
const name = matches[2];
|
|
136
|
+
|
|
137
|
+
const testFunctionLineIndex = nearestLine(testFunctionPattern, i);
|
|
138
|
+
if (testFunctionLineIndex === -1) {
|
|
139
|
+
throw new Error("Could not find nearest test function, did `forge test` output change?");
|
|
140
|
+
}
|
|
141
|
+
const testFileLineIndex = nearestLine(testFilePattern, testFunctionLineIndex);
|
|
142
|
+
if (testFileLineIndex === -1) {
|
|
143
|
+
throw new Error("Could not find nearest test filename, did `forge test` output change?");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const functionMatches = lines[testFunctionLineIndex].match(testFunctionPattern);
|
|
147
|
+
if (!functionMatches) {
|
|
148
|
+
throw new Error("Could not parse test function name, did `forge test` output change?");
|
|
149
|
+
}
|
|
150
|
+
const fileMatches = lines[testFileLineIndex].match(testFilePattern);
|
|
151
|
+
if (!fileMatches) {
|
|
152
|
+
throw new Error("Could not parse test filename, did `forge test` output change?");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const test = functionMatches[1];
|
|
156
|
+
const file = fileMatches[1];
|
|
157
|
+
|
|
158
|
+
gasReport.push({ file, test, name, gasUsed });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
gasReport.sort((a, b) => a.file.localeCompare(b.file));
|
|
162
|
+
|
|
163
|
+
return gasReport;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printGasReport(gasReport: GasReport, compare?: string) {
|
|
167
|
+
if (compare) console.log(chalk.bold(`Gas report compared to ${compare}`));
|
|
168
|
+
|
|
169
|
+
const headers = [
|
|
170
|
+
chalk.bold("File"),
|
|
171
|
+
chalk.bold("Test"),
|
|
172
|
+
chalk.bold("Name"),
|
|
173
|
+
chalk.bold("Gas used"),
|
|
174
|
+
...(compare ? [chalk.bold("Prev gas used"), chalk.bold("Difference")] : []),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const values = gasReport.map((entry) => {
|
|
178
|
+
const diff = entry.prevGasUsed ? entry.gasUsed - entry.prevGasUsed : 0;
|
|
179
|
+
const diffEntry = diff > 0 ? chalk.red(`+${diff}`) : diff < 0 ? chalk.green(`${diff}`) : diff;
|
|
180
|
+
const compareColumns = compare ? [entry.prevGasUsed ?? "n/a", diffEntry] : [];
|
|
181
|
+
const gasUsedEntry = diff > 0 ? chalk.red(entry.gasUsed) : diff < 0 ? chalk.green(entry.gasUsed) : entry.gasUsed;
|
|
182
|
+
return [entry.file, entry.test, entry.name, gasUsedEntry, ...compareColumns];
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const rows = [headers, ...values];
|
|
186
|
+
|
|
187
|
+
console.log(table(rows, { border: getBorderCharacters("norc") }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function saveGasReport(gasReport: GasReport, path: string) {
|
|
191
|
+
console.log(chalk.bold(`Saving gas report to ${path}`));
|
|
192
|
+
writeFileSync(path, `${JSON.stringify(gasReport, null, 2)}\n`);
|
|
193
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs";
|
|
2
|
+
|
|
3
|
+
type Options = {
|
|
4
|
+
name: string;
|
|
5
|
+
upper: boolean | undefined;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const commandModule: CommandModule<Options, Options> = {
|
|
9
|
+
command: "hello <name>",
|
|
10
|
+
|
|
11
|
+
describe: "Greet <name> with Hello",
|
|
12
|
+
|
|
13
|
+
builder(yargs) {
|
|
14
|
+
return yargs
|
|
15
|
+
.options({
|
|
16
|
+
upper: { type: "boolean" },
|
|
17
|
+
})
|
|
18
|
+
.positional("name", { type: "string", demandOption: true });
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
handler({ name }) {
|
|
22
|
+
const greeting = `Gm, ${name}!`;
|
|
23
|
+
console.log(greeting);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default commandModule;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CommandModule } from "yargs";
|
|
2
|
+
|
|
3
|
+
import devnode from "./devnode";
|
|
4
|
+
import faucet from "./faucet";
|
|
5
|
+
import gasReport from "./gas-report";
|
|
6
|
+
import hello from "./hello";
|
|
7
|
+
import tablegen from "./tablegen";
|
|
8
|
+
import tsgen from "./tsgen";
|
|
9
|
+
import deploy from "./deploy";
|
|
10
|
+
import worldgen from "./worldgen";
|
|
11
|
+
import setVersion from "./set-version";
|
|
12
|
+
import test from "./test";
|
|
13
|
+
import trace from "./trace";
|
|
14
|
+
import devContracts from "./dev-contracts";
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
|
|
17
|
+
export const commands: CommandModule<any, any>[] = [
|
|
18
|
+
deploy,
|
|
19
|
+
devnode,
|
|
20
|
+
faucet,
|
|
21
|
+
gasReport,
|
|
22
|
+
hello,
|
|
23
|
+
tablegen,
|
|
24
|
+
tsgen,
|
|
25
|
+
worldgen,
|
|
26
|
+
setVersion,
|
|
27
|
+
test,
|
|
28
|
+
trace,
|
|
29
|
+
devContracts,
|
|
30
|
+
];
|