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,146 @@
|
|
|
1
|
+
const { ethers } = require('ethers');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
const ETHNOTARY_DIR = path.join(os.homedir(), '.ethnotary');
|
|
7
|
+
const KEYSTORE_PATH = path.join(ETHNOTARY_DIR, 'keystore.json');
|
|
8
|
+
|
|
9
|
+
// Ensure .ethnotary directory exists
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
if (!fs.existsSync(ETHNOTARY_DIR)) {
|
|
12
|
+
fs.mkdirSync(ETHNOTARY_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get wallet from various sources (priority order):
|
|
18
|
+
* 1. --private-key flag
|
|
19
|
+
* 2. PRIVATE_KEY env var
|
|
20
|
+
* 3. Encrypted keystore (prompts for password)
|
|
21
|
+
*/
|
|
22
|
+
async function getWallet(options = {}) {
|
|
23
|
+
// Priority 1: Direct private key from CLI flag
|
|
24
|
+
if (options.privateKey) {
|
|
25
|
+
try {
|
|
26
|
+
return new ethers.Wallet(options.privateKey);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
throw new Error(`Invalid private key provided: ${e.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Priority 2: Environment variable
|
|
33
|
+
if (process.env.PRIVATE_KEY) {
|
|
34
|
+
try {
|
|
35
|
+
return new ethers.Wallet(process.env.PRIVATE_KEY);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error(`Invalid PRIVATE_KEY in environment: ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Priority 3: Encrypted keystore
|
|
42
|
+
if (keystoreExists()) {
|
|
43
|
+
if (!options.password) {
|
|
44
|
+
// In non-interactive mode (--json), we can't prompt
|
|
45
|
+
if (options.json) {
|
|
46
|
+
throw new Error('No private key provided. Use --private-key or set PRIVATE_KEY env var.');
|
|
47
|
+
}
|
|
48
|
+
// Interactive mode - prompt for password
|
|
49
|
+
const inquirer = require('inquirer');
|
|
50
|
+
const { password } = await inquirer.prompt([{
|
|
51
|
+
type: 'password',
|
|
52
|
+
name: 'password',
|
|
53
|
+
message: 'Enter keystore password:',
|
|
54
|
+
mask: '*'
|
|
55
|
+
}]);
|
|
56
|
+
options.password = password;
|
|
57
|
+
}
|
|
58
|
+
return await loadKeystore(options.password);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error('No wallet configured. Use --private-key, set PRIVATE_KEY env var, or run "ethnotary wallet init"');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if keystore file exists
|
|
66
|
+
*/
|
|
67
|
+
function keystoreExists() {
|
|
68
|
+
return fs.existsSync(KEYSTORE_PATH);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create new wallet and save encrypted keystore
|
|
73
|
+
*/
|
|
74
|
+
async function createKeystore(password) {
|
|
75
|
+
ensureDir();
|
|
76
|
+
const wallet = ethers.Wallet.createRandom();
|
|
77
|
+
const encryptedJson = await wallet.encrypt(password);
|
|
78
|
+
fs.writeFileSync(KEYSTORE_PATH, encryptedJson);
|
|
79
|
+
return wallet;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Import existing key/mnemonic and save encrypted keystore
|
|
84
|
+
*/
|
|
85
|
+
async function importKeystore(keyOrMnemonic, password) {
|
|
86
|
+
ensureDir();
|
|
87
|
+
let wallet;
|
|
88
|
+
|
|
89
|
+
// Try as mnemonic first
|
|
90
|
+
if (keyOrMnemonic.includes(' ')) {
|
|
91
|
+
try {
|
|
92
|
+
wallet = ethers.Wallet.fromPhrase(keyOrMnemonic);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw new Error(`Invalid mnemonic phrase: ${e.message}`);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// Try as private key
|
|
98
|
+
try {
|
|
99
|
+
wallet = new ethers.Wallet(keyOrMnemonic);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(`Invalid private key: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const encryptedJson = await wallet.encrypt(password);
|
|
106
|
+
fs.writeFileSync(KEYSTORE_PATH, encryptedJson);
|
|
107
|
+
return wallet;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load and decrypt keystore
|
|
112
|
+
*/
|
|
113
|
+
async function loadKeystore(password) {
|
|
114
|
+
if (!keystoreExists()) {
|
|
115
|
+
throw new Error('No keystore found. Run "ethnotary wallet init" first.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const encryptedJson = fs.readFileSync(KEYSTORE_PATH, 'utf8');
|
|
119
|
+
try {
|
|
120
|
+
return await ethers.Wallet.fromEncryptedJson(encryptedJson, password);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
throw new Error('Incorrect password or corrupted keystore');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get wallet address from keystore without decrypting
|
|
128
|
+
*/
|
|
129
|
+
function getKeystoreAddress() {
|
|
130
|
+
if (!keystoreExists()) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const encryptedJson = JSON.parse(fs.readFileSync(KEYSTORE_PATH, 'utf8'));
|
|
134
|
+
return encryptedJson.address ? `0x${encryptedJson.address}` : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
getWallet,
|
|
139
|
+
keystoreExists,
|
|
140
|
+
createKeystore,
|
|
141
|
+
importKeystore,
|
|
142
|
+
loadKeystore,
|
|
143
|
+
getKeystoreAddress,
|
|
144
|
+
ETHNOTARY_DIR,
|
|
145
|
+
KEYSTORE_PATH
|
|
146
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized constants for ethnotary CLI
|
|
3
|
+
*
|
|
4
|
+
* UPDATE THESE ADDRESSES when deploying new factory/verifier versions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Default MSA Factory address - same across all EVM networks via CREATE2
|
|
8
|
+
const DEFAULT_MSA_FACTORY = '0x25115564780D3Da99623EaeB9f4eFE88eD801261';
|
|
9
|
+
|
|
10
|
+
// Default PIN Verifier (Groth16Verifier) address - same across all EVM networks via CREATE2
|
|
11
|
+
const DEFAULT_PIN_VERIFIER = '0x65ee46C4d21405f4a4C8e9d0F8a3832c1B885ab4';
|
|
12
|
+
|
|
13
|
+
// MSAFactory ABI - functions needed for account creation
|
|
14
|
+
const MSA_FACTORY_ABI = [
|
|
15
|
+
"function newMSA(address[] calldata _owners, uint _required, bytes32 _pinHash, string calldata _name) external payable returns (address)",
|
|
16
|
+
"function predictMSAAddress(address[] calldata _owners, uint _required, bytes32 _pinHash, string calldata _name) external view returns (address)",
|
|
17
|
+
"function notaryFee() view returns (uint256)",
|
|
18
|
+
"function pinVerifier() view returns (address)",
|
|
19
|
+
"event NewMSACreated(address indexed msaAddress)"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// MultiSig ABI - functions needed for account management
|
|
23
|
+
const MULTISIG_ABI = [
|
|
24
|
+
"function getOwners() view returns (address[])",
|
|
25
|
+
"function required() view returns (uint)",
|
|
26
|
+
"function isOwner(address) view returns (bool)",
|
|
27
|
+
"function pinHash() view returns (bytes32)",
|
|
28
|
+
"function pinNonce() view returns (uint256)",
|
|
29
|
+
"function addOwner(address accountOwner, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public",
|
|
30
|
+
"function removeOwner(address accountOwner, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public",
|
|
31
|
+
"function replaceOwner(address accountOwner, address newOwner, uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC) public"
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the factory address for a given network
|
|
36
|
+
* Priority: network-specific env var > generic env var > default
|
|
37
|
+
*
|
|
38
|
+
* @param {string} network - Network key (e.g., 'sepolia', 'base-sepolia')
|
|
39
|
+
* @returns {string} Factory address
|
|
40
|
+
*/
|
|
41
|
+
function getFactoryAddress(network) {
|
|
42
|
+
if (network) {
|
|
43
|
+
const envKey = `${network.toUpperCase().replace('-', '_')}_FACTORY_ADDRESS`;
|
|
44
|
+
if (process.env[envKey]) {
|
|
45
|
+
return process.env[envKey];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return process.env.MSA_FACTORY_ADDRESS || DEFAULT_MSA_FACTORY;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the PIN verifier address
|
|
53
|
+
* Priority: env var > default
|
|
54
|
+
*
|
|
55
|
+
* @returns {string} Verifier address
|
|
56
|
+
*/
|
|
57
|
+
function getVerifierAddress() {
|
|
58
|
+
return process.env.PIN_VERIFIER_ADDRESS || DEFAULT_PIN_VERIFIER;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
DEFAULT_MSA_FACTORY,
|
|
63
|
+
DEFAULT_PIN_VERIFIER,
|
|
64
|
+
MSA_FACTORY_ABI,
|
|
65
|
+
MULTISIG_ABI,
|
|
66
|
+
getFactoryAddress,
|
|
67
|
+
getVerifierAddress
|
|
68
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { ethers } = require('ethers');
|
|
5
|
+
|
|
6
|
+
const ETHNOTARY_DIR = path.join(os.homedir(), '.ethnotary');
|
|
7
|
+
const CONTACTS_PATH = path.join(ETHNOTARY_DIR, 'contacts.json');
|
|
8
|
+
|
|
9
|
+
// Ensure .ethnotary directory exists
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
if (!fs.existsSync(ETHNOTARY_DIR)) {
|
|
12
|
+
fs.mkdirSync(ETHNOTARY_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Load contacts file
|
|
17
|
+
function loadContacts() {
|
|
18
|
+
if (!fs.existsSync(CONTACTS_PATH)) {
|
|
19
|
+
return { contacts: {} };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(CONTACTS_PATH, 'utf8'));
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return { contacts: {} };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Save contacts file
|
|
29
|
+
function saveContacts(data) {
|
|
30
|
+
ensureDir();
|
|
31
|
+
fs.writeFileSync(CONTACTS_PATH, JSON.stringify(data, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Add or update a contact for an owner address
|
|
36
|
+
* @param {string} address - Owner's Ethereum address
|
|
37
|
+
* @param {Object} contactInfo - Contact methods { telegram, whatsapp, email, webhook }
|
|
38
|
+
*/
|
|
39
|
+
function addContact(address, contactInfo) {
|
|
40
|
+
if (!ethers.isAddress(address)) {
|
|
41
|
+
throw new Error(`Invalid address: ${address}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const normalizedAddress = ethers.getAddress(address);
|
|
45
|
+
const data = loadContacts();
|
|
46
|
+
|
|
47
|
+
// Merge with existing contact info
|
|
48
|
+
data.contacts[normalizedAddress] = {
|
|
49
|
+
...data.contacts[normalizedAddress],
|
|
50
|
+
...contactInfo,
|
|
51
|
+
updatedAt: new Date().toISOString()
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
saveContacts(data);
|
|
55
|
+
return data.contacts[normalizedAddress];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get contact info for an address
|
|
60
|
+
*/
|
|
61
|
+
function getContact(address) {
|
|
62
|
+
if (!ethers.isAddress(address)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const normalizedAddress = ethers.getAddress(address);
|
|
67
|
+
const data = loadContacts();
|
|
68
|
+
return data.contacts[normalizedAddress] || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get contacts for multiple addresses
|
|
73
|
+
*/
|
|
74
|
+
function getContacts(addresses) {
|
|
75
|
+
const data = loadContacts();
|
|
76
|
+
const results = [];
|
|
77
|
+
|
|
78
|
+
for (const address of addresses) {
|
|
79
|
+
if (ethers.isAddress(address)) {
|
|
80
|
+
const normalizedAddress = ethers.getAddress(address);
|
|
81
|
+
const contact = data.contacts[normalizedAddress];
|
|
82
|
+
if (contact) {
|
|
83
|
+
results.push({
|
|
84
|
+
address: normalizedAddress,
|
|
85
|
+
...contact
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List all contacts
|
|
96
|
+
*/
|
|
97
|
+
function listContacts() {
|
|
98
|
+
const data = loadContacts();
|
|
99
|
+
return Object.entries(data.contacts).map(([address, info]) => ({
|
|
100
|
+
address,
|
|
101
|
+
...info
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Remove a contact
|
|
107
|
+
*/
|
|
108
|
+
function removeContact(address) {
|
|
109
|
+
if (!ethers.isAddress(address)) {
|
|
110
|
+
throw new Error(`Invalid address: ${address}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalizedAddress = ethers.getAddress(address);
|
|
114
|
+
const data = loadContacts();
|
|
115
|
+
|
|
116
|
+
if (!data.contacts[normalizedAddress]) {
|
|
117
|
+
throw new Error(`No contact found for address: ${address}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
delete data.contacts[normalizedAddress];
|
|
121
|
+
saveContacts(data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
addContact,
|
|
126
|
+
getContact,
|
|
127
|
+
getContacts,
|
|
128
|
+
listContacts,
|
|
129
|
+
removeContact,
|
|
130
|
+
CONTACTS_PATH
|
|
131
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { ethers } = require('ethers');
|
|
5
|
+
|
|
6
|
+
const ETHNOTARY_DIR = path.join(os.homedir(), '.ethnotary');
|
|
7
|
+
const CONTRACTS_PATH = path.join(ETHNOTARY_DIR, 'contracts.json');
|
|
8
|
+
|
|
9
|
+
// Ensure .ethnotary directory exists
|
|
10
|
+
function ensureDir() {
|
|
11
|
+
if (!fs.existsSync(ETHNOTARY_DIR)) {
|
|
12
|
+
fs.mkdirSync(ETHNOTARY_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Load contracts file
|
|
17
|
+
function loadContracts() {
|
|
18
|
+
if (!fs.existsSync(CONTRACTS_PATH)) {
|
|
19
|
+
return { contracts: {}, default: null };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(CONTRACTS_PATH, 'utf8'));
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return { contracts: {}, default: null };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Save contracts file
|
|
29
|
+
function saveContracts(data) {
|
|
30
|
+
ensureDir();
|
|
31
|
+
fs.writeFileSync(CONTRACTS_PATH, JSON.stringify(data, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Migrate old single-network format to new multi-network format
|
|
35
|
+
function migrateContract(contract) {
|
|
36
|
+
if (contract.network && !contract.networks) {
|
|
37
|
+
// Old format: { network: "sepolia" } -> New format: { networks: ["sepolia"] }
|
|
38
|
+
return {
|
|
39
|
+
...contract,
|
|
40
|
+
networks: [contract.network],
|
|
41
|
+
network: undefined // Remove old field
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return contract;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save a contract with alias
|
|
49
|
+
* @param {string} alias - Contract alias
|
|
50
|
+
* @param {string} address - Contract address
|
|
51
|
+
* @param {string|string[]} networks - Network(s) the contract is deployed on
|
|
52
|
+
* @param {string} label - Optional label
|
|
53
|
+
*/
|
|
54
|
+
function saveContract(alias, address, networks, label = '') {
|
|
55
|
+
if (!ethers.isAddress(address)) {
|
|
56
|
+
throw new Error(`Invalid address: ${address}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Normalize networks to array
|
|
60
|
+
const networkArray = Array.isArray(networks) ? networks : [networks];
|
|
61
|
+
|
|
62
|
+
const data = loadContracts();
|
|
63
|
+
|
|
64
|
+
// Check if contract already exists - merge networks if so
|
|
65
|
+
if (data.contracts[alias]) {
|
|
66
|
+
const existing = migrateContract(data.contracts[alias]);
|
|
67
|
+
const mergedNetworks = [...new Set([...existing.networks, ...networkArray])];
|
|
68
|
+
data.contracts[alias] = {
|
|
69
|
+
address: address,
|
|
70
|
+
networks: mergedNetworks,
|
|
71
|
+
created: existing.created,
|
|
72
|
+
updated: new Date().toISOString(),
|
|
73
|
+
label: label || existing.label || ''
|
|
74
|
+
};
|
|
75
|
+
} else {
|
|
76
|
+
data.contracts[alias] = {
|
|
77
|
+
address: address,
|
|
78
|
+
networks: networkArray,
|
|
79
|
+
created: new Date().toISOString(),
|
|
80
|
+
label: label || ''
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
saveContracts(data);
|
|
85
|
+
return data.contracts[alias];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Add network(s) to an existing contract
|
|
90
|
+
*/
|
|
91
|
+
function addNetworkToContract(alias, networks) {
|
|
92
|
+
const data = loadContracts();
|
|
93
|
+
if (!data.contracts[alias]) {
|
|
94
|
+
throw new Error(`Contract alias not found: ${alias}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const contract = migrateContract(data.contracts[alias]);
|
|
98
|
+
const networkArray = Array.isArray(networks) ? networks : [networks];
|
|
99
|
+
contract.networks = [...new Set([...contract.networks, ...networkArray])];
|
|
100
|
+
contract.updated = new Date().toISOString();
|
|
101
|
+
|
|
102
|
+
data.contracts[alias] = contract;
|
|
103
|
+
saveContracts(data);
|
|
104
|
+
return contract;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove network from a contract
|
|
109
|
+
*/
|
|
110
|
+
function removeNetworkFromContract(alias, network) {
|
|
111
|
+
const data = loadContracts();
|
|
112
|
+
if (!data.contracts[alias]) {
|
|
113
|
+
throw new Error(`Contract alias not found: ${alias}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const contract = migrateContract(data.contracts[alias]);
|
|
117
|
+
contract.networks = contract.networks.filter(n => n !== network);
|
|
118
|
+
contract.updated = new Date().toISOString();
|
|
119
|
+
|
|
120
|
+
if (contract.networks.length === 0) {
|
|
121
|
+
throw new Error(`Cannot remove last network from contract. Use "ethnotary remove ${alias}" to delete the contract.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
data.contracts[alias] = contract;
|
|
125
|
+
saveContracts(data);
|
|
126
|
+
return contract;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get contract by alias or address
|
|
131
|
+
* Returns { address, networks, label } or throws if not found
|
|
132
|
+
*/
|
|
133
|
+
function getContract(aliasOrAddress) {
|
|
134
|
+
// If it's a valid address, return it directly (no network info)
|
|
135
|
+
if (ethers.isAddress(aliasOrAddress)) {
|
|
136
|
+
return { address: aliasOrAddress, networks: [], label: null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Look up by alias
|
|
140
|
+
const data = loadContracts();
|
|
141
|
+
const contract = data.contracts[aliasOrAddress];
|
|
142
|
+
if (!contract) {
|
|
143
|
+
throw new Error(`Contract alias not found: ${aliasOrAddress}. Use "ethnotary list" to see saved contracts.`);
|
|
144
|
+
}
|
|
145
|
+
return migrateContract(contract);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Resolve address from alias, address, or default
|
|
150
|
+
*/
|
|
151
|
+
function resolveAddress(aliasOrAddress) {
|
|
152
|
+
if (aliasOrAddress) {
|
|
153
|
+
return getContract(aliasOrAddress).address;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Try default
|
|
157
|
+
const defaultContract = getDefaultContract();
|
|
158
|
+
if (defaultContract) {
|
|
159
|
+
return defaultContract.address;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new Error('No address provided and no default contract set. Use --address or run "ethnotary contract default <alias>"');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* List all saved contracts
|
|
167
|
+
*/
|
|
168
|
+
function listContracts() {
|
|
169
|
+
const data = loadContracts();
|
|
170
|
+
return Object.entries(data.contracts).map(([alias, contract]) => ({
|
|
171
|
+
alias,
|
|
172
|
+
...migrateContract(contract),
|
|
173
|
+
isDefault: data.default === alias
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Remove a saved contract
|
|
179
|
+
*/
|
|
180
|
+
function removeContract(alias) {
|
|
181
|
+
const data = loadContracts();
|
|
182
|
+
if (!data.contracts[alias]) {
|
|
183
|
+
throw new Error(`Contract alias not found: ${alias}`);
|
|
184
|
+
}
|
|
185
|
+
delete data.contracts[alias];
|
|
186
|
+
if (data.default === alias) {
|
|
187
|
+
data.default = null;
|
|
188
|
+
}
|
|
189
|
+
saveContracts(data);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get default contract
|
|
194
|
+
*/
|
|
195
|
+
function getDefaultContract() {
|
|
196
|
+
const data = loadContracts();
|
|
197
|
+
if (!data.default) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const contract = data.contracts[data.default];
|
|
201
|
+
if (!contract) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return { alias: data.default, ...contract };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Set default contract
|
|
209
|
+
*/
|
|
210
|
+
function setDefaultContract(alias) {
|
|
211
|
+
const data = loadContracts();
|
|
212
|
+
if (!data.contracts[alias]) {
|
|
213
|
+
throw new Error(`Contract alias not found: ${alias}`);
|
|
214
|
+
}
|
|
215
|
+
data.default = alias;
|
|
216
|
+
saveContracts(data);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a decoupled contract alias when sync fails
|
|
221
|
+
*/
|
|
222
|
+
function createDecoupledContract(originalAlias, address, network, label = '') {
|
|
223
|
+
const decoupledAlias = `${originalAlias}-${network}-decoupled`;
|
|
224
|
+
const data = loadContracts();
|
|
225
|
+
|
|
226
|
+
data.contracts[decoupledAlias] = {
|
|
227
|
+
address: address,
|
|
228
|
+
networks: [network],
|
|
229
|
+
created: new Date().toISOString(),
|
|
230
|
+
label: label || `Decoupled from ${originalAlias}`,
|
|
231
|
+
decoupledFrom: originalAlias,
|
|
232
|
+
decoupledAt: new Date().toISOString()
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
saveContracts(data);
|
|
236
|
+
return { alias: decoupledAlias, ...data.contracts[decoupledAlias] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get contract networks (for interoperability)
|
|
241
|
+
* If no aliasOrAddress provided, uses default contract
|
|
242
|
+
*/
|
|
243
|
+
function getContractNetworks(aliasOrAddress) {
|
|
244
|
+
if (!aliasOrAddress) {
|
|
245
|
+
const defaultContract = getDefaultContract();
|
|
246
|
+
if (defaultContract) {
|
|
247
|
+
return migrateContract(defaultContract).networks || [];
|
|
248
|
+
}
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
const contract = getContract(aliasOrAddress);
|
|
252
|
+
return contract.networks || [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
saveContract,
|
|
257
|
+
getContract,
|
|
258
|
+
resolveAddress,
|
|
259
|
+
listContracts,
|
|
260
|
+
removeContract,
|
|
261
|
+
getDefaultContract,
|
|
262
|
+
setDefaultContract,
|
|
263
|
+
addNetworkToContract,
|
|
264
|
+
removeNetworkFromContract,
|
|
265
|
+
createDecoupledContract,
|
|
266
|
+
getContractNetworks,
|
|
267
|
+
migrateContract,
|
|
268
|
+
CONTRACTS_PATH
|
|
269
|
+
};
|