rocketh 0.9.2 → 0.10.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rocketh",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "deploy smart contract on ethereum-compatible networks",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -15,8 +15,9 @@
15
15
  "devDependencies": {
16
16
  "@types/figlet": "^1.5.8",
17
17
  "@types/node": "^20.11.19",
18
+ "@types/prompts": "^2.4.9",
18
19
  "abitype": "^1.0.0",
19
- "eip-1193": "^0.4.7",
20
+ "eip-1193": "^0.5.0",
20
21
  "ipfs-gateway-emulator": "4.2.1-ipfs.2",
21
22
  "rimraf": "^5.0.5",
22
23
  "tsup": "^8.0.2",
@@ -33,6 +34,7 @@
33
34
  "ldenv": "^0.3.9",
34
35
  "named-logs": "^0.2.2",
35
36
  "named-logs-console": "^0.3.0",
37
+ "prompts": "^2.4.2",
36
38
  "viem": "^2.7.11"
37
39
  },
38
40
  "scripts": {
package/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env node
2
2
  import {loadEnv} from 'ldenv';
3
- import {loadAndExecuteDeployments, readConfig} from '.';
3
+ import {ConfigOptions, loadAndExecuteDeployments, readConfig} from '.';
4
4
  import {Command} from 'commander';
5
5
  import pkg from '../package.json';
6
6
 
@@ -20,6 +20,5 @@ program
20
20
  .parse(process.argv);
21
21
 
22
22
  const options = program.opts();
23
- const config = readConfig(options as any);
24
23
 
25
- loadAndExecuteDeployments({...config, logLevel: 1});
24
+ loadAndExecuteDeployments({...(options as ConfigOptions), logLevel: 1, askBeforeProceeding: true});
@@ -5,12 +5,12 @@ import {UnknownDeployments} from './types';
5
5
 
6
6
  export function loadDeployments(
7
7
  deploymentsPath: string,
8
- subPath: string,
8
+ networkName: string,
9
9
  onlyABIAndAddress?: boolean,
10
10
  expectedChain?: {chainId: string; genesisHash?: `0x${string}`; deleteDeploymentsIfDifferentGenesisHash?: boolean}
11
11
  ): {deployments: UnknownDeployments; chainId?: string; genesisHash?: `0x${string}`} {
12
12
  const deploymentsFound: UnknownDeployments = {};
13
- const deployPath = path.join(deploymentsPath, subPath);
13
+ const deployPath = path.join(deploymentsPath, networkName);
14
14
 
15
15
  let filesStats;
16
16
  try {
@@ -34,7 +34,7 @@ export function loadDeployments(
34
34
  genesisHash = chainData.genesisHash;
35
35
  } else {
36
36
  throw new Error(
37
- `A '.chain' or '.chainId' file is expected to be present in the deployment folder for network ${subPath}`
37
+ `A '.chain' or '.chainId' file is expected to be present in the deployment folder for network ${networkName}`
38
38
  );
39
39
  }
40
40
  }
@@ -85,7 +85,9 @@ export async function createEnvironment<
85
85
  providedContext: ProvidedContext<Artifacts, NamedAccounts>
86
86
  ): Promise<{internal: InternalEnvironment; external: Environment<Artifacts, NamedAccounts, Deployments>}> {
87
87
  const provider =
88
- 'provider' in config ? config.provider : (new JSONRPCHTTPProvider(config.nodeUrl) as EIP1193ProviderWithoutEvents);
88
+ 'provider' in config.network
89
+ ? config.network.provider
90
+ : (new JSONRPCHTTPProvider(config.network.nodeUrl) as EIP1193ProviderWithoutEvents);
89
91
 
90
92
  const transport = custom(provider);
91
93
  const viemClient = createPublicClient({transport});
@@ -100,24 +102,32 @@ export async function createEnvironment<
100
102
 
101
103
  let networkName: string;
102
104
  let saveDeployments: boolean;
103
- let tags: {[tag: string]: boolean} = {};
105
+ let networkTags: {[tag: string]: boolean} = {};
106
+ for (const networkTag of config.network.tags) {
107
+ networkTags[networkTag] = true;
108
+ }
109
+
104
110
  if ('nodeUrl' in config) {
105
- networkName = config.networkName;
111
+ networkName = config.network.name;
106
112
  saveDeployments = true;
107
113
  } else {
108
- if (config.networkName) {
109
- networkName = config.networkName;
114
+ if (config.network.name) {
115
+ networkName = config.network.name;
110
116
  } else {
111
117
  networkName = 'memory';
112
118
  }
113
119
  if (networkName === 'memory' || networkName === 'hardhat') {
114
- tags['memory'] = true;
120
+ networkTags['memory'] = true;
115
121
  saveDeployments = false;
116
122
  } else {
117
123
  saveDeployments = true;
118
124
  }
119
125
  }
120
126
 
127
+ if (config.saveDeployments !== undefined) {
128
+ saveDeployments = config.saveDeployments;
129
+ }
130
+
121
131
  const resolvedAccounts: {[name: string]: ResolvedAccount} = {};
122
132
 
123
133
  const accountCache: {[name: string]: ResolvedAccount} = {};
@@ -205,16 +215,26 @@ export async function createEnvironment<
205
215
  artifacts: providedContext.artifacts as Artifacts,
206
216
  network: {
207
217
  name: networkName,
218
+ fork: config.network.fork,
208
219
  saveDeployments,
209
- tags,
220
+ tags: networkTags,
210
221
  },
211
222
  };
212
223
 
213
- const {deployments} = loadDeployments(config.deployments, context.network.name, false, {
214
- chainId,
215
- genesisHash,
216
- deleteDeploymentsIfDifferentGenesisHash: true,
217
- });
224
+ // console.log(`context`, JSON.stringify(context.network, null, 2));
225
+
226
+ const {deployments} = loadDeployments(
227
+ config.deployments,
228
+ context.network.name,
229
+ false,
230
+ context.network.fork
231
+ ? undefined
232
+ : {
233
+ chainId,
234
+ genesisHash,
235
+ deleteDeploymentsIfDifferentGenesisHash: true,
236
+ }
237
+ );
218
238
 
219
239
  const namedAccounts: {[name: string]: EIP1193Account} = {};
220
240
  const namedSigners: {[name: string]: NamedSigner} = {};
@@ -209,30 +209,49 @@ export type Context<
209
209
  artifacts: Artifacts;
210
210
  };
211
211
 
212
- type BaseConfig = {
213
- networkName?: string;
212
+ type NetworkConfigBase = {
213
+ name: string;
214
+ tags: string[];
215
+ fork?: boolean;
216
+ };
217
+ type NetworkConfigForJSONRPC = NetworkConfigBase & {
218
+ nodeUrl: string;
219
+ };
220
+
221
+ type NetworkConfigForEIP1193Provider = NetworkConfigBase & {
222
+ provider: EIP1193ProviderWithoutEvents;
223
+ };
224
+
225
+ export type NetworkConfig = NetworkConfigForJSONRPC | NetworkConfigForEIP1193Provider;
226
+
227
+ export type Config = {
228
+ network: NetworkConfig;
229
+ networkTags?: string[];
214
230
  scripts?: string;
215
231
  deployments?: string;
232
+ saveDeployments?: boolean;
216
233
 
217
234
  tags?: string[];
235
+ askBeforeProceeding?: boolean;
236
+
218
237
  logLevel?: number;
219
238
  // TODO
220
239
  gasPricing?: {};
221
240
  };
222
241
 
223
- type ConfigForJSONRPC = BaseConfig & {
224
- networkName: string;
225
- nodeUrl: string;
226
- };
227
-
228
- type ConfigForEIP1193Provider = BaseConfig & {
229
- provider: EIP1193ProviderWithoutEvents;
242
+ export type ResolvedConfig = Config & {
243
+ deployments: string;
244
+ scripts: string;
245
+ tags: string[];
246
+ network: {
247
+ name: string;
248
+ tags: string[];
249
+ fork?: boolean;
250
+ };
251
+ saveDeployments?: boolean;
252
+ askBeforeProceeding?: boolean;
230
253
  };
231
254
 
232
- export type Config = ConfigForJSONRPC | ConfigForEIP1193Provider;
233
-
234
- export type ResolvedConfig = Config & {deployments: string; scripts: string; tags: string[]; networkName: string};
235
-
236
255
  export interface Environment<
237
256
  Artifacts extends UnknownArtifacts = UnknownArtifacts,
238
257
  NamedAccounts extends UnresolvedUnknownNamedAccounts = UnresolvedUnknownNamedAccounts,
@@ -13,6 +13,10 @@ import type {
13
13
  import {createEnvironment} from '../environment';
14
14
  import {DeployScriptFunction, DeployScriptModule, ProvidedContext} from './types';
15
15
  import {logger, setLogLevel, spin} from '../internal/logging';
16
+ import {EIP1193GenericRequestProvider, EIP1193ProviderWithoutEvents} from 'eip-1193';
17
+ import {getRoughGasPriceEstimate} from '../utils/eth';
18
+ import prompts from 'prompts';
19
+ import {formatEther} from 'viem';
16
20
 
17
21
  if (!process.env['ROCKETH_SKIP_ESBUILD']) {
18
22
  require('esbuild-register/dist/node').register();
@@ -39,28 +43,71 @@ export function execute<
39
43
  return scriptModule as unknown as DeployScriptModule<Artifacts, NamedAccounts, ArgumentsType, Deployments>;
40
44
  }
41
45
 
42
- export type ConfigOptions = {network: string; deployments?: string; scripts?: string; tags?: string};
46
+ export type ConfigOptions = {
47
+ network?: string | {fork: string};
48
+ deployments?: string;
49
+ scripts?: string;
50
+ tags?: string;
51
+ logLevel?: number;
52
+ provider?: EIP1193ProviderWithoutEvents | EIP1193GenericRequestProvider;
53
+ ignoreMissingRPC?: boolean;
54
+ saveDeployments?: boolean;
55
+ askBeforeProceeding?: boolean;
56
+ };
43
57
 
44
- export function readConfig(options: ConfigOptions, extra?: {ignoreMissingRPC?: boolean}): Config {
45
- type Networks = {[name: string]: {rpcUrl: string}};
46
- type ConfigFile = {networks: Networks};
58
+ export function readConfig(options: ConfigOptions): Config {
59
+ type Networks = {[name: string]: {rpcUrl?: string; tags?: string[]}};
60
+ type ConfigFile = {networks: Networks; deployments?: string; scripts?: string};
47
61
  let configFile: ConfigFile | undefined;
48
62
  try {
49
63
  const configString = fs.readFileSync('./rocketh.json', 'utf-8');
50
64
  configFile = JSON.parse(configString);
51
65
  } catch {}
52
66
 
53
- let nodeUrl: string;
67
+ if (configFile) {
68
+ if (!options.deployments && configFile.deployments) {
69
+ options.deployments = configFile.deployments;
70
+ }
71
+ if (!options.scripts && configFile.scripts) {
72
+ options.scripts = configFile.scripts;
73
+ }
74
+ }
75
+
54
76
  const fromEnv = process.env['ETH_NODE_URI_' + options.network];
55
- if (typeof fromEnv === 'string') {
56
- nodeUrl = fromEnv;
57
- } else {
58
- if (configFile) {
59
- const network = configFile.networks && configFile.networks[options.network];
60
- if (network) {
61
- nodeUrl = network.rpcUrl;
77
+ const fork = typeof options.network !== 'string';
78
+ let networkName = 'memory';
79
+ if (options.network) {
80
+ if (typeof options.network === 'string') {
81
+ networkName = options.network;
82
+ } else if ('fork' in options.network) {
83
+ networkName = options.network.fork;
84
+ }
85
+ }
86
+
87
+ let networkTags: string[] = (configFile?.networks && configFile?.networks[networkName]?.tags) || [];
88
+ if (!options.provider) {
89
+ let nodeUrl: string;
90
+ if (typeof fromEnv === 'string') {
91
+ nodeUrl = fromEnv;
92
+ } else {
93
+ if (configFile) {
94
+ const network = configFile.networks && configFile.networks[networkName];
95
+ if (network && network.rpcUrl) {
96
+ nodeUrl = network.rpcUrl;
97
+ } else {
98
+ if (options?.ignoreMissingRPC) {
99
+ nodeUrl = '';
100
+ } else {
101
+ if (options.network === 'localhost') {
102
+ nodeUrl = 'http://127.0.0.1:8545';
103
+ } else {
104
+ logger.error(`network "${options.network}" is not configured. Please add it to the rocketh.json file`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ }
62
109
  } else {
63
- if (extra?.ignoreMissingRPC) {
110
+ if (options?.ignoreMissingRPC) {
64
111
  nodeUrl = '';
65
112
  } else {
66
113
  if (options.network === 'localhost') {
@@ -71,40 +118,52 @@ export function readConfig(options: ConfigOptions, extra?: {ignoreMissingRPC?: b
71
118
  }
72
119
  }
73
120
  }
74
- } else {
75
- if (extra?.ignoreMissingRPC) {
76
- nodeUrl = '';
77
- } else {
78
- if (options.network === 'localhost') {
79
- nodeUrl = 'http://127.0.0.1:8545';
80
- } else {
81
- logger.error(`network "${options.network}" is not configured. Please add it to the rocketh.json file`);
82
- process.exit(1);
83
- }
84
- }
85
121
  }
122
+ return {
123
+ network: {
124
+ nodeUrl,
125
+ name: networkName,
126
+ tags: networkTags,
127
+ fork,
128
+ },
129
+ deployments: options.deployments,
130
+ saveDeployments: options.saveDeployments,
131
+ scripts: options.scripts,
132
+ tags: typeof options.tags === 'undefined' ? undefined : options.tags.split(','),
133
+ logLevel: options.logLevel,
134
+ askBeforeProceeding: options.askBeforeProceeding,
135
+ };
136
+ } else {
137
+ return {
138
+ network: {
139
+ provider: options.provider as EIP1193ProviderWithoutEvents,
140
+ name: networkName,
141
+ tags: networkTags,
142
+ fork,
143
+ },
144
+ deployments: options.deployments,
145
+ saveDeployments: options.saveDeployments,
146
+ scripts: options.scripts,
147
+ tags: typeof options.tags === 'undefined' ? undefined : options.tags.split(','),
148
+ logLevel: options.logLevel,
149
+ askBeforeProceeding: options.askBeforeProceeding,
150
+ };
86
151
  }
87
-
88
- return {
89
- nodeUrl,
90
- networkName: options.network,
91
- deployments: options.deployments,
92
- scripts: options.scripts,
93
- tags: typeof options.tags === 'undefined' ? undefined : options.tags.split(','),
94
- };
95
152
  }
96
153
 
97
- export function readAndResolveConfig(options: ConfigOptions, extra?: {ignoreMissingRPC?: boolean}): ResolvedConfig {
98
- return resolveConfig(readConfig(options, extra));
154
+ export function readAndResolveConfig(options: ConfigOptions): ResolvedConfig {
155
+ return resolveConfig(readConfig(options));
99
156
  }
100
157
 
101
158
  export function resolveConfig(config: Config): ResolvedConfig {
102
159
  const resolvedConfig: ResolvedConfig = {
103
160
  ...config,
104
- networkName: config.networkName || 'memory',
161
+ network: config.network, // TODO default to || {name: 'memory'....}
105
162
  deployments: config.deployments || 'deployments',
106
163
  scripts: config.scripts || 'deploy',
107
164
  tags: config.tags || [],
165
+ networkTags: config.networkTags || [],
166
+ saveDeployments: config.saveDeployments,
108
167
  };
109
168
  return resolvedConfig;
110
169
  }
@@ -112,8 +171,8 @@ export function resolveConfig(config: Config): ResolvedConfig {
112
171
  export async function loadEnvironment<
113
172
  Artifacts extends UnknownArtifacts = UnknownArtifacts,
114
173
  NamedAccounts extends UnresolvedUnknownNamedAccounts = UnresolvedUnknownNamedAccounts
115
- >(config: Config, context: ProvidedContext<Artifacts, NamedAccounts>): Promise<Environment> {
116
- const resolvedConfig = resolveConfig(config);
174
+ >(options: ConfigOptions, context: ProvidedContext<Artifacts, NamedAccounts>): Promise<Environment> {
175
+ const resolvedConfig = readAndResolveConfig(options);
117
176
  const {external, internal} = await createEnvironment(resolvedConfig, context);
118
177
  return external;
119
178
  }
@@ -123,8 +182,10 @@ export async function loadAndExecuteDeployments<
123
182
  NamedAccounts extends UnresolvedUnknownNamedAccounts = UnresolvedUnknownNamedAccounts,
124
183
  ArgumentsType = undefined,
125
184
  Deployments extends UnknownDeployments = UnknownDeployments
126
- >(config: Config, args?: ArgumentsType): Promise<Environment> {
127
- const resolvedConfig = resolveConfig(config);
185
+ >(options: ConfigOptions, args?: ArgumentsType): Promise<Environment> {
186
+ const resolvedConfig = readAndResolveConfig(options);
187
+ // console.log(JSON.stringify(options, null, 2));
188
+ // console.log(JSON.stringify(resolvedConfig, null, 2));
128
189
  return executeDeployScripts<Artifacts, NamedAccounts, ArgumentsType, Deployments>(resolvedConfig, args);
129
190
  }
130
191
 
@@ -269,6 +330,30 @@ export async function executeDeployScripts<
269
330
  recurseDependencies(scriptFilePath);
270
331
  }
271
332
 
333
+ if (config.askBeforeProceeding) {
334
+ const gasPriceEstimate = await getRoughGasPriceEstimate(external.network.provider);
335
+ const prompt = await prompts({
336
+ type: 'confirm',
337
+ name: 'proceed',
338
+ message: `gas price is currently in this range:
339
+ slow: ${formatEther(gasPriceEstimate.slow.maxFeePerGas)} (priority: ${formatEther(
340
+ gasPriceEstimate.slow.maxPriorityFeePerGas
341
+ )})
342
+ average: ${formatEther(gasPriceEstimate.average.maxFeePerGas)} (priority: ${formatEther(
343
+ gasPriceEstimate.average.maxPriorityFeePerGas
344
+ )})
345
+ fast: ${formatEther(gasPriceEstimate.fast.maxFeePerGas)} (priority: ${formatEther(
346
+ gasPriceEstimate.fast.maxPriorityFeePerGas
347
+ )})
348
+
349
+ Do you want to proceed (note that gas price can change for each tx)`,
350
+ });
351
+
352
+ if (!prompt.proceed) {
353
+ process.exit();
354
+ }
355
+ }
356
+
272
357
  for (const deployScript of scriptsToRun.concat(scriptsToRunAtTheEnd)) {
273
358
  const filename = path.basename(deployScript.filePath);
274
359
  const relativeFilepath = path.relative('.', deployScript.filePath);
@@ -0,0 +1,103 @@
1
+ import {EIP1193BlockTag, EIP1193ProviderWithoutEvents} from 'eip-1193';
2
+
3
+ function avg(arr: bigint[]) {
4
+ const sum = arr.reduce((a: bigint, v: bigint) => a + v);
5
+ return sum / BigInt(arr.length);
6
+ }
7
+
8
+ type EIP1193FeeHistory = {
9
+ oldestBlock: string;
10
+ reward: `0x${string}`[][];
11
+ baseFeePerGas: string[];
12
+ gasUsedRatio: string[];
13
+ };
14
+
15
+ export type EstimateGasPriceOptions = {
16
+ blockCount: number;
17
+ newestBlock: EIP1193BlockTag;
18
+ rewardPercentiles: number[];
19
+ };
20
+
21
+ export type RoughEstimateGasPriceOptions = {
22
+ blockCount: number;
23
+ newestBlock: EIP1193BlockTag;
24
+ rewardPercentiles: [number, number, number];
25
+ };
26
+
27
+ export type GasPrice = {maxFeePerGas: bigint; maxPriorityFeePerGas: bigint};
28
+ export type EstimateGasPriceResult = GasPrice[];
29
+ export type RoughEstimateGasPriceResult = {slow: GasPrice; average: GasPrice; fast: GasPrice};
30
+
31
+ export async function getGasPriceEstimate(
32
+ provider: EIP1193ProviderWithoutEvents,
33
+ options?: Partial<EstimateGasPriceOptions>
34
+ ): Promise<EstimateGasPriceResult> {
35
+ const defaultOptions: EstimateGasPriceOptions = {
36
+ blockCount: 20,
37
+ newestBlock: 'pending',
38
+ rewardPercentiles: [10, 50, 80],
39
+ };
40
+ const optionsResolved = options ? {...defaultOptions, ...options} : defaultOptions;
41
+
42
+ const historicalBlocks = optionsResolved.blockCount;
43
+
44
+ const rawFeeHistory = await provider.request<EIP1193FeeHistory>({
45
+ method: 'eth_feeHistory',
46
+ params: [`0x${historicalBlocks.toString(16)}`, optionsResolved.newestBlock, optionsResolved.rewardPercentiles],
47
+ });
48
+
49
+ let blockNum = Number(rawFeeHistory.oldestBlock);
50
+ const lastBlock = blockNum + rawFeeHistory.reward.length;
51
+ let index = 0;
52
+ const blocksHistory: {number: number; baseFeePerGas: bigint; gasUsedRatio: number; priorityFeePerGas: bigint[]}[] =
53
+ [];
54
+ while (blockNum < lastBlock) {
55
+ blocksHistory.push({
56
+ number: blockNum,
57
+ baseFeePerGas: BigInt(rawFeeHistory.baseFeePerGas[index]),
58
+ gasUsedRatio: Number(rawFeeHistory.gasUsedRatio[index]),
59
+ priorityFeePerGas: rawFeeHistory.reward[index].map((x) => BigInt(x)),
60
+ });
61
+ blockNum += 1;
62
+ index += 1;
63
+ }
64
+
65
+ const percentilePriorityFeeAverages: bigint[] = [];
66
+ for (let i = 0; i < optionsResolved.rewardPercentiles.length; i++) {
67
+ percentilePriorityFeeAverages.push(avg(blocksHistory.map((b) => b.priorityFeePerGas[i])));
68
+ }
69
+
70
+ const baseFeePerGas = BigInt(rawFeeHistory.baseFeePerGas[rawFeeHistory.baseFeePerGas.length - 1]);
71
+
72
+ const result: EstimateGasPriceResult = [];
73
+ for (let i = 0; i < optionsResolved.rewardPercentiles.length; i++) {
74
+ result.push({
75
+ maxFeePerGas: percentilePriorityFeeAverages[i] + baseFeePerGas,
76
+ maxPriorityFeePerGas: percentilePriorityFeeAverages[i],
77
+ });
78
+ }
79
+ return result;
80
+ }
81
+
82
+ export async function getRoughGasPriceEstimate(
83
+ provider: EIP1193ProviderWithoutEvents,
84
+ options?: Partial<RoughEstimateGasPriceOptions>
85
+ ): Promise<RoughEstimateGasPriceResult> {
86
+ const defaultOptions: EstimateGasPriceOptions = {
87
+ blockCount: 20,
88
+ newestBlock: 'pending',
89
+ rewardPercentiles: [10, 50, 80],
90
+ };
91
+ const optionsResolved = options ? {...defaultOptions, ...options} : defaultOptions;
92
+
93
+ if (optionsResolved.rewardPercentiles.length !== 3) {
94
+ throw new Error(`rough gas estimate require 3 percentile, it defaults to [10,50,80]`);
95
+ }
96
+
97
+ const result = await getGasPriceEstimate(provider, optionsResolved);
98
+ return {
99
+ slow: result[0],
100
+ average: result[1],
101
+ fast: result[2],
102
+ };
103
+ }