ethnotary 1.0.0 → 1.0.2

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 CHANGED
@@ -1,15 +1,66 @@
1
1
  # Ethnotary CLI
2
2
 
3
- CLI for managing MultiSig accounts, transactions, and data queries across EVM networks.
3
+ A command-line tool for managing multi-signature wallets across EVM networks. Built for teams, DAOs, and AI agents who need secure, collaborative control over on-chain treasuries.
4
4
 
5
- ## Installation
5
+ **Key Use Cases:**
6
+ - **Treasury Management** – Collaboratively manage funds with multi-sig approval workflows
7
+ - **Payment Collection** – Receive and track payments across multiple networks
8
+ - **AI Agent Integration** – Enable AI agents to submit transactions while humans retain approval authority
9
+ - **Cross-Chain Operations** – Deploy and manage the same wallet on Sepolia, and Base-Sepolia, and more networks to come.
10
+
11
+ With multi-signature approvals, every transaction requires consensus—keeping you in control while enabling automation and collaboration.
12
+
13
+ ## How Multi-Signature Works
14
+
15
+ A multi-signature (multi-sig) wallet requires multiple approvals before a transaction can execute. You define the number of **owners** and how many **confirmations** are required.
16
+
17
+ **Common Configurations:**
18
+
19
+ | Setup | Use Case |
20
+ |-------|----------|
21
+ | **1 of 1** | Single owner, instant execution. Good for personal wallets or AI agents with full autonomy. |
22
+ | **1 of 2** | Either owner can approve. Useful for shared access (e.g., you + your AI agent). |
23
+ | **2 of 2** | Both owners must approve. Maximum security for two-party agreements. |
24
+ | **2 of 3** | Any 2 of 3 owners must approve. Balances security with availability—if one key is lost, funds are still accessible. |
25
+ | **3 of 5** | Majority approval for DAOs or teams. Prevents single points of failure. |
6
26
 
27
+ **Example: AI Agent Integration (1 of 2)**
28
+ ```
29
+ Owners: [Your Wallet, AI Agent Wallet]
30
+ Required: 1
31
+
32
+ → AI agent can submit AND execute transactions autonomously
33
+ → You can also execute transactions when needed
34
+ ```
35
+
36
+ **Example: Human Oversight (2 of 2)**
37
+ ```
38
+ Owners: [Your Wallet, AI Agent Wallet]
39
+ Required: 2
40
+
41
+ → AI agent submits transactions
42
+ → You review and approve before execution
43
+ → Nothing happens without your confirmation
44
+ ```
45
+
46
+ ## Prerequisites
47
+
48
+ - **Node.js** (v16 or higher) - [Download](https://nodejs.org/)
49
+ - **npm** (comes with Node.js)
50
+ - **RPC URL** – Connection to your target network (e.g., from [Infura](https://infura.io), [Alchemy](https://alchemy.com), or a public RPC)
51
+ - **ETH for gas** – Enough cryptocurrency on your target network to pay for transaction fees
52
+
53
+ Verify Node.js installation:
7
54
  ```bash
8
- # Install globally
9
- npm install -g .
55
+ node --version # Should show v16.x.x or higher
56
+ npm --version # Should show 8.x.x or higher
57
+ ```
58
+
59
+ ## Installation
10
60
 
11
- # Or use npm link for development
12
- npm link
61
+ ```bash
62
+ # Install globally from npm
63
+ npm install -g ethnotary
13
64
  ```
14
65
 
15
66
  ## Quick Start
@@ -60,6 +111,31 @@ ethnotary wallet import # Import existing key/mnemonic
60
111
  ethnotary wallet show # Display wallet address
61
112
  ```
62
113
 
114
+ ## Wallet Security
115
+
116
+ Your private keys **never leave your machine**. Ethnotary stores wallet credentials locally in an encrypted keystore file, protected by a password you choose.
117
+
118
+ **Wallet Priority:**
119
+ 1. `--private-key` flag – Explicit override for single commands
120
+ 2. **Encrypted keystore** – Default, password-protected (recommended)
121
+ 3. `PRIVATE_KEY` env var – Fallback for scripts/automation
122
+
123
+ ```bash
124
+ # Create a new encrypted wallet (stored locally)
125
+ ethnotary wallet init
126
+
127
+ # View your wallet address
128
+ ethnotary wallet show
129
+ ```
130
+
131
+ **What stays on your machine:**
132
+ - Private keys (encrypted in keystore)
133
+ - Wallet passwords (never stored, prompted each time)
134
+ - RPC URLs and API keys (in `~/.ethnotary/config.json`)
135
+
136
+ **What goes on-chain:**
137
+ - Only signed transactions you explicitly submit
138
+ - Your public address (visible in transactions)
63
139
  ### Account Management
64
140
 
65
141
  Account management commands apply changes across ALL networks the contract is deployed on.
@@ -333,33 +409,21 @@ All configuration is stored in `~/.ethnotary/`:
333
409
 
334
410
  Supported networks (use `--network <name>` or `--chain-id <id>`):
335
411
 
336
- **Mainnets:**
412
+ **Currently Supported:**
413
+ | Network | Name | Chain ID |
414
+ |---------|------|----------|
415
+ | Sepolia | `sepolia` | 11155111 |
416
+ | Base Sepolia | `base-sepolia` | 84532 |
417
+
418
+ **Coming Soon:**
337
419
  | Network | Name | Chain ID |
338
420
  |---------|------|----------|
339
421
  | Ethereum Mainnet | `ethereum` | 1 |
340
- | Optimism | `optimism` | 10 |
341
422
  | Base | `base` | 8453 |
342
423
  | Arbitrum One | `arbitrum` | 42161 |
343
- | Arbitrum Nova | `arbitrum-nova` | 42170 |
344
- | zkSync Era | `zksync-era` | 324 |
345
- | Scroll | `scroll` | 534352 |
346
- | Polygon zkEVM | `polygon-zkevm` | 1101 |
347
- | Linea | `linea` | 59144 |
424
+ | Optimism | `optimism` | 10 |
348
425
  | Polygon PoS | `polygon` | 137 |
349
- | Gnosis Chain | `gnosis` | 100 |
350
- | Avalanche C-Chain | `avalanche` | 43114 |
351
- | Celo | `celo` | 42220 |
352
- | Soneium | `soneium` | 1868 |
353
-
354
- **Testnets:**
355
- | Network | Name | Chain ID |
356
- |---------|------|----------|
357
- | Sepolia | `sepolia` | 11155111 |
358
- | Base Sepolia | `base-sepolia` | 84532 |
359
- | Arbitrum Sepolia | `arbitrum-sepolia` | 421614 |
360
- | Polygon Mumbai | `polygon-mumbai` | 80001 |
361
- | Polygon Amoy | `polygon-amoy` | 80002 |
362
- | Avalanche Fuji | `avalanche-fuji` | 43113 |
426
+ | *...and more* | | |
363
427
 
364
428
  **Configure RPC URLs:**
365
429
  ```bash
@@ -168,40 +168,75 @@ account
168
168
 
169
169
  try {
170
170
  const address = resolveAddress(options.address);
171
- // Get first network from contract's networks, or use specified network
172
171
  const contractNetworks = getContractNetworks(options.address);
173
- const networkKey = globalOpts.network || contractNetworks[0] || 'sepolia';
174
- const network = await getNetwork(networkKey);
175
- const provider = new ethers.JsonRpcProvider(network.rpc);
172
+ const networksToQuery = contractNetworks.length > 0 ? contractNetworks : ['sepolia'];
176
173
 
177
- // Check if contract exists
178
- const code = await provider.getCode(address);
179
- if (code === '0x') {
180
- out.error(`No contract found at ${address} on ${network.name}`);
181
- return;
182
- }
174
+ out.startSpinner('Fetching account info across all networks...');
183
175
 
184
- const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
176
+ // Fetch balances from all networks in parallel
177
+ const balancePromises = networksToQuery.map(async (networkKey) => {
178
+ try {
179
+ const network = await getNetwork(networkKey);
180
+ const provider = new ethers.JsonRpcProvider(network.rpc);
181
+ const balance = await provider.getBalance(address);
182
+ return { network: networkKey, balance: ethers.formatEther(balance), raw: balance };
183
+ } catch (e) {
184
+ return { network: networkKey, balance: 'unavailable', error: e.message };
185
+ }
186
+ });
185
187
 
186
- out.startSpinner('Fetching account info...');
188
+ // Get account details from first available network
189
+ let owners = [];
190
+ let required = 0;
191
+ let primaryNetwork = null;
187
192
 
188
- const [owners, required, balance] = await Promise.all([
189
- multisig.getOwners(),
190
- multisig.required(),
191
- provider.getBalance(address)
192
- ]);
193
+ for (const networkKey of networksToQuery) {
194
+ try {
195
+ const network = await getNetwork(networkKey);
196
+ const provider = new ethers.JsonRpcProvider(network.rpc);
197
+ const code = await provider.getCode(address);
198
+ if (code !== '0x') {
199
+ const multisig = new ethers.Contract(address, MULTISIG_ABI, provider);
200
+ [owners, required] = await Promise.all([
201
+ multisig.getOwners(),
202
+ multisig.required()
203
+ ]);
204
+ primaryNetwork = networkKey;
205
+ break;
206
+ }
207
+ } catch (e) {
208
+ continue;
209
+ }
210
+ }
211
+
212
+ if (!primaryNetwork) {
213
+ out.failSpinner('Contract not found on any network');
214
+ return;
215
+ }
216
+
217
+ const balances = await Promise.all(balancePromises);
218
+ const totalBalance = balances.reduce((sum, b) => {
219
+ if (b.raw) return sum + b.raw;
220
+ return sum;
221
+ }, 0n);
193
222
 
194
223
  out.succeedSpinner('Account info retrieved');
195
224
 
225
+ // Format balances object
226
+ const balancesByNetwork = {};
227
+ for (const b of balances) {
228
+ balancesByNetwork[b.network] = b.error ? b.balance : `${b.balance} ETH`;
229
+ }
230
+
196
231
  out.print({
197
232
  address,
198
- network: networkKey,
199
- networks: contractNetworks,
233
+ networks: networksToQuery,
200
234
  owners: owners.map(o => o),
201
235
  required: Number(required),
202
236
  ownerCount: owners.length,
203
237
  confirmationsNeeded: `${required} of ${owners.length}`,
204
- balance: ethers.formatEther(balance) + ' ETH'
238
+ balances: balancesByNetwork,
239
+ totalBalance: ethers.formatEther(totalBalance) + ' ETH'
205
240
  });
206
241
 
207
242
  } catch (error) {
@@ -151,26 +151,27 @@ wallet
151
151
  const out = createOutput(globalOpts);
152
152
 
153
153
  try {
154
- // Check for private key in options or env
154
+ // Priority 1: --private-key flag (explicit override)
155
155
  if (globalOpts.privateKey) {
156
156
  const wallet = new ethers.Wallet(globalOpts.privateKey);
157
157
  out.print({ address: wallet.address, source: 'private-key-flag' });
158
158
  return;
159
159
  }
160
160
 
161
- if (process.env.PRIVATE_KEY) {
162
- const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
163
- out.print({ address: wallet.address, source: 'environment' });
164
- return;
165
- }
166
-
167
- // Check keystore
161
+ // Priority 2: Keystore (default if exists)
168
162
  if (keystoreExists()) {
169
163
  const address = getKeystoreAddress();
170
164
  out.print({ address: address, source: 'keystore' });
171
165
  return;
172
166
  }
173
167
 
168
+ // Priority 3: Environment variable (fallback)
169
+ if (process.env.PRIVATE_KEY) {
170
+ const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
171
+ out.print({ address: wallet.address, source: 'environment' });
172
+ return;
173
+ }
174
+
174
175
  out.error('No wallet configured. Use --private-key, set PRIVATE_KEY env var, or run "ethnotary wallet init"');
175
176
 
176
177
  } catch (error) {
package/cli/index.js CHANGED
@@ -558,18 +558,34 @@ async function checkRpcConfig() {
558
558
  const receipt = await tx.wait();
559
559
 
560
560
  // Get deployed address from event or use predicted
561
- let actualAddress = predictedAddress;
561
+ let actualAddress = null;
562
562
  for (const log of receipt.logs) {
563
563
  try {
564
564
  const parsed = factory.interface.parseLog(log);
565
565
  if (parsed && parsed.name === 'NewMSACreated') {
566
- actualAddress = parsed.args.msa;
566
+ // The event has a single indexed address parameter
567
+ actualAddress = parsed.args.msaAddress || parsed.args[0];
567
568
  break;
568
569
  }
569
- } catch (e) {}
570
+ } catch (e) {
571
+ // Log might be from a different contract, try parsing the topic directly
572
+ // NewMSACreated(address indexed msaAddress) - topic[1] contains the address
573
+ if (log.topics && log.topics.length >= 2) {
574
+ const eventSig = ethers.id('NewMSACreated(address)');
575
+ if (log.topics[0] === eventSig) {
576
+ actualAddress = ethers.getAddress('0x' + log.topics[1].slice(26));
577
+ break;
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ // Fallback to predicted address if event parsing failed
584
+ if (!actualAddress) {
585
+ actualAddress = predictedAddress;
570
586
  }
571
587
 
572
- if (!deployedAddress) {
588
+ if (!deployedAddress && actualAddress) {
573
589
  deployedAddress = actualAddress;
574
590
  }
575
591
 
package/cli/utils/auth.js CHANGED
@@ -15,12 +15,12 @@ function ensureDir() {
15
15
 
16
16
  /**
17
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)
18
+ * 1. --private-key flag (explicit override)
19
+ * 2. Encrypted keystore (default if exists)
20
+ * 3. PRIVATE_KEY env var (fallback for scripts/automation)
21
21
  */
22
22
  async function getWallet(options = {}) {
23
- // Priority 1: Direct private key from CLI flag
23
+ // Priority 1: Direct private key from CLI flag (explicit override)
24
24
  if (options.privateKey) {
25
25
  try {
26
26
  return new ethers.Wallet(options.privateKey);
@@ -29,20 +29,14 @@ async function getWallet(options = {}) {
29
29
  }
30
30
  }
31
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
32
+ // Priority 2: Encrypted keystore (default if exists)
42
33
  if (keystoreExists()) {
43
34
  if (!options.password) {
44
- // In non-interactive mode (--json), we can't prompt
35
+ // In non-interactive mode (--json), fall back to env var
45
36
  if (options.json) {
37
+ if (process.env.PRIVATE_KEY) {
38
+ return new ethers.Wallet(process.env.PRIVATE_KEY);
39
+ }
46
40
  throw new Error('No private key provided. Use --private-key or set PRIVATE_KEY env var.');
47
41
  }
48
42
  // Interactive mode - prompt for password
@@ -58,6 +52,15 @@ async function getWallet(options = {}) {
58
52
  return await loadKeystore(options.password);
59
53
  }
60
54
 
55
+ // Priority 3: Environment variable (fallback for scripts/automation)
56
+ if (process.env.PRIVATE_KEY) {
57
+ try {
58
+ return new ethers.Wallet(process.env.PRIVATE_KEY);
59
+ } catch (e) {
60
+ throw new Error(`Invalid PRIVATE_KEY in environment: ${e.message}`);
61
+ }
62
+ }
63
+
61
64
  throw new Error('No wallet configured. Use --private-key, set PRIVATE_KEY env var, or run "ethnotary wallet init"');
62
65
  }
63
66
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  // Default MSA Factory address - same across all EVM networks via CREATE2
8
- const DEFAULT_MSA_FACTORY = '0x25115564780D3Da99623EaeB9f4eFE88eD801261';
8
+ const DEFAULT_MSA_FACTORY = '0x3DEB514B2ac536b8048f5b37182196cf9d5dDD45';
9
9
 
10
10
  // Default PIN Verifier (Groth16Verifier) address - same across all EVM networks via CREATE2
11
11
  const DEFAULT_PIN_VERIFIER = '0x65ee46C4d21405f4a4C8e9d0F8a3832c1B885ab4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethnotary",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI for managing MultiSig accounts, transactions, and data queries across EVM networks",
5
5
  "main": "cli/index.js",
6
6
  "bin": {