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,369 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const { createOutput } = require('../../utils/output');
|
|
8
|
+
|
|
9
|
+
const ETHNOTARY_DIR = path.join(os.homedir(), '.ethnotary');
|
|
10
|
+
const CONFIG_PATH = path.join(ETHNOTARY_DIR, 'config.json');
|
|
11
|
+
|
|
12
|
+
// RPC provider suggestions
|
|
13
|
+
const RPC_PROVIDERS = {
|
|
14
|
+
infura: {
|
|
15
|
+
name: 'Infura',
|
|
16
|
+
url: 'https://infura.io',
|
|
17
|
+
description: 'Popular provider, requires API key'
|
|
18
|
+
},
|
|
19
|
+
alchemy: {
|
|
20
|
+
name: 'Alchemy',
|
|
21
|
+
url: 'https://alchemy.com',
|
|
22
|
+
description: 'Full-featured provider, requires API key'
|
|
23
|
+
},
|
|
24
|
+
quicknode: {
|
|
25
|
+
name: 'QuickNode',
|
|
26
|
+
url: 'https://quicknode.com',
|
|
27
|
+
description: 'High-performance provider, requires API key'
|
|
28
|
+
},
|
|
29
|
+
publicnode: {
|
|
30
|
+
name: 'PublicNode',
|
|
31
|
+
url: 'https://publicnode.com',
|
|
32
|
+
description: 'Free public RPCs (rate limited)'
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Default network definitions
|
|
37
|
+
const DEFAULT_NETWORKS = {
|
|
38
|
+
sepolia: { name: 'Sepolia', chainId: 11155111, testnet: true },
|
|
39
|
+
'base-sepolia': { name: 'Base Sepolia', chainId: 84532, testnet: true },
|
|
40
|
+
'arbitrum-sepolia': { name: 'Arbitrum Sepolia', chainId: 421614, testnet: true },
|
|
41
|
+
ethereum: { name: 'Ethereum Mainnet', chainId: 1, testnet: false },
|
|
42
|
+
base: { name: 'Base', chainId: 8453, testnet: false },
|
|
43
|
+
arbitrum: { name: 'Arbitrum One', chainId: 42161, testnet: false },
|
|
44
|
+
optimism: { name: 'Optimism', chainId: 10, testnet: false },
|
|
45
|
+
polygon: { name: 'Polygon', chainId: 137, testnet: false }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Ensure directory exists
|
|
49
|
+
function ensureDir() {
|
|
50
|
+
if (!fs.existsSync(ETHNOTARY_DIR)) {
|
|
51
|
+
fs.mkdirSync(ETHNOTARY_DIR, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Load config
|
|
56
|
+
function loadConfig() {
|
|
57
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
58
|
+
return { networks: {}, rpc: {} };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
62
|
+
if (!cfg.networks) cfg.networks = {};
|
|
63
|
+
if (!cfg.rpc) cfg.rpc = {};
|
|
64
|
+
return cfg;
|
|
65
|
+
} catch {
|
|
66
|
+
return { networks: {}, rpc: {} };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Save config
|
|
71
|
+
function saveConfig(config) {
|
|
72
|
+
ensureDir();
|
|
73
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const config = new Command('config')
|
|
77
|
+
.description('Configuration commands');
|
|
78
|
+
|
|
79
|
+
// config rpc - Add or update RPC URL for a network
|
|
80
|
+
config
|
|
81
|
+
.command('rpc')
|
|
82
|
+
.description('Add or update RPC URL for a network')
|
|
83
|
+
.argument('[network]', 'Network name (e.g., sepolia, base, arbitrum)')
|
|
84
|
+
.option('--url <url>', 'RPC URL to set')
|
|
85
|
+
.action(async (network, options, command) => {
|
|
86
|
+
const globalOpts = command.parent.parent.opts();
|
|
87
|
+
const out = createOutput(globalOpts);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const cfg = loadConfig();
|
|
91
|
+
|
|
92
|
+
// If no network specified, show current RPC config
|
|
93
|
+
if (!network) {
|
|
94
|
+
const rpcEntries = Object.entries(cfg.rpc);
|
|
95
|
+
if (rpcEntries.length === 0) {
|
|
96
|
+
out.info('No RPC URLs configured.');
|
|
97
|
+
out.info('Use "ethnotary config rpc <network> --url <url>" to add one.');
|
|
98
|
+
}
|
|
99
|
+
out.print({
|
|
100
|
+
rpc: cfg.rpc
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If URL provided via flag, set it directly
|
|
106
|
+
if (options.url) {
|
|
107
|
+
if (!options.url.startsWith('http://') && !options.url.startsWith('https://')) {
|
|
108
|
+
out.error('RPC URL must start with http:// or https://');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
cfg.rpc[network] = options.url;
|
|
112
|
+
saveConfig(cfg);
|
|
113
|
+
out.success(`RPC URL set for ${network}`);
|
|
114
|
+
out.print({ network, url: options.url });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Interactive mode - prompt for URL
|
|
119
|
+
if (globalOpts.json) {
|
|
120
|
+
out.error('--url required in JSON mode');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(chalk.cyan(`\nConfiguring RPC for: ${network}`));
|
|
125
|
+
console.log(chalk.gray('\nWhere to get RPC URLs:'));
|
|
126
|
+
for (const [key, provider] of Object.entries(RPC_PROVIDERS)) {
|
|
127
|
+
console.log(chalk.gray(` • ${provider.name}: ${provider.url} - ${provider.description}`));
|
|
128
|
+
}
|
|
129
|
+
console.log('');
|
|
130
|
+
|
|
131
|
+
const { rpcUrl } = await inquirer.prompt([{
|
|
132
|
+
type: 'input',
|
|
133
|
+
name: 'rpcUrl',
|
|
134
|
+
message: `Enter RPC URL for ${network}:`,
|
|
135
|
+
validate: (input) => {
|
|
136
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
137
|
+
return 'RPC URL must start with http:// or https://';
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}]);
|
|
142
|
+
|
|
143
|
+
cfg.rpc[network] = rpcUrl;
|
|
144
|
+
saveConfig(cfg);
|
|
145
|
+
out.success(`RPC URL set for ${network}`);
|
|
146
|
+
out.print({ network, url: rpcUrl });
|
|
147
|
+
|
|
148
|
+
} catch (error) {
|
|
149
|
+
out.error(error.message);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// config network - Add or update a network
|
|
154
|
+
config
|
|
155
|
+
.command('network')
|
|
156
|
+
.description('Add or update a network configuration')
|
|
157
|
+
.argument('[network]', 'Network name/key')
|
|
158
|
+
.option('--name <name>', 'Display name for the network')
|
|
159
|
+
.option('--chain-id <chainId>', 'Chain ID', parseInt)
|
|
160
|
+
.option('--rpc <url>', 'RPC URL for the network')
|
|
161
|
+
.option('--testnet', 'Mark as testnet')
|
|
162
|
+
.action(async (network, options, command) => {
|
|
163
|
+
const globalOpts = command.parent.parent.opts();
|
|
164
|
+
const out = createOutput(globalOpts);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const cfg = loadConfig();
|
|
168
|
+
|
|
169
|
+
// If no network specified, list all networks
|
|
170
|
+
if (!network) {
|
|
171
|
+
const allNetworks = { ...DEFAULT_NETWORKS };
|
|
172
|
+
// Merge custom networks
|
|
173
|
+
for (const [key, value] of Object.entries(cfg.networks)) {
|
|
174
|
+
allNetworks[key] = value;
|
|
175
|
+
}
|
|
176
|
+
// Add RPC status
|
|
177
|
+
const networksWithStatus = Object.entries(allNetworks).map(([key, net]) => ({
|
|
178
|
+
key,
|
|
179
|
+
...net,
|
|
180
|
+
rpcConfigured: !!(cfg.rpc[key] || process.env[`${key.toUpperCase().replace('-', '_')}_RPC_URL`])
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
if (!globalOpts.json) {
|
|
184
|
+
console.log(chalk.bold('\nConfigured Networks:\n'));
|
|
185
|
+
for (const net of networksWithStatus) {
|
|
186
|
+
const status = net.rpcConfigured ? chalk.green('✓') : chalk.yellow('○');
|
|
187
|
+
const type = net.testnet ? chalk.gray('(testnet)') : '';
|
|
188
|
+
console.log(` ${status} ${net.key} - ${net.name} ${type}`);
|
|
189
|
+
console.log(chalk.gray(` Chain ID: ${net.chainId}`));
|
|
190
|
+
}
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(chalk.gray('Use "ethnotary config network <name>" to add/edit a network'));
|
|
193
|
+
console.log(chalk.gray('Use "ethnotary config rpc <network>" to configure RPC URL\n'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
out.print({ networks: networksWithStatus });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if it's a known network
|
|
201
|
+
const existing = cfg.networks[network] || DEFAULT_NETWORKS[network];
|
|
202
|
+
|
|
203
|
+
// If options provided, update directly
|
|
204
|
+
if (options.name || options.chainId || options.rpc) {
|
|
205
|
+
const netConfig = existing || {};
|
|
206
|
+
if (options.name) netConfig.name = options.name;
|
|
207
|
+
if (options.chainId) netConfig.chainId = options.chainId;
|
|
208
|
+
if (options.testnet !== undefined) netConfig.testnet = options.testnet;
|
|
209
|
+
|
|
210
|
+
cfg.networks[network] = netConfig;
|
|
211
|
+
|
|
212
|
+
if (options.rpc) {
|
|
213
|
+
cfg.rpc[network] = options.rpc;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
saveConfig(cfg);
|
|
217
|
+
out.success(`Network "${network}" configured`);
|
|
218
|
+
out.print({ network, config: netConfig, rpc: cfg.rpc[network] || null });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Interactive mode
|
|
223
|
+
if (globalOpts.json) {
|
|
224
|
+
out.error('Options required in JSON mode (--name, --chain-id, --rpc)');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(chalk.cyan(`\nConfiguring network: ${network}`));
|
|
229
|
+
if (existing) {
|
|
230
|
+
console.log(chalk.gray(`Existing config: ${existing.name}, Chain ID: ${existing.chainId}`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const answers = await inquirer.prompt([
|
|
234
|
+
{
|
|
235
|
+
type: 'input',
|
|
236
|
+
name: 'name',
|
|
237
|
+
message: 'Display name:',
|
|
238
|
+
default: existing?.name || network
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
type: 'number',
|
|
242
|
+
name: 'chainId',
|
|
243
|
+
message: 'Chain ID:',
|
|
244
|
+
default: existing?.chainId,
|
|
245
|
+
validate: (input) => input > 0 ? true : 'Chain ID must be positive'
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
type: 'confirm',
|
|
249
|
+
name: 'testnet',
|
|
250
|
+
message: 'Is this a testnet?',
|
|
251
|
+
default: existing?.testnet || false
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: 'confirm',
|
|
255
|
+
name: 'configureRpc',
|
|
256
|
+
message: 'Configure RPC URL now?',
|
|
257
|
+
default: !cfg.rpc[network]
|
|
258
|
+
}
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
cfg.networks[network] = {
|
|
262
|
+
name: answers.name,
|
|
263
|
+
chainId: answers.chainId,
|
|
264
|
+
testnet: answers.testnet
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
if (answers.configureRpc) {
|
|
268
|
+
console.log(chalk.gray('\nWhere to get RPC URLs:'));
|
|
269
|
+
for (const [key, provider] of Object.entries(RPC_PROVIDERS)) {
|
|
270
|
+
console.log(chalk.gray(` • ${provider.name}: ${provider.url}`));
|
|
271
|
+
}
|
|
272
|
+
console.log('');
|
|
273
|
+
|
|
274
|
+
const { rpcUrl } = await inquirer.prompt([{
|
|
275
|
+
type: 'input',
|
|
276
|
+
name: 'rpcUrl',
|
|
277
|
+
message: 'RPC URL:',
|
|
278
|
+
validate: (input) => {
|
|
279
|
+
if (!input) return true; // Allow empty
|
|
280
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
281
|
+
return 'RPC URL must start with http:// or https://';
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}]);
|
|
286
|
+
|
|
287
|
+
if (rpcUrl) {
|
|
288
|
+
cfg.rpc[network] = rpcUrl;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
saveConfig(cfg);
|
|
293
|
+
out.success(`Network "${network}" configured`);
|
|
294
|
+
out.print({
|
|
295
|
+
network,
|
|
296
|
+
config: cfg.networks[network],
|
|
297
|
+
rpc: cfg.rpc[network] || null
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
} catch (error) {
|
|
301
|
+
out.error(error.message);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// config show - Show all config
|
|
306
|
+
config
|
|
307
|
+
.command('show')
|
|
308
|
+
.description('Show all configuration')
|
|
309
|
+
.action(async (options, command) => {
|
|
310
|
+
const globalOpts = command.parent.parent.opts();
|
|
311
|
+
const out = createOutput(globalOpts);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const cfg = loadConfig();
|
|
315
|
+
|
|
316
|
+
if (!globalOpts.json) {
|
|
317
|
+
console.log(chalk.bold('\nEthnotary Configuration\n'));
|
|
318
|
+
console.log(chalk.gray(`Config file: ${CONFIG_PATH}`));
|
|
319
|
+
|
|
320
|
+
console.log(chalk.bold('\nRPC URLs:'));
|
|
321
|
+
const rpcEntries = Object.entries(cfg.rpc);
|
|
322
|
+
if (rpcEntries.length === 0) {
|
|
323
|
+
console.log(chalk.gray(' No RPC URLs configured'));
|
|
324
|
+
} else {
|
|
325
|
+
for (const [network, url] of rpcEntries) {
|
|
326
|
+
console.log(` ${network}: ${chalk.gray(url.substring(0, 50))}...`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(chalk.bold('\nCustom Networks:'));
|
|
331
|
+
const netEntries = Object.entries(cfg.networks);
|
|
332
|
+
if (netEntries.length === 0) {
|
|
333
|
+
console.log(chalk.gray(' Using default networks only'));
|
|
334
|
+
} else {
|
|
335
|
+
for (const [key, net] of netEntries) {
|
|
336
|
+
console.log(` ${key}: ${net.name} (Chain ID: ${net.chainId})`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
console.log('');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
out.print({
|
|
343
|
+
configPath: CONFIG_PATH,
|
|
344
|
+
rpc: cfg.rpc,
|
|
345
|
+
networks: cfg.networks
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
} catch (error) {
|
|
349
|
+
out.error(error.message);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// config path - Show config file path
|
|
354
|
+
config
|
|
355
|
+
.command('path')
|
|
356
|
+
.description('Show configuration file paths')
|
|
357
|
+
.action(async (options, command) => {
|
|
358
|
+
const globalOpts = command.parent.parent.opts();
|
|
359
|
+
const out = createOutput(globalOpts);
|
|
360
|
+
|
|
361
|
+
out.print({
|
|
362
|
+
configDir: ETHNOTARY_DIR,
|
|
363
|
+
configFile: CONFIG_PATH,
|
|
364
|
+
keystoreFile: path.join(ETHNOTARY_DIR, 'keystore.json'),
|
|
365
|
+
contractsFile: path.join(ETHNOTARY_DIR, 'contracts.json')
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
module.exports = config;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const { ethers } = require('ethers');
|
|
3
|
+
const { createOutput } = require('../../utils/output');
|
|
4
|
+
const {
|
|
5
|
+
addContact,
|
|
6
|
+
listContacts,
|
|
7
|
+
removeContact,
|
|
8
|
+
getContact
|
|
9
|
+
} = require('../../utils/contacts');
|
|
10
|
+
|
|
11
|
+
const contact = new Command('contact')
|
|
12
|
+
.description('Owner contact management for notifications');
|
|
13
|
+
|
|
14
|
+
// contact add - Add or update owner contact info
|
|
15
|
+
contact
|
|
16
|
+
.command('add')
|
|
17
|
+
.description('Add or update contact info for an owner address')
|
|
18
|
+
.requiredOption('--address <address>', 'Owner Ethereum address')
|
|
19
|
+
.option('--telegram <chatId>', 'Telegram chat ID (numeric)')
|
|
20
|
+
.option('--whatsapp <phone>', 'WhatsApp phone number (e.g., +15551234567)')
|
|
21
|
+
.option('--email <email>', 'Email address for notifications')
|
|
22
|
+
.option('--webhook <url>', 'Webhook URL for notifications')
|
|
23
|
+
.action(async (options, command) => {
|
|
24
|
+
const globalOpts = command.parent.parent.opts();
|
|
25
|
+
const out = createOutput(globalOpts);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (!ethers.isAddress(options.address)) {
|
|
29
|
+
out.error(`Invalid address: ${options.address}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!options.telegram && !options.whatsapp && !options.email && !options.webhook) {
|
|
34
|
+
out.error('At least one contact method required: --telegram, --whatsapp, --email, or --webhook');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const contactInfo = {};
|
|
39
|
+
if (options.telegram) contactInfo.telegram = options.telegram;
|
|
40
|
+
if (options.whatsapp) contactInfo.whatsapp = options.whatsapp;
|
|
41
|
+
if (options.email) contactInfo.email = options.email;
|
|
42
|
+
if (options.webhook) contactInfo.webhook = options.webhook;
|
|
43
|
+
|
|
44
|
+
const saved = addContact(options.address, contactInfo);
|
|
45
|
+
|
|
46
|
+
out.success(`Contact saved for ${options.address}`);
|
|
47
|
+
out.print({
|
|
48
|
+
address: ethers.getAddress(options.address),
|
|
49
|
+
...saved
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
} catch (error) {
|
|
53
|
+
out.error(error.message);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// contact list - List all contacts
|
|
58
|
+
contact
|
|
59
|
+
.command('list')
|
|
60
|
+
.description('List all saved owner contacts')
|
|
61
|
+
.action(async (options, command) => {
|
|
62
|
+
const globalOpts = command.parent.parent.opts();
|
|
63
|
+
const out = createOutput(globalOpts);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const contacts = listContacts();
|
|
67
|
+
|
|
68
|
+
if (contacts.length === 0) {
|
|
69
|
+
out.info('No contacts saved. Use "ethnotary contact add" to add one.');
|
|
70
|
+
out.print({ contacts: [] });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
out.print({
|
|
75
|
+
count: contacts.length,
|
|
76
|
+
contacts
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
} catch (error) {
|
|
80
|
+
out.error(error.message);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// contact show - Show contact for specific address
|
|
85
|
+
contact
|
|
86
|
+
.command('show')
|
|
87
|
+
.description('Show contact info for a specific address')
|
|
88
|
+
.requiredOption('--address <address>', 'Owner Ethereum address')
|
|
89
|
+
.action(async (options, command) => {
|
|
90
|
+
const globalOpts = command.parent.parent.opts();
|
|
91
|
+
const out = createOutput(globalOpts);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (!ethers.isAddress(options.address)) {
|
|
95
|
+
out.error(`Invalid address: ${options.address}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const contactInfo = getContact(options.address);
|
|
100
|
+
|
|
101
|
+
if (!contactInfo) {
|
|
102
|
+
out.info(`No contact found for ${options.address}`);
|
|
103
|
+
out.print({ address: options.address, contact: null });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
out.print({
|
|
108
|
+
address: ethers.getAddress(options.address),
|
|
109
|
+
...contactInfo
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
out.error(error.message);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// contact remove - Remove a contact
|
|
118
|
+
contact
|
|
119
|
+
.command('remove')
|
|
120
|
+
.description('Remove contact info for an owner address')
|
|
121
|
+
.requiredOption('--address <address>', 'Owner Ethereum address')
|
|
122
|
+
.action(async (options, command) => {
|
|
123
|
+
const globalOpts = command.parent.parent.opts();
|
|
124
|
+
const out = createOutput(globalOpts);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
removeContact(options.address);
|
|
128
|
+
out.success(`Contact removed for ${options.address}`);
|
|
129
|
+
out.print({
|
|
130
|
+
address: options.address,
|
|
131
|
+
status: 'removed'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
} catch (error) {
|
|
135
|
+
out.error(error.message);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
module.exports = contact;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const { ethers } = require('ethers');
|
|
3
|
+
const { createOutput } = require('../../utils/output');
|
|
4
|
+
const {
|
|
5
|
+
saveContract,
|
|
6
|
+
listContracts,
|
|
7
|
+
removeContract,
|
|
8
|
+
setDefaultContract,
|
|
9
|
+
getDefaultContract
|
|
10
|
+
} = require('../../utils/contracts');
|
|
11
|
+
|
|
12
|
+
const contract = new Command('contract')
|
|
13
|
+
.description('Contract storage commands');
|
|
14
|
+
|
|
15
|
+
// contract add - Save a contract
|
|
16
|
+
contract
|
|
17
|
+
.command('add')
|
|
18
|
+
.description('Save a contract with an alias')
|
|
19
|
+
.requiredOption('--alias <alias>', 'Alias for the contract')
|
|
20
|
+
.requiredOption('--address <address>', 'Contract address')
|
|
21
|
+
.option('--label <label>', 'Optional label/description')
|
|
22
|
+
.option('--network <network>', 'Network the contract is on')
|
|
23
|
+
.action(async (options, command) => {
|
|
24
|
+
const globalOpts = command.parent.parent.opts();
|
|
25
|
+
const out = createOutput(globalOpts);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (!ethers.isAddress(options.address)) {
|
|
29
|
+
out.error(`Invalid address: ${options.address}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const network = options.network || globalOpts.network;
|
|
34
|
+
const saved = saveContract(options.alias, options.address, network, options.label);
|
|
35
|
+
|
|
36
|
+
out.success(`Contract saved as "${options.alias}"`);
|
|
37
|
+
out.print({
|
|
38
|
+
alias: options.alias,
|
|
39
|
+
address: options.address,
|
|
40
|
+
network,
|
|
41
|
+
label: options.label || '',
|
|
42
|
+
created: saved.created
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
out.error(error.message);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// contract list - List saved contracts
|
|
51
|
+
contract
|
|
52
|
+
.command('list')
|
|
53
|
+
.description('List all saved contracts')
|
|
54
|
+
.action(async (options, command) => {
|
|
55
|
+
const globalOpts = command.parent.parent.opts();
|
|
56
|
+
const out = createOutput(globalOpts);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const contracts = listContracts();
|
|
60
|
+
|
|
61
|
+
if (contracts.length === 0) {
|
|
62
|
+
out.info('No contracts saved. Use "ethnotary contract add" to save one.');
|
|
63
|
+
out.print({ contracts: [] });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
out.print({
|
|
68
|
+
count: contracts.length,
|
|
69
|
+
contracts
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
} catch (error) {
|
|
73
|
+
out.error(error.message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// contract remove - Remove a saved contract
|
|
78
|
+
contract
|
|
79
|
+
.command('remove')
|
|
80
|
+
.description('Remove a saved contract')
|
|
81
|
+
.requiredOption('--alias <alias>', 'Alias of contract to remove')
|
|
82
|
+
.action(async (options, command) => {
|
|
83
|
+
const globalOpts = command.parent.parent.opts();
|
|
84
|
+
const out = createOutput(globalOpts);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
removeContract(options.alias);
|
|
88
|
+
out.success(`Contract "${options.alias}" removed`);
|
|
89
|
+
out.print({
|
|
90
|
+
alias: options.alias,
|
|
91
|
+
status: 'removed'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
out.error(error.message);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// contract default - Set or show default contract (legacy, kept for compatibility)
|
|
100
|
+
contract
|
|
101
|
+
.command('default')
|
|
102
|
+
.description('Set or show the default contract (alias for checkout/current)')
|
|
103
|
+
.option('--alias <alias>', 'Set this alias as default')
|
|
104
|
+
.action(async (options, command) => {
|
|
105
|
+
const globalOpts = command.parent.parent.opts();
|
|
106
|
+
const out = createOutput(globalOpts);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (options.alias) {
|
|
110
|
+
setDefaultContract(options.alias);
|
|
111
|
+
out.success(`Switched to "${options.alias}"`);
|
|
112
|
+
out.print({
|
|
113
|
+
current: options.alias,
|
|
114
|
+
status: 'checked_out'
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
const defaultContract = getDefaultContract();
|
|
118
|
+
if (defaultContract) {
|
|
119
|
+
out.print({
|
|
120
|
+
current: defaultContract.alias,
|
|
121
|
+
address: defaultContract.address,
|
|
122
|
+
network: defaultContract.network
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
out.info('No contract checked out. Use "ethnotary checkout <alias>"');
|
|
126
|
+
out.print({ current: null });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
} catch (error) {
|
|
131
|
+
out.error(error.message);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// contract checkout - Switch to a different contract (like git checkout)
|
|
136
|
+
contract
|
|
137
|
+
.command('checkout <alias>')
|
|
138
|
+
.description('Switch to a different contract (like git checkout)')
|
|
139
|
+
.action(async (alias, command) => {
|
|
140
|
+
const globalOpts = command.parent.parent.opts();
|
|
141
|
+
const out = createOutput(globalOpts);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
setDefaultContract(alias);
|
|
145
|
+
const contract = getDefaultContract();
|
|
146
|
+
|
|
147
|
+
out.success(`Switched to "${alias}"`);
|
|
148
|
+
out.print({
|
|
149
|
+
current: alias,
|
|
150
|
+
address: contract.address,
|
|
151
|
+
network: contract.network,
|
|
152
|
+
label: contract.label || ''
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
} catch (error) {
|
|
156
|
+
out.error(error.message);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// contract current - Show current checked out contract (like git branch)
|
|
161
|
+
contract
|
|
162
|
+
.command('current')
|
|
163
|
+
.alias('status')
|
|
164
|
+
.description('Show the currently active contract')
|
|
165
|
+
.action(async (options, command) => {
|
|
166
|
+
const globalOpts = command.parent.parent.opts();
|
|
167
|
+
const out = createOutput(globalOpts);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const current = getDefaultContract();
|
|
171
|
+
|
|
172
|
+
if (current) {
|
|
173
|
+
if (!globalOpts.json) {
|
|
174
|
+
out.info(`On account: ${current.alias}`);
|
|
175
|
+
out.info(`Address: ${current.address}`);
|
|
176
|
+
out.info(`Network: ${current.network}`);
|
|
177
|
+
if (current.label) out.info(`Label: ${current.label}`);
|
|
178
|
+
}
|
|
179
|
+
out.print({
|
|
180
|
+
current: current.alias,
|
|
181
|
+
address: current.address,
|
|
182
|
+
network: current.network,
|
|
183
|
+
label: current.label || ''
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
out.warn('No contract checked out.');
|
|
187
|
+
out.info('Use "ethnotary checkout <alias>" to switch to a contract.');
|
|
188
|
+
out.info('Use "ethnotary contract list" to see available contracts.');
|
|
189
|
+
out.print({ current: null });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
} catch (error) {
|
|
193
|
+
out.error(error.message);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
module.exports = contract;
|