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,841 @@
|
|
|
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, validateNetwork } = require('../../utils/networks');
|
|
6
|
+
const { resolveAddress, getContractNetworks } = require('../../utils/contracts');
|
|
7
|
+
const { buildNotificationPayload, generateApprovalUrl, formatApprovalMessage } = require('../../utils/notifications');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the network for a transaction command
|
|
11
|
+
* If --network is specified, use that
|
|
12
|
+
* Otherwise, prompt user to select from contract's configured networks
|
|
13
|
+
*/
|
|
14
|
+
async function getTransactionNetwork(aliasOrAddress, specifiedNetwork, globalOpts, out) {
|
|
15
|
+
const contractNetworks = getContractNetworks(aliasOrAddress);
|
|
16
|
+
|
|
17
|
+
if (specifiedNetwork) {
|
|
18
|
+
// Use specified network
|
|
19
|
+
const resolved = validateNetwork(specifiedNetwork);
|
|
20
|
+
if (!resolved) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Warn if not in contract's configured networks
|
|
25
|
+
if (contractNetworks.length > 0 && !contractNetworks.includes(resolved.key)) {
|
|
26
|
+
out.warn(`Contract is not configured for network: ${specifiedNetwork}`);
|
|
27
|
+
out.info(`Configured networks: ${contractNetworks.join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rpc = getRpcUrl(resolved.key);
|
|
31
|
+
if (!rpc) {
|
|
32
|
+
out.error(`No RPC configured for ${resolved.key}. Run: ethnotary config rpc ${resolved.key}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { key: resolved.key, config: resolved.config, rpc };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// No network specified - need to prompt or error
|
|
40
|
+
if (contractNetworks.length === 0) {
|
|
41
|
+
out.error('No network specified and contract has no configured networks.');
|
|
42
|
+
out.info('Use --network <network> to specify the target network.');
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (contractNetworks.length === 1) {
|
|
47
|
+
// Only one network - use it
|
|
48
|
+
const key = contractNetworks[0];
|
|
49
|
+
const resolved = validateNetwork(key);
|
|
50
|
+
const rpc = getRpcUrl(key);
|
|
51
|
+
if (!rpc) {
|
|
52
|
+
out.error(`No RPC configured for ${key}. Run: ethnotary config rpc ${key}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return { key, config: resolved.config, rpc };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Multiple networks - prompt user to select (unless in JSON mode)
|
|
59
|
+
if (globalOpts.json) {
|
|
60
|
+
out.error('Multiple networks available. Use --network to specify which one.');
|
|
61
|
+
out.info(`Available: ${contractNetworks.join(', ')}`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const inquirer = require('inquirer');
|
|
66
|
+
const { selectedNetwork } = await inquirer.prompt([{
|
|
67
|
+
type: 'list',
|
|
68
|
+
name: 'selectedNetwork',
|
|
69
|
+
message: 'Select target network:',
|
|
70
|
+
choices: contractNetworks.map(n => ({ name: n, value: n }))
|
|
71
|
+
}]);
|
|
72
|
+
|
|
73
|
+
const resolved = validateNetwork(selectedNetwork);
|
|
74
|
+
const rpc = getRpcUrl(selectedNetwork);
|
|
75
|
+
if (!rpc) {
|
|
76
|
+
out.error(`No RPC configured for ${selectedNetwork}. Run: ethnotary config rpc ${selectedNetwork}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { key: selectedNetwork, config: resolved.config, rpc };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// MultiSig ABI - transaction functions
|
|
84
|
+
const MULTISIG_ABI = [
|
|
85
|
+
"function submitTransaction(address dest, uint256 value, bytes memory func) public returns (uint transactionId)",
|
|
86
|
+
"function confirmTransaction(uint transactionId) public",
|
|
87
|
+
"function execute(uint transactionId) public",
|
|
88
|
+
"function revokeConfirmation(uint transactionId) public",
|
|
89
|
+
"function isOwner(address) view returns (bool)",
|
|
90
|
+
"function getOwners() view returns (address[])",
|
|
91
|
+
"function transactions(uint) view returns (address dest, uint value, bytes func, bool executed, uint id)",
|
|
92
|
+
"function confirmations(uint, address) view returns (bool)",
|
|
93
|
+
"function getConfirmationCount(uint transactionId) view returns (uint count)",
|
|
94
|
+
"function getConfirmations(uint transactionId) view returns (address[])",
|
|
95
|
+
"function required() view returns (uint)",
|
|
96
|
+
"function transactionCount() view returns (uint)",
|
|
97
|
+
"function isConfirmed(uint transactionId) view returns (bool)",
|
|
98
|
+
"event Submission(uint indexed transactionId, address dest, uint256 value, bytes func)",
|
|
99
|
+
"event Confirmation(address indexed sender, uint indexed transactionId)",
|
|
100
|
+
"event Execution(uint indexed transactionId)"
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// ERC20 ABI for transfer
|
|
104
|
+
const ERC20_ABI = [
|
|
105
|
+
"function transfer(address to, uint256 amount) returns (bool)",
|
|
106
|
+
"function decimals() view returns (uint8)",
|
|
107
|
+
"function symbol() view returns (string)",
|
|
108
|
+
"function balanceOf(address) view returns (uint256)"
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// ERC721 ABI for transfer
|
|
112
|
+
const ERC721_ABI = [
|
|
113
|
+
"function safeTransferFrom(address from, address to, uint256 tokenId)",
|
|
114
|
+
"function ownerOf(uint256 tokenId) view returns (address)"
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const tx = new Command('tx')
|
|
118
|
+
.description('Transaction management commands');
|
|
119
|
+
|
|
120
|
+
// tx submit - Submit a new transaction
|
|
121
|
+
tx
|
|
122
|
+
.command('submit')
|
|
123
|
+
.description('Submit a new transaction to the MultiSig')
|
|
124
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
125
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
126
|
+
.requiredOption('--dest <destination>', 'Destination address')
|
|
127
|
+
.option('--value <ethAmount>', 'ETH value to send', '0')
|
|
128
|
+
.option('--data <hexData>', 'Transaction data (hex)', '0x')
|
|
129
|
+
.action(async (options, command) => {
|
|
130
|
+
const globalOpts = command.parent.parent.opts();
|
|
131
|
+
const out = createOutput(globalOpts);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
if (!ethers.isAddress(options.dest)) {
|
|
135
|
+
out.error(`Invalid destination address: ${options.dest}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const address = resolveAddress(options.address);
|
|
140
|
+
const wallet = await getWallet(globalOpts);
|
|
141
|
+
|
|
142
|
+
// Get network - prompts if multiple networks and none specified
|
|
143
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
144
|
+
if (!network) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
149
|
+
const signer = wallet.connect(provider);
|
|
150
|
+
|
|
151
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
152
|
+
|
|
153
|
+
// Verify caller is owner
|
|
154
|
+
const isOwner = await multisig.isOwner(wallet.address);
|
|
155
|
+
if (!isOwner) {
|
|
156
|
+
out.error(`Address ${wallet.address} is not an owner of this MultiSig`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const valueWei = ethers.parseEther(options.value);
|
|
161
|
+
|
|
162
|
+
if (globalOpts.dryRun) {
|
|
163
|
+
out.print({
|
|
164
|
+
dryRun: true,
|
|
165
|
+
action: 'submitTransaction',
|
|
166
|
+
multisig: address,
|
|
167
|
+
destination: options.dest,
|
|
168
|
+
value: options.value + ' ETH',
|
|
169
|
+
data: options.data,
|
|
170
|
+
network: globalOpts.network
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
out.startSpinner('Submitting transaction...');
|
|
176
|
+
const submitTx = await multisig.submitTransaction(options.dest, valueWei, options.data);
|
|
177
|
+
out.updateSpinner(`Transaction sent: ${submitTx.hash}`);
|
|
178
|
+
|
|
179
|
+
const receipt = await submitTx.wait();
|
|
180
|
+
|
|
181
|
+
// Find transaction ID from Submission event
|
|
182
|
+
let transactionId = null;
|
|
183
|
+
for (const log of receipt.logs) {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = multisig.interface.parseLog(log);
|
|
186
|
+
if (parsed?.name === 'Submission') {
|
|
187
|
+
transactionId = Number(parsed.args.transactionId);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
out.succeedSpinner('Transaction submitted');
|
|
194
|
+
|
|
195
|
+
// Get owners and required for notification data
|
|
196
|
+
const [owners, required] = await Promise.all([
|
|
197
|
+
multisig.getOwners(),
|
|
198
|
+
multisig.required()
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// Build notification payload for agents
|
|
202
|
+
const notificationData = buildNotificationPayload({
|
|
203
|
+
transactionId,
|
|
204
|
+
network: network.key,
|
|
205
|
+
contractAddress: address,
|
|
206
|
+
destination: options.dest,
|
|
207
|
+
value: options.value + ' ETH',
|
|
208
|
+
data: options.data,
|
|
209
|
+
confirmations: 1, // Submitter auto-confirms
|
|
210
|
+
required: Number(required),
|
|
211
|
+
owners: owners,
|
|
212
|
+
senderAddress: wallet.address
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
out.print({
|
|
216
|
+
multisig: address,
|
|
217
|
+
network: network.key,
|
|
218
|
+
transactionId,
|
|
219
|
+
destination: options.dest,
|
|
220
|
+
value: options.value + ' ETH',
|
|
221
|
+
txHash: receipt.hash,
|
|
222
|
+
status: 'submitted',
|
|
223
|
+
confirmations: `1/${required}`,
|
|
224
|
+
canExecute: Number(required) <= 1,
|
|
225
|
+
approvalUrl: notificationData.approvalUrl,
|
|
226
|
+
notifyOwners: notificationData.notifyOwners
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
} catch (error) {
|
|
230
|
+
out.error(error.message);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// tx confirm - Confirm a transaction
|
|
235
|
+
tx
|
|
236
|
+
.command('confirm')
|
|
237
|
+
.description('Confirm a pending transaction')
|
|
238
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
239
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
240
|
+
.requiredOption('--txid <transactionId>', 'Transaction ID to confirm', parseInt)
|
|
241
|
+
.action(async (options, command) => {
|
|
242
|
+
const globalOpts = command.parent.parent.opts();
|
|
243
|
+
const out = createOutput(globalOpts);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const address = resolveAddress(options.address);
|
|
247
|
+
const wallet = await getWallet(globalOpts);
|
|
248
|
+
|
|
249
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
250
|
+
if (!network) return;
|
|
251
|
+
|
|
252
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
253
|
+
const signer = wallet.connect(provider);
|
|
254
|
+
|
|
255
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
256
|
+
|
|
257
|
+
// Check if already confirmed (idempotent)
|
|
258
|
+
const alreadyConfirmed = await multisig.confirmations(options.txid, wallet.address);
|
|
259
|
+
if (alreadyConfirmed) {
|
|
260
|
+
out.print({
|
|
261
|
+
multisig: address,
|
|
262
|
+
transactionId: options.txid,
|
|
263
|
+
status: 'already_confirmed',
|
|
264
|
+
message: 'You have already confirmed this transaction'
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (globalOpts.dryRun) {
|
|
270
|
+
out.print({
|
|
271
|
+
dryRun: true,
|
|
272
|
+
action: 'confirmTransaction',
|
|
273
|
+
multisig: address,
|
|
274
|
+
transactionId: options.txid,
|
|
275
|
+
network: globalOpts.network
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
out.startSpinner('Confirming transaction...');
|
|
281
|
+
const confirmTx = await multisig.confirmTransaction(options.txid);
|
|
282
|
+
out.updateSpinner(`Transaction sent: ${confirmTx.hash}`);
|
|
283
|
+
|
|
284
|
+
await confirmTx.wait();
|
|
285
|
+
|
|
286
|
+
// Check confirmation count
|
|
287
|
+
const [confirmCount, required] = await Promise.all([
|
|
288
|
+
multisig.getConfirmationCount(options.txid),
|
|
289
|
+
multisig.required()
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
out.succeedSpinner('Transaction confirmed');
|
|
293
|
+
|
|
294
|
+
out.print({
|
|
295
|
+
multisig: address,
|
|
296
|
+
transactionId: options.txid,
|
|
297
|
+
txHash: confirmTx.hash,
|
|
298
|
+
confirmations: `${confirmCount}/${required}`,
|
|
299
|
+
canExecute: Number(confirmCount) >= Number(required),
|
|
300
|
+
status: 'confirmed'
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
out.error(error.message);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// tx execute - Execute a confirmed transaction
|
|
309
|
+
tx
|
|
310
|
+
.command('execute')
|
|
311
|
+
.description('Execute a fully confirmed transaction')
|
|
312
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
313
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
314
|
+
.requiredOption('--txid <transactionId>', 'Transaction ID to execute', parseInt)
|
|
315
|
+
.action(async (options, command) => {
|
|
316
|
+
const globalOpts = command.parent.parent.opts();
|
|
317
|
+
const out = createOutput(globalOpts);
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const address = resolveAddress(options.address);
|
|
321
|
+
const wallet = await getWallet(globalOpts);
|
|
322
|
+
|
|
323
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
324
|
+
if (!network) return;
|
|
325
|
+
|
|
326
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
327
|
+
const signer = wallet.connect(provider);
|
|
328
|
+
|
|
329
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
330
|
+
|
|
331
|
+
// Check if already executed (idempotent)
|
|
332
|
+
const txData = await multisig.transactions(options.txid);
|
|
333
|
+
if (txData.executed) {
|
|
334
|
+
out.print({
|
|
335
|
+
multisig: address,
|
|
336
|
+
transactionId: options.txid,
|
|
337
|
+
status: 'already_executed',
|
|
338
|
+
message: 'Transaction has already been executed'
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check if confirmed
|
|
344
|
+
const isConfirmed = await multisig.isConfirmed(options.txid);
|
|
345
|
+
if (!isConfirmed) {
|
|
346
|
+
const [confirmCount, required] = await Promise.all([
|
|
347
|
+
multisig.getConfirmationCount(options.txid),
|
|
348
|
+
multisig.required()
|
|
349
|
+
]);
|
|
350
|
+
out.error(`Transaction not fully confirmed. Has ${confirmCount}/${required} confirmations.`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (globalOpts.dryRun) {
|
|
355
|
+
out.print({
|
|
356
|
+
dryRun: true,
|
|
357
|
+
action: 'executeTransaction',
|
|
358
|
+
multisig: address,
|
|
359
|
+
transactionId: options.txid,
|
|
360
|
+
network: globalOpts.network
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
out.startSpinner('Executing transaction...');
|
|
366
|
+
const executeTx = await multisig.execute(options.txid);
|
|
367
|
+
out.updateSpinner(`Transaction sent: ${executeTx.hash}`);
|
|
368
|
+
|
|
369
|
+
await executeTx.wait();
|
|
370
|
+
out.succeedSpinner('Transaction executed');
|
|
371
|
+
|
|
372
|
+
out.print({
|
|
373
|
+
multisig: address,
|
|
374
|
+
transactionId: options.txid,
|
|
375
|
+
txHash: executeTx.hash,
|
|
376
|
+
status: 'executed'
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
} catch (error) {
|
|
380
|
+
out.error(error.message);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// tx revoke - Revoke confirmation
|
|
385
|
+
tx
|
|
386
|
+
.command('revoke')
|
|
387
|
+
.description('Revoke your confirmation from a transaction')
|
|
388
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
389
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
390
|
+
.requiredOption('--txid <transactionId>', 'Transaction ID', parseInt)
|
|
391
|
+
.action(async (options, command) => {
|
|
392
|
+
const globalOpts = command.parent.parent.opts();
|
|
393
|
+
const out = createOutput(globalOpts);
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const address = resolveAddress(options.address);
|
|
397
|
+
const wallet = await getWallet(globalOpts);
|
|
398
|
+
|
|
399
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
400
|
+
if (!network) return;
|
|
401
|
+
|
|
402
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
403
|
+
const signer = wallet.connect(provider);
|
|
404
|
+
|
|
405
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
406
|
+
|
|
407
|
+
// Check if confirmed (idempotent)
|
|
408
|
+
const hasConfirmed = await multisig.confirmations(options.txid, wallet.address);
|
|
409
|
+
if (!hasConfirmed) {
|
|
410
|
+
out.print({
|
|
411
|
+
multisig: address,
|
|
412
|
+
transactionId: options.txid,
|
|
413
|
+
status: 'not_confirmed',
|
|
414
|
+
message: 'You have not confirmed this transaction'
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (globalOpts.dryRun) {
|
|
420
|
+
out.print({
|
|
421
|
+
dryRun: true,
|
|
422
|
+
action: 'revokeConfirmation',
|
|
423
|
+
multisig: address,
|
|
424
|
+
transactionId: options.txid,
|
|
425
|
+
network: globalOpts.network
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
out.startSpinner('Revoking confirmation...');
|
|
431
|
+
const revokeTx = await multisig.revokeConfirmation(options.txid);
|
|
432
|
+
out.updateSpinner(`Transaction sent: ${revokeTx.hash}`);
|
|
433
|
+
|
|
434
|
+
await revokeTx.wait();
|
|
435
|
+
out.succeedSpinner('Confirmation revoked');
|
|
436
|
+
|
|
437
|
+
out.print({
|
|
438
|
+
multisig: address,
|
|
439
|
+
transactionId: options.txid,
|
|
440
|
+
txHash: revokeTx.hash,
|
|
441
|
+
status: 'revoked'
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
} catch (error) {
|
|
445
|
+
out.error(error.message);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// tx pending - List pending transactions across all networks
|
|
450
|
+
tx
|
|
451
|
+
.command('pending')
|
|
452
|
+
.description('List pending transactions across all networks')
|
|
453
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
454
|
+
.option('--network <network>', 'Filter to specific network')
|
|
455
|
+
.action(async (options, command) => {
|
|
456
|
+
const globalOpts = command.parent.parent.opts();
|
|
457
|
+
const out = createOutput(globalOpts);
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const address = resolveAddress(options.address);
|
|
461
|
+
const contractNetworks = getContractNetworks(options.address);
|
|
462
|
+
|
|
463
|
+
// Determine which networks to query
|
|
464
|
+
let networksToQuery = [];
|
|
465
|
+
const specifiedNetwork = options.network || globalOpts.network;
|
|
466
|
+
|
|
467
|
+
if (specifiedNetwork) {
|
|
468
|
+
// Filter to specific network
|
|
469
|
+
const resolved = validateNetwork(specifiedNetwork);
|
|
470
|
+
if (!resolved) {
|
|
471
|
+
out.error(`Unknown network: ${specifiedNetwork}`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
networksToQuery = [resolved.key];
|
|
475
|
+
} else if (contractNetworks.length > 0) {
|
|
476
|
+
// Query all contract networks
|
|
477
|
+
networksToQuery = contractNetworks;
|
|
478
|
+
} else {
|
|
479
|
+
// Fallback to default
|
|
480
|
+
networksToQuery = ['sepolia'];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
out.startSpinner(`Fetching pending transactions across ${networksToQuery.length} network(s)...`);
|
|
484
|
+
|
|
485
|
+
const allPending = [];
|
|
486
|
+
let totalRequired = 0;
|
|
487
|
+
|
|
488
|
+
for (const networkKey of networksToQuery) {
|
|
489
|
+
try {
|
|
490
|
+
const rpc = getRpcUrl(networkKey);
|
|
491
|
+
if (!rpc) {
|
|
492
|
+
out.warn(`No RPC configured for ${networkKey}, skipping...`);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const provider = new ethers.JsonRpcProvider(rpc);
|
|
497
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
|
|
498
|
+
|
|
499
|
+
const [txCount, required] = await Promise.all([
|
|
500
|
+
multisig.transactionCount(),
|
|
501
|
+
multisig.required()
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
totalRequired = Number(required);
|
|
505
|
+
|
|
506
|
+
for (let i = 0; i < Number(txCount); i++) {
|
|
507
|
+
const txData = await multisig.transactions(i);
|
|
508
|
+
if (!txData.executed) {
|
|
509
|
+
const confirmCount = await multisig.getConfirmationCount(i);
|
|
510
|
+
const confirmers = await multisig.getConfirmations(i);
|
|
511
|
+
allPending.push({
|
|
512
|
+
network: networkKey,
|
|
513
|
+
id: i,
|
|
514
|
+
destination: txData.dest,
|
|
515
|
+
value: ethers.formatEther(txData.value) + ' ETH',
|
|
516
|
+
data: txData.func,
|
|
517
|
+
confirmations: `${confirmCount}/${required}`,
|
|
518
|
+
confirmedBy: confirmers,
|
|
519
|
+
canExecute: Number(confirmCount) >= Number(required)
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch (err) {
|
|
524
|
+
out.warn(`Failed to fetch pending on ${networkKey}: ${err.message}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
out.succeedSpinner(`Found ${allPending.length} pending transactions across ${networksToQuery.length} network(s)`);
|
|
529
|
+
|
|
530
|
+
out.print({
|
|
531
|
+
multisig: address,
|
|
532
|
+
networks: networksToQuery,
|
|
533
|
+
required: totalRequired,
|
|
534
|
+
pendingCount: allPending.length,
|
|
535
|
+
transactions: allPending
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
} catch (error) {
|
|
539
|
+
out.error(error.message);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// tx transfer-erc20 - Submit ERC20 transfer
|
|
544
|
+
tx
|
|
545
|
+
.command('transfer-erc20')
|
|
546
|
+
.description('Submit an ERC20 token transfer')
|
|
547
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
548
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
549
|
+
.requiredOption('--token <tokenAddress>', 'ERC20 token contract address')
|
|
550
|
+
.requiredOption('--to <recipient>', 'Recipient address')
|
|
551
|
+
.requiredOption('--amount <amount>', 'Amount to transfer (in token units)')
|
|
552
|
+
.action(async (options, command) => {
|
|
553
|
+
const globalOpts = command.parent.parent.opts();
|
|
554
|
+
const out = createOutput(globalOpts);
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
if (!ethers.isAddress(options.token)) {
|
|
558
|
+
out.error(`Invalid token address: ${options.token}`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (!ethers.isAddress(options.to)) {
|
|
562
|
+
out.error(`Invalid recipient address: ${options.to}`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const address = resolveAddress(options.address);
|
|
567
|
+
const wallet = await getWallet(globalOpts);
|
|
568
|
+
|
|
569
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
570
|
+
if (!network) return;
|
|
571
|
+
|
|
572
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
573
|
+
const signer = wallet.connect(provider);
|
|
574
|
+
|
|
575
|
+
// Get token info
|
|
576
|
+
const token = new ethers.Contract(options.token, ERC20_ABI, provider);
|
|
577
|
+
const [decimals, symbol] = await Promise.all([
|
|
578
|
+
token.decimals(),
|
|
579
|
+
token.symbol()
|
|
580
|
+
]);
|
|
581
|
+
|
|
582
|
+
const amountWei = ethers.parseUnits(options.amount, decimals);
|
|
583
|
+
|
|
584
|
+
// Encode transfer call
|
|
585
|
+
const transferData = token.interface.encodeFunctionData('transfer', [options.to, amountWei]);
|
|
586
|
+
|
|
587
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
588
|
+
|
|
589
|
+
if (globalOpts.dryRun) {
|
|
590
|
+
out.print({
|
|
591
|
+
dryRun: true,
|
|
592
|
+
action: 'transferERC20',
|
|
593
|
+
multisig: address,
|
|
594
|
+
token: options.token,
|
|
595
|
+
symbol,
|
|
596
|
+
to: options.to,
|
|
597
|
+
amount: options.amount,
|
|
598
|
+
network: globalOpts.network
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
out.startSpinner(`Submitting ${symbol} transfer...`);
|
|
604
|
+
const submitTx = await multisig.submitTransaction(options.token, 0, transferData);
|
|
605
|
+
out.updateSpinner(`Transaction sent: ${submitTx.hash}`);
|
|
606
|
+
|
|
607
|
+
const receipt = await submitTx.wait();
|
|
608
|
+
|
|
609
|
+
// Find transaction ID
|
|
610
|
+
let transactionId = null;
|
|
611
|
+
for (const log of receipt.logs) {
|
|
612
|
+
try {
|
|
613
|
+
const parsed = multisig.interface.parseLog(log);
|
|
614
|
+
if (parsed?.name === 'Submission') {
|
|
615
|
+
transactionId = Number(parsed.args.transactionId);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
} catch {}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
out.succeedSpinner('ERC20 transfer submitted');
|
|
622
|
+
|
|
623
|
+
out.print({
|
|
624
|
+
multisig: address,
|
|
625
|
+
transactionId,
|
|
626
|
+
token: options.token,
|
|
627
|
+
symbol,
|
|
628
|
+
to: options.to,
|
|
629
|
+
amount: options.amount,
|
|
630
|
+
txHash: receipt.hash,
|
|
631
|
+
status: 'submitted'
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
} catch (error) {
|
|
635
|
+
out.error(error.message);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// tx transfer-nft - Submit NFT transfer
|
|
640
|
+
tx
|
|
641
|
+
.command('transfer-nft')
|
|
642
|
+
.description('Submit an NFT (ERC721) transfer')
|
|
643
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
644
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
645
|
+
.requiredOption('--token <tokenAddress>', 'NFT contract address')
|
|
646
|
+
.requiredOption('--to <recipient>', 'Recipient address')
|
|
647
|
+
.requiredOption('--tokenid <tokenId>', 'Token ID to transfer')
|
|
648
|
+
.action(async (options, command) => {
|
|
649
|
+
const globalOpts = command.parent.parent.opts();
|
|
650
|
+
const out = createOutput(globalOpts);
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
if (!ethers.isAddress(options.token)) {
|
|
654
|
+
out.error(`Invalid token address: ${options.token}`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (!ethers.isAddress(options.to)) {
|
|
658
|
+
out.error(`Invalid recipient address: ${options.to}`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const address = resolveAddress(options.address);
|
|
663
|
+
const wallet = await getWallet(globalOpts);
|
|
664
|
+
|
|
665
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
666
|
+
if (!network) return;
|
|
667
|
+
|
|
668
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
669
|
+
const signer = wallet.connect(provider);
|
|
670
|
+
|
|
671
|
+
// Encode safeTransferFrom call
|
|
672
|
+
const nft = new ethers.Contract(options.token, ERC721_ABI, provider);
|
|
673
|
+
const transferData = nft.interface.encodeFunctionData('safeTransferFrom', [
|
|
674
|
+
address, // from (the multisig)
|
|
675
|
+
options.to,
|
|
676
|
+
options.tokenid
|
|
677
|
+
]);
|
|
678
|
+
|
|
679
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, signer);
|
|
680
|
+
|
|
681
|
+
if (globalOpts.dryRun) {
|
|
682
|
+
out.print({
|
|
683
|
+
dryRun: true,
|
|
684
|
+
action: 'transferNFT',
|
|
685
|
+
multisig: address,
|
|
686
|
+
token: options.token,
|
|
687
|
+
to: options.to,
|
|
688
|
+
tokenId: options.tokenid,
|
|
689
|
+
network: globalOpts.network
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
out.startSpinner('Submitting NFT transfer...');
|
|
695
|
+
const submitTx = await multisig.submitTransaction(options.token, 0, transferData);
|
|
696
|
+
out.updateSpinner(`Transaction sent: ${submitTx.hash}`);
|
|
697
|
+
|
|
698
|
+
const receipt = await submitTx.wait();
|
|
699
|
+
|
|
700
|
+
// Find transaction ID
|
|
701
|
+
let transactionId = null;
|
|
702
|
+
for (const log of receipt.logs) {
|
|
703
|
+
try {
|
|
704
|
+
const parsed = multisig.interface.parseLog(log);
|
|
705
|
+
if (parsed?.name === 'Submission') {
|
|
706
|
+
transactionId = Number(parsed.args.transactionId);
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
} catch {}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
out.succeedSpinner('NFT transfer submitted');
|
|
713
|
+
|
|
714
|
+
out.print({
|
|
715
|
+
multisig: address,
|
|
716
|
+
transactionId,
|
|
717
|
+
token: options.token,
|
|
718
|
+
to: options.to,
|
|
719
|
+
tokenId: options.tokenid,
|
|
720
|
+
txHash: receipt.hash,
|
|
721
|
+
status: 'submitted'
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
} catch (error) {
|
|
725
|
+
out.error(error.message);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// tx notify - Generate notification payload for a pending transaction
|
|
730
|
+
tx
|
|
731
|
+
.command('notify')
|
|
732
|
+
.description('Generate notification payload for a pending transaction')
|
|
733
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
734
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
735
|
+
.requiredOption('--txid <transactionId>', 'Transaction ID', parseInt)
|
|
736
|
+
.action(async (options, command) => {
|
|
737
|
+
const globalOpts = command.parent.parent.opts();
|
|
738
|
+
const out = createOutput(globalOpts);
|
|
739
|
+
|
|
740
|
+
try {
|
|
741
|
+
const address = resolveAddress(options.address);
|
|
742
|
+
const wallet = await getWallet(globalOpts);
|
|
743
|
+
|
|
744
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
745
|
+
if (!network) return;
|
|
746
|
+
|
|
747
|
+
const provider = new ethers.JsonRpcProvider(network.rpc);
|
|
748
|
+
|
|
749
|
+
const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
|
|
750
|
+
|
|
751
|
+
// Get transaction data
|
|
752
|
+
const [txData, owners, required, confirmCount, confirmers] = await Promise.all([
|
|
753
|
+
multisig.transactions(options.txid),
|
|
754
|
+
multisig.getOwners(),
|
|
755
|
+
multisig.required(),
|
|
756
|
+
multisig.getConfirmationCount(options.txid),
|
|
757
|
+
multisig.getConfirmations(options.txid)
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
if (txData.executed) {
|
|
761
|
+
out.info('Transaction already executed');
|
|
762
|
+
out.print({
|
|
763
|
+
transactionId: options.txid,
|
|
764
|
+
status: 'executed',
|
|
765
|
+
notifyOwners: []
|
|
766
|
+
});
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Build notification payload
|
|
771
|
+
const notificationData = buildNotificationPayload({
|
|
772
|
+
transactionId: options.txid,
|
|
773
|
+
network: globalOpts.network,
|
|
774
|
+
contractAddress: address,
|
|
775
|
+
destination: txData.dest,
|
|
776
|
+
value: ethers.formatEther(txData.value) + ' ETH',
|
|
777
|
+
data: txData.func,
|
|
778
|
+
confirmations: Number(confirmCount),
|
|
779
|
+
required: Number(required),
|
|
780
|
+
owners: owners,
|
|
781
|
+
senderAddress: wallet.address
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
out.print({
|
|
785
|
+
multisig: address,
|
|
786
|
+
transactionId: options.txid,
|
|
787
|
+
destination: txData.dest,
|
|
788
|
+
value: ethers.formatEther(txData.value) + ' ETH',
|
|
789
|
+
confirmations: `${confirmCount}/${required}`,
|
|
790
|
+
confirmedBy: confirmers,
|
|
791
|
+
canExecute: Number(confirmCount) >= Number(required),
|
|
792
|
+
approvalUrl: notificationData.approvalUrl,
|
|
793
|
+
notifyOwners: notificationData.notifyOwners,
|
|
794
|
+
message: notificationData.message
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
} catch (error) {
|
|
798
|
+
out.error(error.message);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// tx link - Generate approval URL for a transaction
|
|
803
|
+
tx
|
|
804
|
+
.command('link')
|
|
805
|
+
.description('Generate approval URL for a transaction')
|
|
806
|
+
.option('--address <address>', 'MultiSig address or alias')
|
|
807
|
+
.option('--network <network>', 'Target network (prompts if not specified)')
|
|
808
|
+
.requiredOption('--txid <transactionId>', 'Transaction ID', parseInt)
|
|
809
|
+
.action(async (options, command) => {
|
|
810
|
+
const globalOpts = command.parent.parent.opts();
|
|
811
|
+
const out = createOutput(globalOpts);
|
|
812
|
+
|
|
813
|
+
try {
|
|
814
|
+
const address = resolveAddress(options.address);
|
|
815
|
+
|
|
816
|
+
const network = await getTransactionNetwork(options.address, options.network || globalOpts.network, globalOpts, out);
|
|
817
|
+
if (!network) return;
|
|
818
|
+
|
|
819
|
+
const approvalUrl = generateApprovalUrl(
|
|
820
|
+
options.txid,
|
|
821
|
+
network.key,
|
|
822
|
+
address
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (globalOpts.json) {
|
|
826
|
+
out.print({
|
|
827
|
+
transactionId: options.txid,
|
|
828
|
+
network: network.key,
|
|
829
|
+
multisig: address,
|
|
830
|
+
approvalUrl
|
|
831
|
+
});
|
|
832
|
+
} else {
|
|
833
|
+
console.log(approvalUrl);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
} catch (error) {
|
|
837
|
+
out.error(error.message);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
module.exports = tx;
|