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
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const { ethers } = require('ethers');
|
|
3
|
+
const { createOutput } = require('../../utils/output');
|
|
4
|
+
const { getWallet } = require('../../utils/auth');
|
|
5
|
+
const { getNetwork, getRpcUrl } = require('../../utils/networks');
|
|
6
|
+
const { computePinHash, generateZkProof } = require('../../utils/pin');
|
|
7
|
+
const { saveContract, resolveAddress, getContractNetworks, getContract, getDefaultContract } = require('../../utils/contracts');
|
|
8
|
+
const { fetchAccountStateAcrossNetworks, analyzeAccountSync, handleDecoupling, preflightAccountOperation, displayPreflightResults } = require('../../utils/crosschain');
|
|
9
|
+
const { MULTISIG_ABI, MSA_FACTORY_ABI, getFactoryAddress } = require('../../utils/constants');
|
|
10
|
+
|
|
11
|
+
const account = new Command('account')
|
|
12
|
+
.description('Account management commands');
|
|
13
|
+
|
|
14
|
+
// account create - Deploy new MultiSig account
|
|
15
|
+
account
|
|
16
|
+
.command('create')
|
|
17
|
+
.description('Deploy a new MultiSig account')
|
|
18
|
+
.requiredOption('--owners <addresses>', 'Comma-separated list of owner addresses')
|
|
19
|
+
.requiredOption('--required <number>', 'Number of required confirmations', parseInt)
|
|
20
|
+
.requiredOption('--pin <pin>', 'PIN for account management')
|
|
21
|
+
.requiredOption('--name <name>', 'Name for the MultiSig account')
|
|
22
|
+
.action(async (options, command) => {
|
|
23
|
+
const globalOpts = command.parent.parent.opts();
|
|
24
|
+
const out = createOutput(globalOpts);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Parse owners
|
|
28
|
+
const owners = options.owners.split(',').map(addr => addr.trim());
|
|
29
|
+
for (const owner of owners) {
|
|
30
|
+
if (!ethers.isAddress(owner)) {
|
|
31
|
+
out.error(`Invalid owner address: ${owner}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.required < 1 || options.required > owners.length) {
|
|
37
|
+
out.error(`Required confirmations must be between 1 and ${owners.length}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get wallet
|
|
42
|
+
const wallet = await getWallet(globalOpts);
|
|
43
|
+
|
|
44
|
+
// Get network
|
|
45
|
+
const network = await getNetwork(globalOpts.network);
|
|
46
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
47
|
+
const signer = wallet.connect(provider);
|
|
48
|
+
|
|
49
|
+
// Compute PIN hash
|
|
50
|
+
const pinHash = computePinHash(options.pin);
|
|
51
|
+
|
|
52
|
+
// Account name (use alias if provided, otherwise default)
|
|
53
|
+
const accountName = options.name || options.saveAs || 'MultiSig';
|
|
54
|
+
|
|
55
|
+
// Get factory address from centralized config
|
|
56
|
+
const factoryAddress = getFactoryAddress(globalOpts.network);
|
|
57
|
+
const factory = new ethers.Contract(factoryAddress, MSA_FACTORY_ABI, signer);
|
|
58
|
+
|
|
59
|
+
// Predict address
|
|
60
|
+
out.startSpinner('Predicting contract address...');
|
|
61
|
+
let predictedAddress;
|
|
62
|
+
try {
|
|
63
|
+
predictedAddress = await factory.predictMSAAddress(owners, options.required, pinHash, accountName);
|
|
64
|
+
out.updateSpinner(`Predicted address: ${predictedAddress}`);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
out.failSpinner('Could not predict address');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if already deployed
|
|
70
|
+
if (predictedAddress) {
|
|
71
|
+
const code = await provider.getCode(predictedAddress);
|
|
72
|
+
if (code !== '0x') {
|
|
73
|
+
out.stopSpinner();
|
|
74
|
+
out.print({
|
|
75
|
+
address: predictedAddress,
|
|
76
|
+
status: 'already_deployed',
|
|
77
|
+
network: globalOpts.network
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Confirm if not --yes
|
|
84
|
+
if (!globalOpts.yes && !globalOpts.json) {
|
|
85
|
+
out.stopSpinner();
|
|
86
|
+
out.info(`Deploying MultiSig to ${network.name}`);
|
|
87
|
+
out.info(`Name: ${accountName}`);
|
|
88
|
+
out.info(`Owners: ${owners.join(', ')}`);
|
|
89
|
+
out.info(`Required: ${options.required}`);
|
|
90
|
+
out.info(`Predicted address: ${predictedAddress || 'unknown'}`);
|
|
91
|
+
|
|
92
|
+
const inquirer = require('inquirer');
|
|
93
|
+
const { confirm } = await inquirer.prompt([{
|
|
94
|
+
type: 'confirm',
|
|
95
|
+
name: 'confirm',
|
|
96
|
+
message: 'Proceed with deployment?',
|
|
97
|
+
default: true
|
|
98
|
+
}]);
|
|
99
|
+
if (!confirm) {
|
|
100
|
+
out.warn('Deployment cancelled');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Dry run check
|
|
106
|
+
if (globalOpts.dryRun) {
|
|
107
|
+
out.print({
|
|
108
|
+
dryRun: true,
|
|
109
|
+
predictedAddress,
|
|
110
|
+
owners,
|
|
111
|
+
required: options.required,
|
|
112
|
+
network: globalOpts.network,
|
|
113
|
+
pinHash
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Deploy
|
|
119
|
+
out.startSpinner('Deploying MultiSig account...');
|
|
120
|
+
const fee = 1000000000000000n; // 0.001 ETH
|
|
121
|
+
|
|
122
|
+
const tx = await factory.newMSA(owners, options.required, pinHash, accountName, { value: fee });
|
|
123
|
+
out.updateSpinner(`Transaction sent: ${tx.hash}`);
|
|
124
|
+
|
|
125
|
+
const receipt = await tx.wait();
|
|
126
|
+
|
|
127
|
+
// Find NewMSACreated event
|
|
128
|
+
let deployedAddress = predictedAddress;
|
|
129
|
+
const event = receipt.logs.find(log => {
|
|
130
|
+
try {
|
|
131
|
+
return factory.interface.parseLog(log)?.name === 'NewMSACreated';
|
|
132
|
+
} catch { return false; }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (event) {
|
|
136
|
+
const parsed = factory.interface.parseLog(event);
|
|
137
|
+
deployedAddress = parsed.args.msaAddress;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
out.succeedSpinner('MultiSig deployed successfully');
|
|
141
|
+
|
|
142
|
+
// Save contract with name as alias
|
|
143
|
+
saveContract(accountName, deployedAddress, globalOpts.network);
|
|
144
|
+
|
|
145
|
+
out.print({
|
|
146
|
+
address: deployedAddress,
|
|
147
|
+
txHash: receipt.hash,
|
|
148
|
+
network: globalOpts.network,
|
|
149
|
+
owners,
|
|
150
|
+
required: options.required,
|
|
151
|
+
name: accountName,
|
|
152
|
+
alias: accountName
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
out.error(error.message);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// account info - Show account information
|
|
161
|
+
account
|
|
162
|
+
.command('info')
|
|
163
|
+
.description('Show account information (owners, required confirmations, balance)')
|
|
164
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
165
|
+
.action(async (options, command) => {
|
|
166
|
+
const globalOpts = command.parent.parent.opts();
|
|
167
|
+
const out = createOutput(globalOpts);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const address = resolveAddress(options.address);
|
|
171
|
+
// Get first network from contract's networks, or use specified network
|
|
172
|
+
const contractNetworks = getContractNetworks(options.address);
|
|
173
|
+
const networkKey = globalOpts.network || contractNetworks[0] || 'sepolia';
|
|
174
|
+
const network = await getNetwork(networkKey);
|
|
175
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
176
|
+
|
|
177
|
+
// Check if contract exists
|
|
178
|
+
const code = await provider.getCode(address);
|
|
179
|
+
if (code === '0x') {
|
|
180
|
+
out.error(`No contract found at ${address} on ${network.name}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
|
|
185
|
+
|
|
186
|
+
out.startSpinner('Fetching account info...');
|
|
187
|
+
|
|
188
|
+
const [owners, required, balance] = await Promise.all([
|
|
189
|
+
multisig.getOwners(),
|
|
190
|
+
multisig.required(),
|
|
191
|
+
provider.getBalance(address)
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
out.succeedSpinner('Account info retrieved');
|
|
195
|
+
|
|
196
|
+
out.print({
|
|
197
|
+
address,
|
|
198
|
+
network: networkKey,
|
|
199
|
+
networks: contractNetworks,
|
|
200
|
+
owners: owners.map(o => o),
|
|
201
|
+
required: Number(required),
|
|
202
|
+
ownerCount: owners.length,
|
|
203
|
+
confirmationsNeeded: `${required} of ${owners.length}`,
|
|
204
|
+
balance: ethers.formatEther(balance) + ' ETH'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
} catch (error) {
|
|
208
|
+
out.error(error.message);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// account owners - List owners
|
|
213
|
+
account
|
|
214
|
+
.command('owners')
|
|
215
|
+
.description('List account owners')
|
|
216
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
217
|
+
.action(async (options, command) => {
|
|
218
|
+
const globalOpts = command.parent.parent.opts();
|
|
219
|
+
const out = createOutput(globalOpts);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const address = resolveAddress(options.address);
|
|
223
|
+
// Get first network from contract's networks, or use specified network
|
|
224
|
+
const contractNetworks = getContractNetworks(options.address);
|
|
225
|
+
const networkKey = globalOpts.network || contractNetworks[0] || 'sepolia';
|
|
226
|
+
const network = await getNetwork(networkKey);
|
|
227
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
228
|
+
|
|
229
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
|
|
230
|
+
|
|
231
|
+
out.startSpinner('Fetching owners...');
|
|
232
|
+
const [owners, required] = await Promise.all([
|
|
233
|
+
multisig.getOwners(),
|
|
234
|
+
multisig.required()
|
|
235
|
+
]);
|
|
236
|
+
out.succeedSpinner('Owners retrieved');
|
|
237
|
+
|
|
238
|
+
out.print({
|
|
239
|
+
address,
|
|
240
|
+
owners: owners.map(o => o),
|
|
241
|
+
required: Number(required),
|
|
242
|
+
total: owners.length
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
} catch (error) {
|
|
246
|
+
out.error(error.message);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// account add - Add owner (applies to all networks)
|
|
251
|
+
account
|
|
252
|
+
.command('add')
|
|
253
|
+
.description('Add a new owner to the account (applies to all configured networks)')
|
|
254
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
255
|
+
.requiredOption('--owner <ownerAddress>', 'Address of new owner to add')
|
|
256
|
+
.requiredOption('--pin <pin>', 'PIN for authentication')
|
|
257
|
+
.option('--force', 'Proceed even if pre-flight check fails')
|
|
258
|
+
.action(async (options, command) => {
|
|
259
|
+
const globalOpts = command.parent.parent.opts();
|
|
260
|
+
const out = createOutput(globalOpts);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
if (!ethers.isAddress(options.owner)) {
|
|
264
|
+
out.error(`Invalid owner address: ${options.owner}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const address = resolveAddress(options.address);
|
|
269
|
+
const wallet = await getWallet(globalOpts);
|
|
270
|
+
|
|
271
|
+
// Get alias - either from options or from default contract
|
|
272
|
+
const aliasOrAddress = options.address || (getDefaultContract()?.alias);
|
|
273
|
+
const networks = getContractNetworks(aliasOrAddress);
|
|
274
|
+
|
|
275
|
+
// Pre-flight check across all networks
|
|
276
|
+
out.startSpinner('Running pre-flight checks across all networks...');
|
|
277
|
+
const preflight = await preflightAccountOperation(
|
|
278
|
+
aliasOrAddress,
|
|
279
|
+
address,
|
|
280
|
+
wallet.address,
|
|
281
|
+
'addOwner',
|
|
282
|
+
{ owner: options.owner }
|
|
283
|
+
);
|
|
284
|
+
out.stopSpinner();
|
|
285
|
+
|
|
286
|
+
displayPreflightResults(preflight, out);
|
|
287
|
+
|
|
288
|
+
if (!preflight.canProceed && !options.force) {
|
|
289
|
+
out.error('Pre-flight check failed. Use --force to proceed anyway.');
|
|
290
|
+
out.print({
|
|
291
|
+
preflightFailed: true,
|
|
292
|
+
networks: preflight.networks
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!preflight.canProceed && options.force) {
|
|
298
|
+
out.warn('Proceeding despite pre-flight failures (--force)');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (globalOpts.dryRun) {
|
|
302
|
+
out.print({
|
|
303
|
+
dryRun: true,
|
|
304
|
+
action: 'addOwner',
|
|
305
|
+
address,
|
|
306
|
+
newOwner: options.owner,
|
|
307
|
+
networks,
|
|
308
|
+
preflight: preflight.networks
|
|
309
|
+
});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Execute on all networks
|
|
314
|
+
const results = [];
|
|
315
|
+
for (const networkKey of networks) {
|
|
316
|
+
try {
|
|
317
|
+
const rpc = getRpcUrl(networkKey);
|
|
318
|
+
if (!rpc) {
|
|
319
|
+
results.push({ network: networkKey, success: false, error: 'No RPC configured' });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
324
|
+
const signer = wallet.connect(provider);
|
|
325
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
326
|
+
|
|
327
|
+
// Check if already owner on this network
|
|
328
|
+
const isAlreadyOwner = await multisig.isOwner(options.owner);
|
|
329
|
+
if (isAlreadyOwner) {
|
|
330
|
+
results.push({ network: networkKey, success: true, status: 'already_owner' });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
out.startSpinner(`Adding owner on ${networkKey}...`);
|
|
335
|
+
|
|
336
|
+
const [storedPinHash, pinNonce] = await Promise.all([
|
|
337
|
+
multisig.pinHash(),
|
|
338
|
+
multisig.pinNonce()
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
const { pA, pB, pC } = await generateZkProof(options.pin, storedPinHash, pinNonce, wallet.address);
|
|
342
|
+
|
|
343
|
+
const tx = await multisig.addOwner(options.owner, pA, pB, pC);
|
|
344
|
+
await tx.wait();
|
|
345
|
+
|
|
346
|
+
out.succeedSpinner(`Owner added on ${networkKey}`);
|
|
347
|
+
results.push({ network: networkKey, success: true, txHash: tx.hash });
|
|
348
|
+
} catch (err) {
|
|
349
|
+
out.failSpinner(`Failed on ${networkKey}: ${err.message}`);
|
|
350
|
+
results.push({ network: networkKey, success: false, error: err.message });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const successful = results.filter(r => r.success).length;
|
|
355
|
+
const failed = results.filter(r => !r.success);
|
|
356
|
+
|
|
357
|
+
// Handle decoupling if some networks failed
|
|
358
|
+
if (failed.length > 0) {
|
|
359
|
+
handleDecoupling(aliasOrAddress, address, failed, out);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
out.print({
|
|
363
|
+
address,
|
|
364
|
+
owner: options.owner,
|
|
365
|
+
networksProcessed: networks.length,
|
|
366
|
+
successful,
|
|
367
|
+
failed: failed.length,
|
|
368
|
+
results,
|
|
369
|
+
status: failed.length === 0 ? 'added' : 'partially_added'
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Force exit since snarkjs keeps handles open
|
|
373
|
+
process.exit(0);
|
|
374
|
+
|
|
375
|
+
} catch (error) {
|
|
376
|
+
out.error(error.message);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// account remove - Remove owner (applies to all networks)
|
|
382
|
+
account
|
|
383
|
+
.command('remove')
|
|
384
|
+
.description('Remove an owner from the account (applies to all configured networks)')
|
|
385
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
386
|
+
.requiredOption('--owner <ownerAddress>', 'Address of owner to remove')
|
|
387
|
+
.requiredOption('--pin <pin>', 'PIN for authentication')
|
|
388
|
+
.option('--force', 'Proceed even if pre-flight check fails')
|
|
389
|
+
.action(async (options, command) => {
|
|
390
|
+
const globalOpts = command.parent.parent.opts();
|
|
391
|
+
const out = createOutput(globalOpts);
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
if (!ethers.isAddress(options.owner)) {
|
|
395
|
+
out.error(`Invalid owner address: ${options.owner}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const address = resolveAddress(options.address);
|
|
400
|
+
const wallet = await getWallet(globalOpts);
|
|
401
|
+
|
|
402
|
+
// Get alias - either from options or from default contract
|
|
403
|
+
const aliasOrAddress = options.address || (getDefaultContract()?.alias);
|
|
404
|
+
const networks = getContractNetworks(aliasOrAddress);
|
|
405
|
+
|
|
406
|
+
// Pre-flight check across all networks
|
|
407
|
+
out.startSpinner('Running pre-flight checks across all networks...');
|
|
408
|
+
const preflight = await preflightAccountOperation(
|
|
409
|
+
aliasOrAddress,
|
|
410
|
+
address,
|
|
411
|
+
wallet.address,
|
|
412
|
+
'removeOwner',
|
|
413
|
+
{ owner: options.owner }
|
|
414
|
+
);
|
|
415
|
+
out.stopSpinner();
|
|
416
|
+
|
|
417
|
+
displayPreflightResults(preflight, out);
|
|
418
|
+
|
|
419
|
+
if (!preflight.canProceed && !options.force) {
|
|
420
|
+
out.error('Pre-flight check failed. Use --force to proceed anyway.');
|
|
421
|
+
out.print({
|
|
422
|
+
preflightFailed: true,
|
|
423
|
+
networks: preflight.networks
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!preflight.canProceed && options.force) {
|
|
429
|
+
out.warn('Proceeding despite pre-flight failures (--force)');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (globalOpts.dryRun) {
|
|
433
|
+
out.print({
|
|
434
|
+
dryRun: true,
|
|
435
|
+
action: 'removeOwner',
|
|
436
|
+
address,
|
|
437
|
+
owner: options.owner,
|
|
438
|
+
networks,
|
|
439
|
+
preflight: preflight.networks
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Execute on all networks
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const networkKey of networks) {
|
|
447
|
+
try {
|
|
448
|
+
const rpc = getRpcUrl(networkKey);
|
|
449
|
+
if (!rpc) {
|
|
450
|
+
results.push({ network: networkKey, success: false, error: 'No RPC configured' });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
455
|
+
const signer = wallet.connect(provider);
|
|
456
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
457
|
+
|
|
458
|
+
// Check if owner exists on this network
|
|
459
|
+
const isOwner = await multisig.isOwner(options.owner);
|
|
460
|
+
if (!isOwner) {
|
|
461
|
+
results.push({ network: networkKey, success: true, status: 'not_owner' });
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
out.startSpinner(`Removing owner on ${networkKey}...`);
|
|
466
|
+
|
|
467
|
+
const [storedPinHash, pinNonce] = await Promise.all([
|
|
468
|
+
multisig.pinHash(),
|
|
469
|
+
multisig.pinNonce()
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
const { pA, pB, pC } = await generateZkProof(options.pin, storedPinHash, pinNonce, wallet.address);
|
|
473
|
+
|
|
474
|
+
const tx = await multisig.removeOwner(options.owner, pA, pB, pC);
|
|
475
|
+
await tx.wait();
|
|
476
|
+
|
|
477
|
+
out.succeedSpinner(`Owner removed on ${networkKey}`);
|
|
478
|
+
results.push({ network: networkKey, success: true, txHash: tx.hash });
|
|
479
|
+
} catch (err) {
|
|
480
|
+
out.failSpinner(`Failed on ${networkKey}: ${err.message}`);
|
|
481
|
+
results.push({ network: networkKey, success: false, error: err.message });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const successful = results.filter(r => r.success).length;
|
|
486
|
+
const failed = results.filter(r => !r.success);
|
|
487
|
+
|
|
488
|
+
// Handle decoupling if some networks failed
|
|
489
|
+
if (failed.length > 0) {
|
|
490
|
+
handleDecoupling(aliasOrAddress, address, failed, out);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
out.print({
|
|
494
|
+
address,
|
|
495
|
+
owner: options.owner,
|
|
496
|
+
networksProcessed: networks.length,
|
|
497
|
+
successful,
|
|
498
|
+
failed: failed.length,
|
|
499
|
+
results,
|
|
500
|
+
status: failed.length === 0 ? 'removed' : 'partially_removed'
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Force exit since snarkjs keeps handles open
|
|
504
|
+
process.exit(0);
|
|
505
|
+
|
|
506
|
+
} catch (error) {
|
|
507
|
+
out.error(error.message);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// account replace - Replace owner (applies to all networks)
|
|
513
|
+
account
|
|
514
|
+
.command('replace')
|
|
515
|
+
.description('Replace an existing owner with a new one (applies to all configured networks)')
|
|
516
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
517
|
+
.requiredOption('--old <oldOwner>', 'Address of owner to replace')
|
|
518
|
+
.requiredOption('--new <newOwner>', 'Address of new owner')
|
|
519
|
+
.requiredOption('--pin <pin>', 'PIN for authentication')
|
|
520
|
+
.option('--force', 'Proceed even if pre-flight check fails')
|
|
521
|
+
.action(async (options, command) => {
|
|
522
|
+
const globalOpts = command.parent.parent.opts();
|
|
523
|
+
const out = createOutput(globalOpts);
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
if (!ethers.isAddress(options.old)) {
|
|
527
|
+
out.error(`Invalid old owner address: ${options.old}`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (!ethers.isAddress(options.new)) {
|
|
531
|
+
out.error(`Invalid new owner address: ${options.new}`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const address = resolveAddress(options.address);
|
|
536
|
+
const wallet = await getWallet(globalOpts);
|
|
537
|
+
|
|
538
|
+
// Get alias - either from options or from default contract
|
|
539
|
+
const aliasOrAddress = options.address || (getDefaultContract()?.alias);
|
|
540
|
+
const networks = getContractNetworks(aliasOrAddress);
|
|
541
|
+
|
|
542
|
+
// Pre-flight check across all networks
|
|
543
|
+
out.startSpinner('Running pre-flight checks across all networks...');
|
|
544
|
+
const preflight = await preflightAccountOperation(
|
|
545
|
+
aliasOrAddress,
|
|
546
|
+
address,
|
|
547
|
+
wallet.address,
|
|
548
|
+
'replaceOwner',
|
|
549
|
+
{ oldOwner: options.old, newOwner: options.new }
|
|
550
|
+
);
|
|
551
|
+
out.stopSpinner();
|
|
552
|
+
|
|
553
|
+
displayPreflightResults(preflight, out);
|
|
554
|
+
|
|
555
|
+
if (!preflight.canProceed && !options.force) {
|
|
556
|
+
out.error('Pre-flight check failed. Use --force to proceed anyway.');
|
|
557
|
+
out.print({
|
|
558
|
+
preflightFailed: true,
|
|
559
|
+
networks: preflight.networks
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!preflight.canProceed && options.force) {
|
|
565
|
+
out.warn('Proceeding despite pre-flight failures (--force)');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (globalOpts.dryRun) {
|
|
569
|
+
out.print({
|
|
570
|
+
dryRun: true,
|
|
571
|
+
action: 'replaceOwner',
|
|
572
|
+
address,
|
|
573
|
+
oldOwner: options.old,
|
|
574
|
+
newOwner: options.new,
|
|
575
|
+
networks,
|
|
576
|
+
preflight: preflight.networks
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Execute on all networks
|
|
582
|
+
const results = [];
|
|
583
|
+
for (const networkKey of networks) {
|
|
584
|
+
try {
|
|
585
|
+
const rpc = getRpcUrl(networkKey);
|
|
586
|
+
if (!rpc) {
|
|
587
|
+
results.push({ network: networkKey, success: false, error: 'No RPC configured' });
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
592
|
+
const signer = wallet.connect(provider);
|
|
593
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
594
|
+
|
|
595
|
+
out.startSpinner(`Replacing owner on ${networkKey}...`);
|
|
596
|
+
|
|
597
|
+
const [storedPinHash, pinNonce] = await Promise.all([
|
|
598
|
+
multisig.pinHash(),
|
|
599
|
+
multisig.pinNonce()
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
const { pA, pB, pC } = await generateZkProof(options.pin, storedPinHash, pinNonce, wallet.address);
|
|
603
|
+
|
|
604
|
+
const tx = await multisig.replaceOwner(options.old, options.new, pA, pB, pC);
|
|
605
|
+
await tx.wait();
|
|
606
|
+
|
|
607
|
+
out.succeedSpinner(`Owner replaced on ${networkKey}`);
|
|
608
|
+
results.push({ network: networkKey, success: true, txHash: tx.hash });
|
|
609
|
+
} catch (err) {
|
|
610
|
+
out.failSpinner(`Failed on ${networkKey}: ${err.message}`);
|
|
611
|
+
results.push({ network: networkKey, success: false, error: err.message });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const successful = results.filter(r => r.success).length;
|
|
616
|
+
const failed = results.filter(r => !r.success);
|
|
617
|
+
|
|
618
|
+
// Handle decoupling if some networks failed
|
|
619
|
+
if (failed.length > 0) {
|
|
620
|
+
handleDecoupling(aliasOrAddress, address, failed, out);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
out.print({
|
|
624
|
+
address,
|
|
625
|
+
oldOwner: options.old,
|
|
626
|
+
newOwner: options.new,
|
|
627
|
+
networksProcessed: networks.length,
|
|
628
|
+
successful,
|
|
629
|
+
failed: failed.length,
|
|
630
|
+
results,
|
|
631
|
+
status: failed.length === 0 ? 'replaced' : 'partially_replaced'
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Force exit since snarkjs keeps handles open
|
|
635
|
+
process.exit(0);
|
|
636
|
+
|
|
637
|
+
} catch (error) {
|
|
638
|
+
out.error(error.message);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// account sync - Synchronize account state across all networks
|
|
644
|
+
account
|
|
645
|
+
.command('sync')
|
|
646
|
+
.description('Synchronize account owners and requirements across all networks')
|
|
647
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
648
|
+
.option('--dry-run', 'Show what would be synced without making changes')
|
|
649
|
+
.requiredOption('--pin <pin>', 'PIN for authentication')
|
|
650
|
+
.action(async (options, command) => {
|
|
651
|
+
const globalOpts = command.parent.parent.opts();
|
|
652
|
+
const out = createOutput(globalOpts);
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const address = resolveAddress(options.address);
|
|
656
|
+
const networks = getContractNetworks(options.address);
|
|
657
|
+
|
|
658
|
+
if (networks.length < 2) {
|
|
659
|
+
out.info('Account is only configured for one network. Nothing to sync.');
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
out.startSpinner(`Checking account state across ${networks.length} networks...`);
|
|
664
|
+
|
|
665
|
+
// Fetch state from all networks
|
|
666
|
+
const states = await fetchAccountStateAcrossNetworks(options.address, address);
|
|
667
|
+
const analysis = analyzeAccountSync(states);
|
|
668
|
+
|
|
669
|
+
out.stopSpinner();
|
|
670
|
+
|
|
671
|
+
if (analysis.inSync) {
|
|
672
|
+
out.print({
|
|
673
|
+
address,
|
|
674
|
+
networks,
|
|
675
|
+
status: 'in_sync',
|
|
676
|
+
owners: analysis.canonical.owners,
|
|
677
|
+
required: analysis.canonical.required,
|
|
678
|
+
message: 'Account is in sync across all networks'
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Show discrepancies
|
|
684
|
+
if (!globalOpts.json) {
|
|
685
|
+
const chalk = require('chalk');
|
|
686
|
+
console.log(chalk.yellow('\n⚠️ Account is out of sync:\n'));
|
|
687
|
+
console.log(chalk.gray(`Canonical state (${analysis.networkCount - analysis.discrepancies.length} networks):`));
|
|
688
|
+
console.log(chalk.gray(` Owners: ${analysis.canonical.owners.join(', ')}`));
|
|
689
|
+
console.log(chalk.gray(` Required: ${analysis.canonical.required}\n`));
|
|
690
|
+
|
|
691
|
+
for (const d of analysis.discrepancies) {
|
|
692
|
+
console.log(chalk.red(`Network: ${d.network}`));
|
|
693
|
+
if (d.missingOwners.length > 0) {
|
|
694
|
+
console.log(chalk.yellow(` Missing owners: ${d.missingOwners.join(', ')}`));
|
|
695
|
+
}
|
|
696
|
+
if (d.extraOwners.length > 0) {
|
|
697
|
+
console.log(chalk.yellow(` Extra owners: ${d.extraOwners.join(', ')}`));
|
|
698
|
+
}
|
|
699
|
+
if (d.requirementMismatch) {
|
|
700
|
+
console.log(chalk.yellow(` Required: ${d.currentRequired} (should be ${analysis.canonical.required})`));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (options.dryRun || globalOpts.dryRun) {
|
|
706
|
+
out.print({
|
|
707
|
+
dryRun: true,
|
|
708
|
+
address,
|
|
709
|
+
canonical: analysis.canonical,
|
|
710
|
+
discrepancies: analysis.discrepancies
|
|
711
|
+
});
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Confirm sync
|
|
716
|
+
if (!globalOpts.yes && !globalOpts.json) {
|
|
717
|
+
const inquirer = require('inquirer');
|
|
718
|
+
const { confirm } = await inquirer.prompt([{
|
|
719
|
+
type: 'confirm',
|
|
720
|
+
name: 'confirm',
|
|
721
|
+
message: 'Proceed with synchronization?',
|
|
722
|
+
default: false
|
|
723
|
+
}]);
|
|
724
|
+
if (!confirm) {
|
|
725
|
+
out.warn('Sync cancelled');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Get wallet for signing
|
|
731
|
+
const wallet = await getWallet(globalOpts);
|
|
732
|
+
const results = [];
|
|
733
|
+
|
|
734
|
+
// Sync each discrepant network
|
|
735
|
+
for (const d of analysis.discrepancies) {
|
|
736
|
+
out.startSpinner(`Syncing ${d.network}...`);
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const rpc = getRpcUrl(d.network);
|
|
740
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
741
|
+
const signer = wallet.connect(provider);
|
|
742
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
743
|
+
|
|
744
|
+
// Get zkSNARK proof data
|
|
745
|
+
const [storedPinHash, pinNonce] = await Promise.all([
|
|
746
|
+
multisig.pinHash(),
|
|
747
|
+
multisig.pinNonce()
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
// Add missing owners
|
|
751
|
+
for (const owner of d.missingOwners) {
|
|
752
|
+
out.updateSpinner(`Adding owner ${owner.slice(0, 10)}... on ${d.network}`);
|
|
753
|
+
const { pA, pB, pC } = await generateZkProof(options.pin, storedPinHash, pinNonce, wallet.address);
|
|
754
|
+
const tx = await multisig.addOwner(owner, pA, pB, pC);
|
|
755
|
+
await tx.wait();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Remove extra owners
|
|
759
|
+
for (const owner of d.extraOwners) {
|
|
760
|
+
out.updateSpinner(`Removing owner ${owner.slice(0, 10)}... on ${d.network}`);
|
|
761
|
+
const { pA, pB, pC } = await generateZkProof(options.pin, storedPinHash, pinNonce, wallet.address);
|
|
762
|
+
const tx = await multisig.removeOwner(owner, pA, pB, pC);
|
|
763
|
+
await tx.wait();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Fix requirement if needed
|
|
767
|
+
if (d.requirementMismatch) {
|
|
768
|
+
out.updateSpinner(`Updating requirement on ${d.network}`);
|
|
769
|
+
const { pA, pB, pC } = await generateZkProof(options.pin, storedPinHash, pinNonce, wallet.address);
|
|
770
|
+
const tx = await multisig.changeRequirement(analysis.canonical.required, pA, pB, pC);
|
|
771
|
+
await tx.wait();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
results.push({ network: d.network, success: true });
|
|
775
|
+
out.succeedSpinner(`Synced ${d.network}`);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
results.push({ network: d.network, success: false, error: err.message });
|
|
778
|
+
out.failSpinner(`Failed to sync ${d.network}: ${err.message}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const successful = results.filter(r => r.success).length;
|
|
783
|
+
out.print({
|
|
784
|
+
address,
|
|
785
|
+
syncedNetworks: successful,
|
|
786
|
+
totalDiscrepancies: analysis.discrepancies.length,
|
|
787
|
+
results,
|
|
788
|
+
status: successful === analysis.discrepancies.length ? 'fully_synced' : 'partially_synced'
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// Force exit since snarkjs keeps handles open
|
|
792
|
+
process.exit(0);
|
|
793
|
+
|
|
794
|
+
} catch (error) {
|
|
795
|
+
out.error(error.message);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// account status - Check sync status across all networks
|
|
801
|
+
account
|
|
802
|
+
.command('status')
|
|
803
|
+
.description('Check account sync status across all configured networks')
|
|
804
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
805
|
+
.action(async (options, command) => {
|
|
806
|
+
const globalOpts = command.parent.parent.opts();
|
|
807
|
+
const out = createOutput(globalOpts);
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
const address = resolveAddress(options.address);
|
|
811
|
+
const networks = getContractNetworks(options.address);
|
|
812
|
+
|
|
813
|
+
if (networks.length === 0) {
|
|
814
|
+
out.warn('No networks configured for this contract.');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
out.startSpinner(`Checking account state across ${networks.length} networks...`);
|
|
819
|
+
|
|
820
|
+
const states = await fetchAccountStateAcrossNetworks(options.address, address);
|
|
821
|
+
const analysis = analyzeAccountSync(states);
|
|
822
|
+
|
|
823
|
+
out.stopSpinner();
|
|
824
|
+
|
|
825
|
+
if (analysis.inSync) {
|
|
826
|
+
if (!globalOpts.json) {
|
|
827
|
+
const chalk = require('chalk');
|
|
828
|
+
console.log(chalk.green('\n✓ Account is in sync across all networks\n'));
|
|
829
|
+
console.log(chalk.gray(`Networks: ${networks.join(', ')}`));
|
|
830
|
+
console.log(chalk.gray(`Owners: ${analysis.canonical.owners.length}`));
|
|
831
|
+
console.log(chalk.gray(`Required: ${analysis.canonical.required}`));
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
if (!globalOpts.json) {
|
|
835
|
+
const chalk = require('chalk');
|
|
836
|
+
console.log(chalk.yellow('\n⚠️ Account is OUT OF SYNC\n'));
|
|
837
|
+
console.log(chalk.gray('Run "ethnotary account sync" to synchronize.'));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
out.print({
|
|
842
|
+
address,
|
|
843
|
+
networks,
|
|
844
|
+
inSync: analysis.inSync,
|
|
845
|
+
canonical: analysis.canonical,
|
|
846
|
+
discrepancies: analysis.discrepancies,
|
|
847
|
+
networkStates: states
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
} catch (error) {
|
|
851
|
+
out.error(error.message);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
module.exports = account;
|