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.
@@ -0,0 +1,278 @@
1
+ const { ethers } = require('ethers');
2
+ const { getRpcUrl, validateNetwork } = require('./networks');
3
+ const { getContractNetworks, createDecoupledContract } = require('./contracts');
4
+
5
+ const MULTISIG_ABI = [
6
+ "function getOwners() view returns (address[])",
7
+ "function required() view returns (uint)",
8
+ "function isOwner(address) view returns (bool)",
9
+ "function pinHash() view returns (bytes32)",
10
+ "function pinNonce() view returns (uint256)",
11
+ "function addOwner(address accountOwner, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public",
12
+ "function removeOwner(address accountOwner, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public",
13
+ "function replaceOwner(address accountOwner, address newOwner, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public",
14
+ "function changeRequirement(uint _required, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public"
15
+ ];
16
+
17
+ /**
18
+ * Fetch account state from all configured networks
19
+ */
20
+ async function fetchAccountStateAcrossNetworks(aliasOrAddress, address) {
21
+ const networks = getContractNetworks(aliasOrAddress);
22
+ const states = [];
23
+
24
+ for (const networkKey of networks) {
25
+ try {
26
+ const rpc = getRpcUrl(networkKey);
27
+ if (!rpc) continue;
28
+
29
+ const provider = new ethers.JsonRpcProvider(rpc);
30
+ const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
31
+
32
+ const [owners, required] = await Promise.all([
33
+ multisig.getOwners(),
34
+ multisig.required()
35
+ ]);
36
+
37
+ states.push({
38
+ network: networkKey,
39
+ owners: owners.map(o => o.toLowerCase()),
40
+ required: Number(required),
41
+ success: true
42
+ });
43
+ } catch (err) {
44
+ states.push({
45
+ network: networkKey,
46
+ error: err.message,
47
+ success: false
48
+ });
49
+ }
50
+ }
51
+
52
+ return states;
53
+ }
54
+
55
+ /**
56
+ * Check if account is in sync across all networks
57
+ * Returns { inSync: boolean, canonical: { owners, required }, discrepancies: [] }
58
+ */
59
+ function analyzeAccountSync(states) {
60
+ const successfulStates = states.filter(s => s.success);
61
+
62
+ if (successfulStates.length === 0) {
63
+ return { inSync: false, error: 'No networks responded', discrepancies: [] };
64
+ }
65
+
66
+ if (successfulStates.length === 1) {
67
+ return {
68
+ inSync: true,
69
+ canonical: {
70
+ owners: successfulStates[0].owners,
71
+ required: successfulStates[0].required
72
+ },
73
+ discrepancies: []
74
+ };
75
+ }
76
+
77
+ // Find canonical state (most common configuration)
78
+ const configCounts = {};
79
+ for (const state of successfulStates) {
80
+ const key = JSON.stringify({ owners: state.owners.sort(), required: state.required });
81
+ configCounts[key] = (configCounts[key] || 0) + 1;
82
+ }
83
+
84
+ // Get the most common configuration
85
+ const sortedConfigs = Object.entries(configCounts).sort((a, b) => b[1] - a[1]);
86
+ const canonicalConfig = JSON.parse(sortedConfigs[0][0]);
87
+
88
+ // Find discrepancies
89
+ const discrepancies = [];
90
+ for (const state of successfulStates) {
91
+ const stateKey = JSON.stringify({ owners: state.owners.sort(), required: state.required });
92
+ const canonicalKey = JSON.stringify({ owners: canonicalConfig.owners.sort(), required: canonicalConfig.required });
93
+
94
+ if (stateKey !== canonicalKey) {
95
+ const missingOwners = canonicalConfig.owners.filter(o => !state.owners.includes(o));
96
+ const extraOwners = state.owners.filter(o => !canonicalConfig.owners.includes(o));
97
+
98
+ discrepancies.push({
99
+ network: state.network,
100
+ currentOwners: state.owners,
101
+ currentRequired: state.required,
102
+ missingOwners,
103
+ extraOwners,
104
+ requirementMismatch: state.required !== canonicalConfig.required
105
+ });
106
+ }
107
+ }
108
+
109
+ return {
110
+ inSync: discrepancies.length === 0,
111
+ canonical: canonicalConfig,
112
+ discrepancies,
113
+ networkCount: successfulStates.length
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Handle decoupling when an account operation fails on some networks
119
+ */
120
+ function handleDecoupling(alias, address, failedNetworks, out) {
121
+ const chalk = require('chalk');
122
+
123
+ if (failedNetworks.length === 0) return;
124
+
125
+ out.warn(`\n⚠️ Account is now out of sync on ${failedNetworks.length} network(s):`);
126
+
127
+ for (const { network, error } of failedNetworks) {
128
+ out.warn(` - ${network}: ${error}`);
129
+
130
+ // Create decoupled contract alias
131
+ const decoupled = createDecoupledContract(alias, address, network);
132
+ out.info(` Created decoupled alias: ${decoupled.alias}`);
133
+ }
134
+
135
+ out.info('\nTo re-sync accounts, run:');
136
+ out.info(chalk.white(` ethnotary account sync --address ${alias}`));
137
+ }
138
+
139
+ /**
140
+ * Pre-flight check for account management operations across all networks
141
+ * Estimates gas and checks wallet balance on each network
142
+ * Returns { canProceed, networks: [{ network, gasEstimate, balance, sufficient, error }] }
143
+ */
144
+ async function preflightAccountOperation(aliasOrAddress, address, walletAddress, operation, operationArgs) {
145
+ const networks = getContractNetworks(aliasOrAddress);
146
+ const results = [];
147
+ let allSufficient = true;
148
+ let hasErrors = false;
149
+
150
+ for (const networkKey of networks) {
151
+ try {
152
+ const rpc = getRpcUrl(networkKey);
153
+ if (!rpc) {
154
+ results.push({
155
+ network: networkKey,
156
+ error: 'No RPC configured',
157
+ sufficient: false
158
+ });
159
+ hasErrors = true;
160
+ continue;
161
+ }
162
+
163
+ const provider = new ethers.JsonRpcProvider(rpc);
164
+ const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
165
+
166
+ // Get wallet balance
167
+ const balance = await provider.getBalance(walletAddress);
168
+
169
+ // Get current gas price
170
+ const feeData = await provider.getFeeData();
171
+ const gasPrice = feeData.gasPrice || feeData.maxFeePerGas || ethers.parseUnits('20', 'gwei');
172
+
173
+ // Use fixed gas estimates for account operations
174
+ // (Can't estimate with mock proof - zkSNARK verifier rejects invalid proofs)
175
+ // These are conservative estimates based on typical gas usage
176
+ let gasEstimate;
177
+ if (operation === 'addOwner' || operation === 'removeOwner') {
178
+ gasEstimate = 350000n; // ~300k typical + buffer
179
+ } else if (operation === 'replaceOwner') {
180
+ gasEstimate = 400000n; // slightly higher for replace
181
+ } else if (operation === 'changeRequirement') {
182
+ gasEstimate = 100000n; // simpler operation
183
+ } else {
184
+ gasEstimate = 350000n; // default
185
+ }
186
+
187
+ // Verify contract exists on this network
188
+ try {
189
+ const code = await provider.getCode(address);
190
+ if (code === '0x') {
191
+ results.push({
192
+ network: networkKey,
193
+ error: 'Contract not deployed on this network',
194
+ balance: ethers.formatEther(balance),
195
+ sufficient: false
196
+ });
197
+ hasErrors = true;
198
+ allSufficient = false;
199
+ continue;
200
+ }
201
+ } catch (codeErr) {
202
+ // Continue with balance check even if code check fails
203
+ }
204
+
205
+ // Add 20% buffer to gas estimate
206
+ const gasWithBuffer = (gasEstimate * 120n) / 100n;
207
+ const estimatedCost = gasWithBuffer * gasPrice;
208
+ const sufficient = balance >= estimatedCost;
209
+
210
+ if (!sufficient) {
211
+ allSufficient = false;
212
+ }
213
+
214
+ results.push({
215
+ network: networkKey,
216
+ gasEstimate: Number(gasWithBuffer),
217
+ gasPrice: ethers.formatUnits(gasPrice, 'gwei') + ' gwei',
218
+ estimatedCost: ethers.formatEther(estimatedCost) + ' ETH',
219
+ balance: ethers.formatEther(balance) + ' ETH',
220
+ sufficient,
221
+ error: null
222
+ });
223
+
224
+ } catch (err) {
225
+ results.push({
226
+ network: networkKey,
227
+ error: err.message,
228
+ sufficient: false
229
+ });
230
+ hasErrors = true;
231
+ allSufficient = false;
232
+ }
233
+ }
234
+
235
+ return {
236
+ canProceed: allSufficient && !hasErrors,
237
+ allSufficient,
238
+ hasErrors,
239
+ networks: results
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Display preflight results to user
245
+ */
246
+ function displayPreflightResults(preflight, out) {
247
+ const chalk = require('chalk');
248
+
249
+ if (!out.json) {
250
+ console.log(chalk.cyan('\n📋 Pre-flight Check Results:\n'));
251
+
252
+ for (const result of preflight.networks) {
253
+ if (result.error) {
254
+ console.log(chalk.red(` ✗ ${result.network}: ${result.error}`));
255
+ } else if (!result.sufficient) {
256
+ console.log(chalk.yellow(` ⚠ ${result.network}: Insufficient balance`));
257
+ console.log(chalk.gray(` Balance: ${result.balance}`));
258
+ console.log(chalk.gray(` Required: ~${result.estimatedCost}`));
259
+ } else {
260
+ console.log(chalk.green(` ✓ ${result.network}: Ready`));
261
+ console.log(chalk.gray(` Balance: ${result.balance}`));
262
+ console.log(chalk.gray(` Est. cost: ~${result.estimatedCost}`));
263
+ }
264
+ }
265
+ console.log('');
266
+ }
267
+
268
+ return preflight;
269
+ }
270
+
271
+ module.exports = {
272
+ MULTISIG_ABI,
273
+ fetchAccountStateAcrossNetworks,
274
+ analyzeAccountSync,
275
+ handleDecoupling,
276
+ preflightAccountOperation,
277
+ displayPreflightResults
278
+ };
@@ -0,0 +1,335 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const ETHNOTARY_DIR = path.join(os.homedir(), '.ethnotary');
6
+ const CONFIG_PATH = path.join(ETHNOTARY_DIR, 'config.json');
7
+
8
+ // Comprehensive network definitions
9
+ const DEFAULT_NETWORKS = {
10
+ // Mainnets
11
+ ethereum: { name: 'Ethereum Mainnet', chainId: 1, testnet: false },
12
+ optimism: { name: 'Optimism', chainId: 10, testnet: false },
13
+ base: { name: 'Base', chainId: 8453, testnet: false },
14
+ arbitrum: { name: 'Arbitrum One', chainId: 42161, testnet: false },
15
+ 'arbitrum-nova': { name: 'Arbitrum Nova', chainId: 42170, testnet: false },
16
+ 'zksync-era': { name: 'zkSync Era', chainId: 324, testnet: false },
17
+ scroll: { name: 'Scroll', chainId: 534352, testnet: false },
18
+ 'polygon-zkevm': { name: 'Polygon zkEVM', chainId: 1101, testnet: false },
19
+ linea: { name: 'Linea', chainId: 59144, testnet: false },
20
+ polygon: { name: 'Polygon PoS', chainId: 137, testnet: false },
21
+ gnosis: { name: 'Gnosis Chain', chainId: 100, testnet: false },
22
+ avalanche: { name: 'Avalanche C-Chain', chainId: 43114, testnet: false },
23
+ celo: { name: 'Celo', chainId: 42220, testnet: false },
24
+ soneium: { name: 'Soneium', chainId: 1868, testnet: false },
25
+
26
+ // Testnets
27
+ sepolia: { name: 'Sepolia', chainId: 11155111, testnet: true },
28
+ 'base-sepolia': { name: 'Base Sepolia', chainId: 84532, testnet: true },
29
+ 'arbitrum-sepolia': { name: 'Arbitrum Sepolia', chainId: 421614, testnet: true },
30
+ 'polygon-mumbai': { name: 'Polygon Mumbai', chainId: 80001, testnet: true },
31
+ 'polygon-amoy': { name: 'Polygon Amoy', chainId: 80002, testnet: true },
32
+ 'avalanche-fuji': { name: 'Avalanche Fuji', chainId: 43113, testnet: true }
33
+ };
34
+
35
+ // Build chain ID to network key mapping
36
+ const CHAIN_ID_TO_NETWORK = {};
37
+ for (const [key, config] of Object.entries(DEFAULT_NETWORKS)) {
38
+ CHAIN_ID_TO_NETWORK[config.chainId] = key;
39
+ }
40
+
41
+ // RPC provider suggestions
42
+ const RPC_PROVIDERS = [
43
+ { name: 'Infura', url: 'https://infura.io' },
44
+ { name: 'Alchemy', url: 'https://alchemy.com' },
45
+ { name: 'QuickNode', url: 'https://quicknode.com' },
46
+ { name: 'PublicNode', url: 'https://publicnode.com' }
47
+ ];
48
+
49
+ // Load config from ~/.ethnotary/config.json
50
+ function loadConfig() {
51
+ if (!fs.existsSync(CONFIG_PATH)) {
52
+ return { networks: {}, rpc: {} };
53
+ }
54
+ try {
55
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
56
+ if (!cfg.networks) cfg.networks = {};
57
+ if (!cfg.rpc) cfg.rpc = {};
58
+ return cfg;
59
+ } catch {
60
+ return { networks: {}, rpc: {} };
61
+ }
62
+ }
63
+
64
+ // Save config
65
+ function saveConfig(config) {
66
+ if (!fs.existsSync(ETHNOTARY_DIR)) {
67
+ fs.mkdirSync(ETHNOTARY_DIR, { recursive: true });
68
+ }
69
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
70
+ }
71
+
72
+ // Get RPC URL for a network (config file > env var)
73
+ function getRpcUrl(networkName) {
74
+ const cfg = loadConfig();
75
+
76
+ // Priority 1: Config file
77
+ if (cfg.rpc[networkName]) {
78
+ return cfg.rpc[networkName];
79
+ }
80
+
81
+ // Priority 2: Environment variable (legacy support)
82
+ const envVar = `${networkName.toUpperCase().replace(/-/g, '_')}_RPC_URL`;
83
+ if (process.env[envVar]) {
84
+ return process.env[envVar];
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ const DEFAULT_NETWORK = 'sepolia';
91
+
92
+ // Resolve network from name or chain ID
93
+ function resolveNetwork(networkOrChainId) {
94
+ const cfg = loadConfig();
95
+
96
+ // Check if it's a chain ID (number)
97
+ const chainId = parseInt(networkOrChainId);
98
+ if (!isNaN(chainId)) {
99
+ // Look up by chain ID
100
+ const networkKey = CHAIN_ID_TO_NETWORK[chainId];
101
+ if (networkKey) {
102
+ return { key: networkKey, config: DEFAULT_NETWORKS[networkKey] };
103
+ }
104
+ // Check custom networks
105
+ for (const [key, config] of Object.entries(cfg.networks || {})) {
106
+ if (config.chainId === chainId) {
107
+ return { key, config };
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ // It's a network name - normalize it
114
+ const normalized = networkOrChainId.toLowerCase().trim();
115
+
116
+ // Check default networks
117
+ if (DEFAULT_NETWORKS[normalized]) {
118
+ return { key: normalized, config: DEFAULT_NETWORKS[normalized] };
119
+ }
120
+
121
+ // Check custom networks
122
+ if (cfg.networks && cfg.networks[normalized]) {
123
+ return { key: normalized, config: cfg.networks[normalized] };
124
+ }
125
+
126
+ return null;
127
+ }
128
+
129
+ // Validate network and return helpful error if not found
130
+ function validateNetwork(networkOrChainId) {
131
+ const resolved = resolveNetwork(networkOrChainId);
132
+
133
+ if (!resolved) {
134
+ const chalk = require('chalk');
135
+ const allNetworks = { ...DEFAULT_NETWORKS, ...loadConfig().networks };
136
+ const networkList = Object.entries(allNetworks)
137
+ .map(([key, cfg]) => ` ${key} (${cfg.chainId})`)
138
+ .join('\n');
139
+
140
+ console.log(chalk.red(`\n✗ Unknown network: "${networkOrChainId}"`));
141
+ console.log(chalk.yellow('\nPlease double-check the network name or chain ID.'));
142
+ console.log(chalk.gray('\nSupported networks:'));
143
+ console.log(chalk.gray(networkList));
144
+ console.log(chalk.cyan('\nTo add a custom network:'));
145
+ console.log(chalk.white(` ethnotary config network <name> --chain-id <id> --rpc <url>`));
146
+ console.log(chalk.cyan('\nTo request support for a new network:'));
147
+ console.log(chalk.white(' https://github.com/ethnotary/cli/issues/new'));
148
+
149
+ return null;
150
+ }
151
+
152
+ return resolved;
153
+ }
154
+
155
+ // Ensure RPC is configured for a network, prompt if missing
156
+ async function ensureRpcConfigured(networkKey, networkConfig, options = {}) {
157
+ const rpc = getRpcUrl(networkKey);
158
+
159
+ if (rpc) {
160
+ return rpc;
161
+ }
162
+
163
+ // In JSON mode, fail instead of prompting
164
+ if (options.json) {
165
+ const chalk = require('chalk');
166
+ console.log(chalk.red(`\n✗ No RPC URL configured for ${networkConfig.name}`));
167
+ console.log(chalk.cyan('\nTo configure an RPC URL, run:'));
168
+ console.log(chalk.white(` ethnotary config rpc ${networkKey} --url <your-rpc-url>`));
169
+ return null;
170
+ }
171
+
172
+ // Interactive prompt
173
+ return await promptForRpc(networkKey, networkConfig);
174
+ }
175
+
176
+ // Prompt for RPC URL if not configured
177
+ async function promptForRpc(networkName, networkConfig) {
178
+ const inquirer = require('inquirer');
179
+ const chalk = require('chalk');
180
+
181
+ console.log(chalk.yellow(`\n⚠️ No RPC URL configured for ${networkConfig.name}.`));
182
+ console.log(chalk.cyan('\nTo configure an RPC URL, run:'));
183
+ console.log(chalk.white(` ethnotary config rpc ${networkName} --url <your-rpc-url>`));
184
+ console.log(chalk.gray('\nOr run interactively:'));
185
+ console.log(chalk.white(` ethnotary config rpc ${networkName}`));
186
+ console.log(chalk.gray('\nWhere to get RPC URLs:'));
187
+ for (const provider of RPC_PROVIDERS) {
188
+ console.log(chalk.gray(` • ${provider.name}: ${provider.url}`));
189
+ }
190
+ console.log('');
191
+
192
+ const { rpcUrl } = await inquirer.prompt([{
193
+ type: 'input',
194
+ name: 'rpcUrl',
195
+ message: `Enter RPC URL for ${networkConfig.name}:`,
196
+ validate: (input) => {
197
+ if (!input.startsWith('http://') && !input.startsWith('https://')) {
198
+ return 'RPC URL must start with http:// or https://';
199
+ }
200
+ return true;
201
+ }
202
+ }]);
203
+
204
+ // Save to config file
205
+ const cfg = loadConfig();
206
+ cfg.rpc[networkName] = rpcUrl;
207
+ saveConfig(cfg);
208
+ console.log(chalk.green(`✓ RPC URL saved to ~/.ethnotary/config.json\n`));
209
+
210
+ return rpcUrl;
211
+ }
212
+
213
+ async function getNetwork(networkName) {
214
+ const cfg = loadConfig();
215
+
216
+ // Get network config (custom > default)
217
+ const networkConfig = cfg.networks[networkName] || DEFAULT_NETWORKS[networkName];
218
+ if (!networkConfig) {
219
+ const available = [...Object.keys(DEFAULT_NETWORKS), ...Object.keys(cfg.networks)];
220
+ throw new Error(`Unknown network: ${networkName}. Available: ${[...new Set(available)].join(', ')}`);
221
+ }
222
+
223
+ let rpc = getRpcUrl(networkName);
224
+
225
+ // If no RPC configured, prompt for one
226
+ if (!rpc) {
227
+ rpc = await promptForRpc(networkName, networkConfig);
228
+ }
229
+
230
+ return {
231
+ name: networkConfig.name,
232
+ rpc,
233
+ chainId: networkConfig.chainId
234
+ };
235
+ }
236
+
237
+ function listNetworks() {
238
+ const cfg = loadConfig();
239
+ const allNetworks = { ...DEFAULT_NETWORKS, ...cfg.networks };
240
+
241
+ return Object.entries(allNetworks).map(([key, value]) => ({
242
+ key,
243
+ name: value.name,
244
+ chainId: value.chainId,
245
+ configured: !!getRpcUrl(key)
246
+ }));
247
+ }
248
+
249
+ // Parse multiple networks from comma-separated string (names or chain IDs)
250
+ function parseNetworks(networkString) {
251
+ if (!networkString) return [];
252
+
253
+ const parts = networkString.split(',').map(s => s.trim()).filter(Boolean);
254
+ const results = [];
255
+ const errors = [];
256
+
257
+ for (const part of parts) {
258
+ const resolved = resolveNetwork(part);
259
+ if (resolved) {
260
+ results.push(resolved);
261
+ } else {
262
+ errors.push(part);
263
+ }
264
+ }
265
+
266
+ return { networks: results, errors };
267
+ }
268
+
269
+ // Validate multiple networks and show helpful errors
270
+ function validateNetworks(networkString) {
271
+ const { networks, errors } = parseNetworks(networkString);
272
+
273
+ if (errors.length > 0) {
274
+ const chalk = require('chalk');
275
+ const allNetworks = { ...DEFAULT_NETWORKS, ...loadConfig().networks };
276
+ const networkList = Object.entries(allNetworks)
277
+ .map(([key, cfg]) => ` ${key} (${cfg.chainId})`)
278
+ .join('\n');
279
+
280
+ console.log(chalk.red(`\n✗ Unknown network(s): ${errors.join(', ')}`));
281
+ console.log(chalk.yellow('\nPlease double-check the network name(s) or chain ID(s).'));
282
+ console.log(chalk.gray('\nSupported networks:'));
283
+ console.log(chalk.gray(networkList));
284
+ console.log(chalk.cyan('\nTo add a custom network:'));
285
+ console.log(chalk.white(` ethnotary config network <name> --chain-id <id> --rpc <url>`));
286
+ console.log(chalk.cyan('\nTo request support for a new network:'));
287
+ console.log(chalk.white(' https://github.com/ethnotary/cli/issues/new'));
288
+
289
+ return null;
290
+ }
291
+
292
+ return networks;
293
+ }
294
+
295
+ // Ensure RPC is configured for multiple networks, prompt for missing ones
296
+ async function ensureRpcsConfigured(networks, options = {}) {
297
+ const chalk = require('chalk');
298
+ const results = [];
299
+
300
+ for (const { key, config } of networks) {
301
+ const rpc = getRpcUrl(key);
302
+
303
+ if (rpc) {
304
+ results.push({ key, config, rpc });
305
+ } else {
306
+ if (options.json) {
307
+ console.log(chalk.red(`\n✗ No RPC URL configured for ${config.name}`));
308
+ console.log(chalk.cyan('\nTo configure an RPC URL, run:'));
309
+ console.log(chalk.white(` ethnotary config rpc ${key} --url <your-rpc-url>`));
310
+ return null;
311
+ }
312
+
313
+ // Interactive prompt
314
+ const promptedRpc = await promptForRpc(key, config);
315
+ results.push({ key, config, rpc: promptedRpc });
316
+ }
317
+ }
318
+
319
+ return results;
320
+ }
321
+
322
+ module.exports = {
323
+ DEFAULT_NETWORKS,
324
+ DEFAULT_NETWORK,
325
+ CHAIN_ID_TO_NETWORK,
326
+ getNetwork,
327
+ listNetworks,
328
+ getRpcUrl,
329
+ resolveNetwork,
330
+ validateNetwork,
331
+ ensureRpcConfigured,
332
+ parseNetworks,
333
+ validateNetworks,
334
+ ensureRpcsConfigured
335
+ };