ethnotary 1.0.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.
- package/README.md +514 -0
- package/cli/commands/account/index.js +855 -0
- package/cli/commands/config/index.js +369 -0
- package/cli/commands/contact/index.js +139 -0
- package/cli/commands/contract/index.js +197 -0
- package/cli/commands/data/index.js +536 -0
- package/cli/commands/tx/index.js +841 -0
- package/cli/commands/wallet/index.js +181 -0
- package/cli/index.js +624 -0
- package/cli/utils/auth.js +146 -0
- package/cli/utils/constants.js +68 -0
- package/cli/utils/contacts.js +131 -0
- package/cli/utils/contracts.js +269 -0
- package/cli/utils/crosschain.js +278 -0
- package/cli/utils/networks.js +335 -0
- package/cli/utils/notifications.js +135 -0
- package/cli/utils/output.js +123 -0
- package/cli/utils/pin.js +89 -0
- package/data/balance.js +680 -0
- package/data/events.js +334 -0
- package/data/pending.js +261 -0
- package/data/scanWorker.js +169 -0
- package/data/token_cache.json +54 -0
- package/data/token_database.json +92 -0
- package/data/tokens.js +380 -0
- package/package.json +57 -0
package/cli/index.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
// Load environment variables
|
|
8
|
+
require('dotenv').config();
|
|
9
|
+
|
|
10
|
+
// Package info
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
|
|
13
|
+
// Check RPC configuration on startup - warn but don't exit
|
|
14
|
+
async function checkRpcConfig() {
|
|
15
|
+
const { listNetworks } = require('./utils/networks');
|
|
16
|
+
const networks = listNetworks();
|
|
17
|
+
const configured = networks.filter(n => n.configured);
|
|
18
|
+
|
|
19
|
+
if (configured.length === 0) {
|
|
20
|
+
console.log(chalk.yellow('\n⚠️ No RPC URLs configured. Some commands may not work.'));
|
|
21
|
+
console.log(chalk.gray('Run a transaction command to be prompted for an RPC URL.\n'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Run async check before starting CLI
|
|
26
|
+
(async () => {
|
|
27
|
+
await checkRpcConfig();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name('ethnotary')
|
|
31
|
+
.description('CLI for managing MultiSig accounts, transactions, and data queries across EVM networks')
|
|
32
|
+
.version(pkg.version);
|
|
33
|
+
|
|
34
|
+
// Global options available to all commands
|
|
35
|
+
program
|
|
36
|
+
.option('--json', 'Output in JSON format (machine-readable)')
|
|
37
|
+
.option('--private-key <key>', 'Use this private key directly')
|
|
38
|
+
.option('--network <name>', 'Network to use (defaults to all contract networks for data commands)')
|
|
39
|
+
.option('--yes', 'Skip all confirmation prompts')
|
|
40
|
+
.option('--dry-run', 'Simulate without executing');
|
|
41
|
+
|
|
42
|
+
// Register command groups
|
|
43
|
+
program.addCommand(require('./commands/wallet'));
|
|
44
|
+
program.addCommand(require('./commands/account'));
|
|
45
|
+
program.addCommand(require('./commands/tx'));
|
|
46
|
+
program.addCommand(require('./commands/data'));
|
|
47
|
+
program.addCommand(require('./commands/contract'));
|
|
48
|
+
program.addCommand(require('./commands/contact'));
|
|
49
|
+
program.addCommand(require('./commands/config'));
|
|
50
|
+
|
|
51
|
+
// Top-level shortcuts (like git)
|
|
52
|
+
const { setDefaultContract, getDefaultContract, listContracts } = require('./utils/contracts');
|
|
53
|
+
|
|
54
|
+
// ethnotary checkout <alias> - shortcut for contract checkout
|
|
55
|
+
program
|
|
56
|
+
.command('checkout <alias>')
|
|
57
|
+
.description('Switch to a different contract (like git checkout)')
|
|
58
|
+
.action((alias) => {
|
|
59
|
+
try {
|
|
60
|
+
setDefaultContract(alias);
|
|
61
|
+
const current = getDefaultContract();
|
|
62
|
+
console.log(chalk.green(`✓ Switched to "${alias}"`));
|
|
63
|
+
console.log(chalk.gray(` Address: ${current.address}`));
|
|
64
|
+
console.log(chalk.gray(` Network: ${current.network}`));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.log(chalk.red(`✗ ${error.message}`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ethnotary status - show current contract (like git status)
|
|
72
|
+
program
|
|
73
|
+
.command('status')
|
|
74
|
+
.description('Show the currently active contract')
|
|
75
|
+
.action(() => {
|
|
76
|
+
const current = getDefaultContract();
|
|
77
|
+
if (current) {
|
|
78
|
+
// Migrate old format if needed
|
|
79
|
+
const networks = current.networks || (current.network ? [current.network] : []);
|
|
80
|
+
console.log(chalk.cyan(`On account: ${chalk.bold(current.alias)}`));
|
|
81
|
+
console.log(chalk.gray(` Address: ${current.address}`));
|
|
82
|
+
console.log(chalk.gray(` Networks: ${networks.join(', ') || 'none'}`));
|
|
83
|
+
if (current.label) console.log(chalk.gray(` Label: ${current.label}`));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(chalk.yellow('No contract checked out.'));
|
|
86
|
+
console.log(chalk.gray('Use "ethnotary checkout <alias>" to switch to a contract.'));
|
|
87
|
+
const contracts = listContracts();
|
|
88
|
+
if (contracts.length > 0) {
|
|
89
|
+
console.log(chalk.gray(`\nAvailable contracts: ${contracts.map(c => c.alias).join(', ')}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ethnotary list - list all saved contracts
|
|
95
|
+
program
|
|
96
|
+
.command('list')
|
|
97
|
+
.description('List all saved contracts')
|
|
98
|
+
.action(() => {
|
|
99
|
+
const contracts = listContracts();
|
|
100
|
+
const current = getDefaultContract();
|
|
101
|
+
const opts = program.opts();
|
|
102
|
+
|
|
103
|
+
if (opts.json) {
|
|
104
|
+
console.log(JSON.stringify({
|
|
105
|
+
count: contracts.length,
|
|
106
|
+
current: current?.alias || null,
|
|
107
|
+
contracts
|
|
108
|
+
}, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (contracts.length === 0) {
|
|
113
|
+
console.log(chalk.yellow('No contracts saved.'));
|
|
114
|
+
console.log(chalk.gray('Use "ethnotary add" to save a contract.'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(chalk.bold(`\nSaved Contracts (${contracts.length}):\n`));
|
|
119
|
+
for (const c of contracts) {
|
|
120
|
+
const isActive = current && current.alias === c.alias;
|
|
121
|
+
const marker = isActive ? chalk.green('* ') : ' ';
|
|
122
|
+
const name = isActive ? chalk.green.bold(c.alias) : c.alias;
|
|
123
|
+
console.log(`${marker}${name}`);
|
|
124
|
+
console.log(chalk.gray(` Address: ${c.address}`));
|
|
125
|
+
// Support both old (network) and new (networks) format
|
|
126
|
+
const networks = c.networks || (c.network ? [c.network] : []);
|
|
127
|
+
console.log(chalk.gray(` Networks: ${networks.join(', ')}`));
|
|
128
|
+
if (c.label) console.log(chalk.gray(` Label: ${c.label}`));
|
|
129
|
+
if (c.decoupledFrom) console.log(chalk.yellow(` ⚠️ Decoupled from: ${c.decoupledFrom}`));
|
|
130
|
+
}
|
|
131
|
+
console.log('');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ethnotary add - save a contract (shortcut for contract add)
|
|
135
|
+
const { saveContract, removeContract } = require('./utils/contracts');
|
|
136
|
+
const { validateNetwork, ensureRpcConfigured, validateNetworks, ensureRpcsConfigured } = require('./utils/networks');
|
|
137
|
+
const { ethers } = require('ethers');
|
|
138
|
+
|
|
139
|
+
program
|
|
140
|
+
.command('add')
|
|
141
|
+
.description('Save a contract with an alias')
|
|
142
|
+
.requiredOption('--alias <alias>', 'Alias for the contract')
|
|
143
|
+
.requiredOption('--address <address>', 'Contract address')
|
|
144
|
+
.option('--chain-id <chainIds>', 'Chain ID(s), comma-separated (alternative to --network)')
|
|
145
|
+
.option('--label <label>', 'Optional label/description')
|
|
146
|
+
.option('--skip-validation', 'Skip Ethnotary contract validation')
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
const opts = program.opts();
|
|
149
|
+
try {
|
|
150
|
+
if (!ethers.isAddress(options.address)) {
|
|
151
|
+
console.log(chalk.red(`✗ Invalid address: ${options.address}`));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Resolve network(s) from --network or --chain-id (supports comma-separated)
|
|
156
|
+
const networkIdentifier = options.chainId || opts.network;
|
|
157
|
+
if (!networkIdentifier) {
|
|
158
|
+
console.log(chalk.red('✗ Either --network or --chain-id is required'));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check if multiple networks
|
|
163
|
+
const isMultiple = networkIdentifier.includes(',');
|
|
164
|
+
|
|
165
|
+
if (isMultiple) {
|
|
166
|
+
// Multi-network add - validates on each network, saves under single alias
|
|
167
|
+
const networks = validateNetworks(networkIdentifier);
|
|
168
|
+
if (!networks) {
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Ensure all RPCs are configured
|
|
173
|
+
const networksWithRpc = await ensureRpcsConfigured(networks, { json: opts.json });
|
|
174
|
+
if (!networksWithRpc) {
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!opts.json) {
|
|
179
|
+
console.log(chalk.cyan(`\nValidating contract on ${networksWithRpc.length} networks...\n`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const validNetworks = [];
|
|
183
|
+
const failedNetworks = [];
|
|
184
|
+
|
|
185
|
+
for (const { key, config, rpc } of networksWithRpc) {
|
|
186
|
+
// Validate contract on this network
|
|
187
|
+
if (!options.skipValidation) {
|
|
188
|
+
if (!opts.json) {
|
|
189
|
+
console.log(chalk.gray(`Validating on ${config.name}...`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
193
|
+
const contract = new ethers.Contract(
|
|
194
|
+
options.address,
|
|
195
|
+
['function isEthnotaryMultiSig() external view returns (bool)'],
|
|
196
|
+
provider
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const isValid = await contract.isEthnotaryMultiSig();
|
|
201
|
+
if (!isValid) {
|
|
202
|
+
console.log(chalk.red(`✗ Contract not found or invalid on ${config.name}`));
|
|
203
|
+
failedNetworks.push(key);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!opts.json) {
|
|
207
|
+
console.log(chalk.green(`✓ Valid on ${config.name}`));
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.log(chalk.red(`✗ Contract not found or invalid on ${config.name}`));
|
|
211
|
+
failedNetworks.push(key);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
validNetworks.push(key);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (validNetworks.length === 0) {
|
|
219
|
+
console.log(chalk.red('\n✗ Contract not valid on any of the specified networks'));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Save contract with all valid networks under single alias
|
|
224
|
+
const saved = saveContract(options.alias, options.address, validNetworks, options.label);
|
|
225
|
+
|
|
226
|
+
if (opts.json) {
|
|
227
|
+
console.log(JSON.stringify({
|
|
228
|
+
alias: options.alias,
|
|
229
|
+
address: options.address,
|
|
230
|
+
networks: validNetworks,
|
|
231
|
+
failedNetworks: failedNetworks,
|
|
232
|
+
label: options.label || '',
|
|
233
|
+
created: saved.created
|
|
234
|
+
}, null, 2));
|
|
235
|
+
} else {
|
|
236
|
+
console.log(chalk.green(`\n✓ Contract saved as "${options.alias}"`));
|
|
237
|
+
console.log(chalk.gray(` Address: ${options.address}`));
|
|
238
|
+
console.log(chalk.gray(` Networks: ${validNetworks.join(', ')}`));
|
|
239
|
+
if (failedNetworks.length > 0) {
|
|
240
|
+
console.log(chalk.yellow(` Failed: ${failedNetworks.join(', ')}`));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// Single network add
|
|
245
|
+
const resolved = validateNetwork(networkIdentifier);
|
|
246
|
+
if (!resolved) {
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const rpc = await ensureRpcConfigured(resolved.key, resolved.config, { json: opts.json });
|
|
251
|
+
if (!rpc) {
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Validate that this is an Ethnotary MultiSig contract
|
|
256
|
+
if (!options.skipValidation) {
|
|
257
|
+
if (!opts.json) {
|
|
258
|
+
console.log(chalk.gray('Validating Ethnotary contract...'));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
262
|
+
const contract = new ethers.Contract(
|
|
263
|
+
options.address,
|
|
264
|
+
['function isEthnotaryMultiSig() external view returns (bool)'],
|
|
265
|
+
provider
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const isValid = await contract.isEthnotaryMultiSig();
|
|
270
|
+
if (!isValid) {
|
|
271
|
+
console.log(chalk.red(`✗ Contract at ${options.address} is not a valid Ethnotary MultiSig`));
|
|
272
|
+
console.log(chalk.yellow('\nPlease double-check your contract address.'));
|
|
273
|
+
console.log(chalk.gray('To create a new Ethnotary MultiSig, run:'));
|
|
274
|
+
console.log(chalk.white(' ethnotary create --owners <addr1,addr2,...> --required <number> --pin <pin> --name <name>'));
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.log(chalk.red(`✗ Contract at ${options.address} is not a valid Ethnotary MultiSig`));
|
|
279
|
+
console.log(chalk.yellow('\nPlease double-check your contract address.'));
|
|
280
|
+
console.log(chalk.gray('To create a new Ethnotary MultiSig, run:'));
|
|
281
|
+
console.log(chalk.white(' ethnotary create --owners <addr1,addr2,...> --required <number> --pin <pin> --name <name>'));
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const saved = saveContract(options.alias, options.address, resolved.key, options.label);
|
|
287
|
+
|
|
288
|
+
if (opts.json) {
|
|
289
|
+
console.log(JSON.stringify({
|
|
290
|
+
alias: options.alias,
|
|
291
|
+
address: options.address,
|
|
292
|
+
network: resolved.key,
|
|
293
|
+
chainId: resolved.config.chainId,
|
|
294
|
+
label: options.label || '',
|
|
295
|
+
created: saved.created
|
|
296
|
+
}, null, 2));
|
|
297
|
+
} else {
|
|
298
|
+
console.log(chalk.green(`✓ Contract saved as "${options.alias}"`));
|
|
299
|
+
console.log(chalk.gray(` Address: ${options.address}`));
|
|
300
|
+
console.log(chalk.gray(` Network: ${resolved.config.name} (${resolved.key})`));
|
|
301
|
+
console.log(chalk.gray(` Chain ID: ${resolved.config.chainId}`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.log(chalk.red(`✗ ${error.message}`));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ethnotary remove - remove a saved contract (shortcut for contract remove)
|
|
311
|
+
program
|
|
312
|
+
.command('remove <alias>')
|
|
313
|
+
.description('Remove a saved contract')
|
|
314
|
+
.action((alias) => {
|
|
315
|
+
const opts = program.opts();
|
|
316
|
+
try {
|
|
317
|
+
removeContract(alias);
|
|
318
|
+
if (opts.json) {
|
|
319
|
+
console.log(JSON.stringify({ alias, status: 'removed' }, null, 2));
|
|
320
|
+
} else {
|
|
321
|
+
console.log(chalk.green(`✓ Contract "${alias}" removed`));
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.log(chalk.red(`✗ ${error.message}`));
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ethnotary create - shortcut for account create
|
|
330
|
+
program
|
|
331
|
+
.command('create')
|
|
332
|
+
.description('Deploy a new MultiSig account (shortcut for account create)')
|
|
333
|
+
.requiredOption('--owners <addresses>', 'Comma-separated list of owner addresses')
|
|
334
|
+
.requiredOption('--required <number>', 'Number of required confirmations', parseInt)
|
|
335
|
+
.requiredOption('--pin <pin>', 'PIN for account management')
|
|
336
|
+
.requiredOption('--name <name>', 'Name for the MultiSig account')
|
|
337
|
+
.option('--chain-id <chainIds>', 'Chain ID(s), comma-separated (alternative to --network)')
|
|
338
|
+
.action(async (options) => {
|
|
339
|
+
const opts = program.opts();
|
|
340
|
+
const { ethers } = require('ethers');
|
|
341
|
+
const { getWallet } = require('./utils/auth');
|
|
342
|
+
const { computePinHash } = require('./utils/pin');
|
|
343
|
+
const { listContracts, removeContract } = require('./utils/contracts');
|
|
344
|
+
const { MSA_FACTORY_ABI, getFactoryAddress } = require('./utils/constants');
|
|
345
|
+
|
|
346
|
+
// Resolve network(s) from --network or --chain-id
|
|
347
|
+
const networkIdentifier = options.chainId || opts.network;
|
|
348
|
+
if (!networkIdentifier) {
|
|
349
|
+
console.log(chalk.red('✗ Either --network or --chain-id is required'));
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Parse owners
|
|
354
|
+
const owners = options.owners.split(',').map(addr => addr.trim());
|
|
355
|
+
for (const owner of owners) {
|
|
356
|
+
if (!ethers.isAddress(owner)) {
|
|
357
|
+
console.log(chalk.red(`✗ Invalid owner address: ${owner}`));
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (options.required < 1 || options.required > owners.length) {
|
|
363
|
+
console.log(chalk.red(`✗ Required confirmations must be between 1 and ${owners.length}`));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Parse networks
|
|
368
|
+
const networkKeys = networkIdentifier.split(',').map(n => n.trim());
|
|
369
|
+
const networksToProcess = [];
|
|
370
|
+
|
|
371
|
+
for (const netKey of networkKeys) {
|
|
372
|
+
const resolved = validateNetwork(netKey);
|
|
373
|
+
if (!resolved) {
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
networksToProcess.push(resolved);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Ensure all RPCs are configured
|
|
380
|
+
const networksWithRpc = await ensureRpcsConfigured(
|
|
381
|
+
networksToProcess.map(n => n),
|
|
382
|
+
{ json: opts.json }
|
|
383
|
+
);
|
|
384
|
+
if (!networksWithRpc) {
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get wallet ONCE (single password prompt)
|
|
389
|
+
let wallet;
|
|
390
|
+
try {
|
|
391
|
+
wallet = await getWallet(opts);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.log(chalk.red(`✗ ${err.message}`));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Compute PIN hash
|
|
398
|
+
const pinHash = computePinHash(options.pin);
|
|
399
|
+
|
|
400
|
+
// Run pre-flight / dry-run across all networks
|
|
401
|
+
if (!opts.json) {
|
|
402
|
+
console.log(chalk.cyan(`\n📋 Pre-flight Check for ${networksWithRpc.length} network(s)...\n`));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const preflightResults = [];
|
|
406
|
+
for (const { key, config, rpc } of networksWithRpc) {
|
|
407
|
+
try {
|
|
408
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
409
|
+
const signer = wallet.connect(provider);
|
|
410
|
+
|
|
411
|
+
const factoryAddress = getFactoryAddress(key);
|
|
412
|
+
const factory = new ethers.Contract(factoryAddress, MSA_FACTORY_ABI, signer);
|
|
413
|
+
|
|
414
|
+
// Predict address
|
|
415
|
+
let predictedAddress = null;
|
|
416
|
+
try {
|
|
417
|
+
predictedAddress = await factory.predictMSAAddress(owners, options.required, pinHash, options.name);
|
|
418
|
+
} catch (e) {
|
|
419
|
+
// Factory might not be deployed
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check if already deployed
|
|
423
|
+
let alreadyDeployed = false;
|
|
424
|
+
if (predictedAddress) {
|
|
425
|
+
const code = await provider.getCode(predictedAddress);
|
|
426
|
+
alreadyDeployed = code !== '0x';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Get wallet balance and estimate gas
|
|
430
|
+
const balance = await provider.getBalance(wallet.address);
|
|
431
|
+
const feeData = await provider.getFeeData();
|
|
432
|
+
const gasPrice = feeData.gasPrice || feeData.maxFeePerGas || ethers.parseUnits('20', 'gwei');
|
|
433
|
+
const estimatedGas = 500000n; // Approximate gas for deployment
|
|
434
|
+
const deploymentFee = 1000000000000000n; // 0.001 ETH
|
|
435
|
+
const estimatedCost = (estimatedGas * gasPrice) + deploymentFee;
|
|
436
|
+
const sufficient = balance >= estimatedCost;
|
|
437
|
+
|
|
438
|
+
preflightResults.push({
|
|
439
|
+
network: key,
|
|
440
|
+
networkName: config.name,
|
|
441
|
+
rpc,
|
|
442
|
+
predictedAddress,
|
|
443
|
+
alreadyDeployed,
|
|
444
|
+
balance: ethers.formatEther(balance),
|
|
445
|
+
estimatedCost: ethers.formatEther(estimatedCost),
|
|
446
|
+
sufficient,
|
|
447
|
+
ready: sufficient && !alreadyDeployed && predictedAddress !== null,
|
|
448
|
+
error: predictedAddress === null ? 'Factory not deployed or not responding' : null
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (!opts.json) {
|
|
452
|
+
if (alreadyDeployed) {
|
|
453
|
+
console.log(chalk.yellow(` ⚠ ${config.name}: Already deployed at ${predictedAddress}`));
|
|
454
|
+
} else if (!predictedAddress) {
|
|
455
|
+
console.log(chalk.red(` ✗ ${config.name}: Factory not available`));
|
|
456
|
+
} else if (!sufficient) {
|
|
457
|
+
console.log(chalk.red(` ✗ ${config.name}: Insufficient balance`));
|
|
458
|
+
console.log(chalk.gray(` Balance: ${ethers.formatEther(balance)} ETH`));
|
|
459
|
+
console.log(chalk.gray(` Required: ~${ethers.formatEther(estimatedCost)} ETH`));
|
|
460
|
+
} else {
|
|
461
|
+
console.log(chalk.green(` ✓ ${config.name}: Ready`));
|
|
462
|
+
console.log(chalk.gray(` Predicted: ${predictedAddress}`));
|
|
463
|
+
console.log(chalk.gray(` Balance: ${ethers.formatEther(balance)} ETH`));
|
|
464
|
+
console.log(chalk.gray(` Est. cost: ~${ethers.formatEther(estimatedCost)} ETH`));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
preflightResults.push({
|
|
469
|
+
network: key,
|
|
470
|
+
networkName: config.name,
|
|
471
|
+
ready: false,
|
|
472
|
+
error: err.message
|
|
473
|
+
});
|
|
474
|
+
if (!opts.json) {
|
|
475
|
+
console.log(chalk.red(` ✗ ${config.name}: ${err.message}`));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const readyNetworks = preflightResults.filter(r => r.ready);
|
|
481
|
+
const notReadyNetworks = preflightResults.filter(r => !r.ready);
|
|
482
|
+
|
|
483
|
+
if (readyNetworks.length === 0) {
|
|
484
|
+
console.log(chalk.red('\n✗ No networks ready for deployment'));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Show summary and single confirmation
|
|
489
|
+
if (!opts.json && !opts.yes) {
|
|
490
|
+
console.log(chalk.cyan('\n📝 Deployment Summary:\n'));
|
|
491
|
+
console.log(chalk.gray(` Name: ${options.name}`));
|
|
492
|
+
console.log(chalk.gray(` Owners: ${owners.join(', ')}`));
|
|
493
|
+
console.log(chalk.gray(` Required: ${options.required}`));
|
|
494
|
+
console.log(chalk.gray(` Networks ready: ${readyNetworks.map(r => r.network).join(', ')}`));
|
|
495
|
+
if (notReadyNetworks.length > 0) {
|
|
496
|
+
console.log(chalk.yellow(` Networks skipped: ${notReadyNetworks.map(r => r.network).join(', ')}`));
|
|
497
|
+
}
|
|
498
|
+
if (readyNetworks[0]?.predictedAddress) {
|
|
499
|
+
console.log(chalk.gray(` Predicted address: ${readyNetworks[0].predictedAddress}`));
|
|
500
|
+
}
|
|
501
|
+
console.log('');
|
|
502
|
+
|
|
503
|
+
const inquirer = require('inquirer');
|
|
504
|
+
const { confirm } = await inquirer.prompt([{
|
|
505
|
+
type: 'confirm',
|
|
506
|
+
name: 'confirm',
|
|
507
|
+
message: `Deploy to ${readyNetworks.length} network(s)?`,
|
|
508
|
+
default: true
|
|
509
|
+
}]);
|
|
510
|
+
if (!confirm) {
|
|
511
|
+
console.log(chalk.yellow('Deployment cancelled'));
|
|
512
|
+
process.exit(0);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Dry run - just show results
|
|
517
|
+
if (opts.dryRun) {
|
|
518
|
+
if (opts.json) {
|
|
519
|
+
console.log(JSON.stringify({
|
|
520
|
+
dryRun: true,
|
|
521
|
+
name: options.name,
|
|
522
|
+
owners,
|
|
523
|
+
required: options.required,
|
|
524
|
+
networks: preflightResults
|
|
525
|
+
}, null, 2));
|
|
526
|
+
} else {
|
|
527
|
+
console.log(chalk.cyan('\n✓ Dry run complete. No deployments made.'));
|
|
528
|
+
}
|
|
529
|
+
process.exit(0);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Execute deployments
|
|
533
|
+
const successfulNetworks = [];
|
|
534
|
+
const failedNetworks = [];
|
|
535
|
+
let deployedAddress = null;
|
|
536
|
+
|
|
537
|
+
for (const result of readyNetworks) {
|
|
538
|
+
const { network: key, networkName, rpc, predictedAddress } = result;
|
|
539
|
+
|
|
540
|
+
if (!opts.json) {
|
|
541
|
+
console.log(chalk.cyan(`\n📡 Deploying to ${networkName}...`));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
546
|
+
const signer = wallet.connect(provider);
|
|
547
|
+
|
|
548
|
+
const factoryAddress = getFactoryAddress(key);
|
|
549
|
+
const factory = new ethers.Contract(factoryAddress, MSA_FACTORY_ABI, signer);
|
|
550
|
+
const fee = 1000000000000000n; // 0.001 ETH
|
|
551
|
+
|
|
552
|
+
const tx = await factory.newMSA(owners, options.required, pinHash, options.name, { value: fee });
|
|
553
|
+
|
|
554
|
+
if (!opts.json) {
|
|
555
|
+
console.log(chalk.gray(` Transaction: ${tx.hash}`));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const receipt = await tx.wait();
|
|
559
|
+
|
|
560
|
+
// Get deployed address from event or use predicted
|
|
561
|
+
let actualAddress = predictedAddress;
|
|
562
|
+
for (const log of receipt.logs) {
|
|
563
|
+
try {
|
|
564
|
+
const parsed = factory.interface.parseLog(log);
|
|
565
|
+
if (parsed && parsed.name === 'NewMSACreated') {
|
|
566
|
+
actualAddress = parsed.args.msa;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!deployedAddress) {
|
|
573
|
+
deployedAddress = actualAddress;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
successfulNetworks.push(key);
|
|
577
|
+
|
|
578
|
+
if (!opts.json) {
|
|
579
|
+
console.log(chalk.green(` ✓ Deployed to ${actualAddress}`));
|
|
580
|
+
}
|
|
581
|
+
} catch (err) {
|
|
582
|
+
failedNetworks.push({ network: key, error: err.message });
|
|
583
|
+
if (!opts.json) {
|
|
584
|
+
console.log(chalk.red(` ✗ Failed: ${err.message}`));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Save contract with all successful networks
|
|
590
|
+
if (successfulNetworks.length > 0 && deployedAddress) {
|
|
591
|
+
saveContract(options.name, deployedAddress, successfulNetworks);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Final summary
|
|
595
|
+
if (!opts.json) {
|
|
596
|
+
if (successfulNetworks.length > 0) {
|
|
597
|
+
console.log(chalk.green(`\n✓ Deployed to ${successfulNetworks.length}/${readyNetworks.length} network(s)`));
|
|
598
|
+
console.log(chalk.gray(` Alias: ${options.name}`));
|
|
599
|
+
console.log(chalk.gray(` Address: ${deployedAddress}`));
|
|
600
|
+
console.log(chalk.gray(` Networks: ${successfulNetworks.join(', ')}`));
|
|
601
|
+
if (failedNetworks.length > 0) {
|
|
602
|
+
console.log(chalk.yellow(` Failed: ${failedNetworks.map(f => f.network).join(', ')}`));
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
console.log(chalk.red('\n✗ Deployment failed on all networks'));
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
console.log(JSON.stringify({
|
|
609
|
+
address: deployedAddress,
|
|
610
|
+
alias: options.name,
|
|
611
|
+
networks: successfulNetworks,
|
|
612
|
+
failed: failedNetworks
|
|
613
|
+
}, null, 2));
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Parse arguments
|
|
618
|
+
program.parse(process.argv);
|
|
619
|
+
|
|
620
|
+
// Show help if no command provided
|
|
621
|
+
if (!process.argv.slice(2).length) {
|
|
622
|
+
program.outputHelp();
|
|
623
|
+
}
|
|
624
|
+
})();
|