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,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
+ };