zoa-wallet 0.3.3 → 0.3.5

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.
Files changed (3) hide show
  1. package/README.md +425 -79
  2. package/dist/index.mjs +224 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,13 +24,16 @@ yarn global add zoa-wallet
24
24
 
25
25
  ```bash
26
26
  # Create a new wallet
27
- zoa init
27
+ zoa wallet create --password mypassword
28
28
 
29
29
  # Check balances across all chains
30
- zoa balance
30
+ zoa balance --password mypassword
31
+
32
+ # Send ETH on Base
33
+ zoa send --chain base --to 0x1234...abcd --amount 0.01 --password mypassword --yes
31
34
 
32
35
  # Show receive addresses with QR codes
33
- zoa receive
36
+ zoa receive --password mypassword
34
37
 
35
38
  # View supported networks
36
39
  zoa chains
@@ -39,133 +42,476 @@ zoa chains
39
42
  zoa prices
40
43
  ```
41
44
 
42
- ## Commands
45
+ ## Command Reference
46
+
47
+ ### `wallet` — Manage Wallets
48
+
49
+ ```bash
50
+ # Create a new wallet
51
+ zoa wallet create [--label <name>] [--color <hex>] [--password <pw>] [--word-count 12|24]
52
+
53
+ # Import from recovery phrase
54
+ zoa wallet import [--label <name>] [--mnemonic "phrase ..."] [--password <pw>]
55
+
56
+ # List all wallets
57
+ zoa wallet list [--password <pw>]
58
+
59
+ # Switch active wallet
60
+ zoa wallet switch [id-or-label]
61
+
62
+ # Rename a wallet
63
+ zoa wallet rename <id> <new-label>
64
+
65
+ # Change wallet color
66
+ zoa wallet color <id> <hex>
67
+
68
+ # Remove a wallet
69
+ zoa wallet remove <id> [--yes]
70
+
71
+ # Show active wallet details
72
+ zoa wallet info [--password <pw>]
73
+ ```
74
+
75
+ ### `balance` — Check Balances
43
76
 
44
- | Command | Description |
45
- |---------|-------------|
46
- | `zoa init` | Create a new wallet or import from recovery phrase |
47
- | `zoa balance` | Check balances across all supported networks |
48
- | `zoa send` | Send tokens to an address |
49
- | `zoa receive` | Show wallet addresses with QR codes |
50
- | `zoa history` | View transaction history |
51
- | `zoa export` | Export private keys or recovery phrase |
52
- | `zoa chains` | List all supported blockchain networks |
53
- | `zoa prices` | View live token prices |
54
- | `zoa config` | Manage configuration (~/.zoa/config.json) |
55
- | `zoa api` | Manage API keys for programmatic access |
77
+ ```bash
78
+ zoa balance [--password <pw>] [--chain <chain>]
79
+ ```
56
80
 
57
- ## JSON Mode (for Scripts & AI Agents)
81
+ | Flag | Description |
82
+ |------|-------------|
83
+ | `--password <pw>` | Wallet password (skip prompt) |
84
+ | `--chain <chain>` | Filter to a specific chain (`base`, `ethereum`, `solana`, etc.) |
58
85
 
59
- Every command supports `--json` for machine-readable output:
86
+ ### `send` Send Tokens
60
87
 
88
+ Interactive:
61
89
  ```bash
62
- # Create wallet non-interactively
63
- zoa --json init --mnemonic "your twelve word phrase here ..." --password "securepass"
90
+ zoa send
91
+ ```
64
92
 
65
- # Get balances as JSON
66
- zoa --json balance --password "securepass"
93
+ One-liner:
94
+ ```bash
95
+ zoa send --chain base --to 0x... --amount 0.01 --password pw --yes
96
+ ```
67
97
 
68
- # Filter by chain
69
- zoa --json balance --password "securepass" --chain solana
98
+ | Flag | Description |
99
+ |------|-------------|
100
+ | `--password <pw>` | Wallet password |
101
+ | `--to <address>` | Recipient address |
102
+ | `--amount <amount>` | Amount to send |
103
+ | `--chain <chain>` | Network (`base`, `ethereum`, `arbitrum`, `optimism`, `bsc`, `solana`) |
104
+ | `--token <symbol>` | Token symbol (e.g. `USDT`, `USDC`). Defaults to native currency |
105
+ | `--max` | Send maximum available balance (auto-deducts gas for native transfers) |
106
+ | `--batch <transfers>` | Batch send: comma-separated `address:amount[:token[:chain]]` |
107
+ | `--yes` | Skip confirmation prompt |
70
108
 
71
- # Get supported chains
72
- zoa --json chains
109
+ ### `receive` Show Addresses & QR Codes
110
+
111
+ ```bash
112
+ zoa receive [--password <pw>] [--chain <chain>]
113
+ ```
114
+
115
+ ### `history` — Transaction History
116
+
117
+ ```bash
118
+ zoa history [--password <pw>] [--chain <chain>] [--limit <n>]
119
+ ```
120
+
121
+ ### `export` — Export Keys
122
+
123
+ ```bash
124
+ # Export private key
125
+ zoa export --password pw --type key --chain evm
126
+
127
+ # Export recovery phrase
128
+ zoa export --password pw --type mnemonic
129
+ ```
130
+
131
+ | Flag | Description |
132
+ |------|-------------|
133
+ | `--password <pw>` | Wallet password |
134
+ | `--type <type>` | `key` or `mnemonic` |
135
+ | `--chain <chain>` | `evm` or `solana` (for `--type key`) |
136
+
137
+ ### `chains` — List Supported Networks
138
+
139
+ ```bash
140
+ zoa chains
141
+ ```
142
+
143
+ ### `prices` — Token Prices
144
+
145
+ ```bash
146
+ # Default price table
147
+ zoa prices
148
+
149
+ # Search for a specific token
150
+ zoa prices search bitcoin
151
+
152
+ # Browse popular tokens interactively
153
+ zoa prices browse
73
154
 
74
- # Get prices
75
- zoa --json prices --tokens "ethereum,solana,bitcoin"
155
+ # Specify tokens
156
+ zoa prices list --tokens "ethereum,solana,bitcoin"
76
157
  ```
77
158
 
78
- ### Example JSON Output
159
+ ### `api` Manage API Keys
79
160
 
80
161
  ```bash
81
- $ zoa --json chains
162
+ # Set API key
163
+ zoa api set <key>
164
+
165
+ # Show current key
166
+ zoa api show
167
+
168
+ # Remove key
169
+ zoa api remove
170
+ ```
171
+
172
+ ### `config` — Configuration
173
+
174
+ ```bash
175
+ # Show config
176
+ zoa config show
177
+
178
+ # Set a value
179
+ zoa config set <key> <value>
180
+ ```
181
+
182
+ ## One-Liner Reference
183
+
184
+ | Task | Command |
185
+ |------|---------|
186
+ | Send ETH on Base | `zoa send --chain base --to 0x... --amount 0.01 --password pw --yes` |
187
+ | Send USDC on Ethereum | `zoa send --chain ethereum --token USDC --to 0x... --amount 100 --password pw --yes` |
188
+ | Send max ETH | `zoa send --chain base --to 0x... --max --password pw --yes` |
189
+ | Send SOL on Solana | `zoa send --chain solana --to ABC... --amount 1.5 --password pw --yes` |
190
+ | Check balance (Base only) | `zoa balance --chain base --password pw` |
191
+ | Export EVM private key | `zoa export --type key --chain evm --password pw` |
192
+ | Get JSON balances | `zoa --json balance --password pw` |
193
+ | Get JSON prices | `zoa --json prices --tokens "ethereum,solana"` |
194
+
195
+ ## Batch Send
196
+
197
+ Send to multiple recipients, tokens, and chains in a single command.
198
+
199
+ **Format:** `address:amount[:token[:chain]]`
200
+ - `token` defaults to the native currency (ETH, SOL, BNB, etc.)
201
+ - `chain` defaults to the `--chain` flag value, or `base` if not specified
202
+
203
+ ### Examples
204
+
205
+ ```bash
206
+ # Same chain, same token, multiple recipients
207
+ zoa send --chain base --batch "0xAAA...111:0.01,0xBBB...222:0.02,0xCCC...333:0.03" --password pw --yes
208
+
209
+ # Multiple tokens on the same chain
210
+ zoa send --chain ethereum --batch "0xAAA...111:100:USDC,0xBBB...222:50:USDT,0xCCC...333:0.1" --password pw --yes
211
+
212
+ # Multiple chains and tokens in one command
213
+ zoa send --batch "0xAAA...111:100:USDC:ethereum,4Jyn...abc:0.5:SOL:solana,0xBBB...222:50:USDT:base" --password pw --yes
214
+ ```
215
+
216
+ ### Batch Output
217
+
218
+ - Each transfer executes sequentially with progress indicators
219
+ - Failed transfers do not stop remaining transfers
220
+ - A summary is shown at the end with success/failure counts
221
+ - JSON mode returns an array of results
222
+
223
+ ## JSON Mode for AI Agents
224
+
225
+ Every command supports `--json` for machine-readable output. Place the flag before the command name.
226
+
227
+ ```bash
228
+ zoa --json <command> [options]
229
+ ```
230
+
231
+ ### Example Outputs
232
+
233
+ **Create wallet:**
234
+ ```bash
235
+ zoa --json wallet create --password "securepass"
236
+ ```
237
+ ```json
238
+ {
239
+ "address": "0x1234...abcd",
240
+ "solanaAddress": "ABC...xyz",
241
+ "label": "Wallet 1"
242
+ }
243
+ ```
244
+
245
+ **Get balances:**
246
+ ```bash
247
+ zoa --json balance --password pw
248
+ ```
249
+ ```json
250
+ {
251
+ "balances": [
252
+ { "chain": "Base", "token": "ETH", "balance": "0.0542", "usd": "170.21" },
253
+ { "chain": "Base", "token": "USDC", "balance": "100.0", "usd": "100.00" }
254
+ ]
255
+ }
256
+ ```
257
+
258
+ **Send tokens:**
259
+ ```bash
260
+ zoa --json send --chain base --to 0x... --amount 0.01 --password pw --yes
261
+ ```
262
+ ```json
263
+ {
264
+ "status": "sent",
265
+ "hash": "0xabc...def",
266
+ "from": "0x1234...abcd",
267
+ "to": "0x5678...efgh",
268
+ "token": "ETH",
269
+ "amount": "0.01",
270
+ "chain": "Base",
271
+ "explorerUrl": "https://basescan.org/tx/0xabc...def"
272
+ }
273
+ ```
274
+
275
+ **Batch send:**
276
+ ```bash
277
+ zoa --json send --batch "0xA:0.01,0xB:0.02" --chain base --password pw --yes
278
+ ```
279
+ ```json
280
+ {
281
+ "batch": true,
282
+ "results": [
283
+ { "to": "0xA...", "amount": "0.01", "token": "ETH", "chain": "Base", "status": "sent", "hash": "0x..." },
284
+ { "to": "0xB...", "amount": "0.02", "token": "ETH", "chain": "Base", "status": "sent", "hash": "0x..." }
285
+ ],
286
+ "summary": { "total": 2, "succeeded": 2, "failed": 0 }
287
+ }
288
+ ```
289
+
290
+ **Get chains:**
291
+ ```bash
292
+ zoa --json chains
82
293
  ```
83
294
  ```json
84
295
  {
85
296
  "chains": [
86
297
  { "name": "Ethereum", "symbol": "ETH", "chainId": 1 },
87
298
  { "name": "Base", "symbol": "ETH", "chainId": 8453 },
88
- { "name": "Solana", "symbol": "SOL", "chainId": -1 },
89
- ...
299
+ { "name": "Solana", "symbol": "SOL", "chainId": -1 }
300
+ ]
301
+ }
302
+ ```
303
+
304
+ **Get prices:**
305
+ ```bash
306
+ zoa --json prices --tokens "ethereum,solana"
307
+ ```
308
+ ```json
309
+ {
310
+ "prices": [
311
+ { "id": "ethereum", "symbol": "ETH", "price": 3245.67, "change24h": 2.34 },
312
+ { "id": "solana", "symbol": "SOL", "price": 142.89, "change24h": -1.12 }
90
313
  ]
91
314
  }
92
315
  ```
93
316
 
94
- ## Non-Interactive Mode
317
+ ## API Reference
95
318
 
96
- All commands accept flags for fully non-interactive usage perfect for CI/CD, scripts, and AI agents:
319
+ The ZOA backend API provides programmatic access. All `/v1` endpoints require an API key in the `Authorization` header.
320
+
321
+ ### Authentication
97
322
 
98
323
  ```bash
99
- # Init without prompts
100
- zoa init --mnemonic "..." --password "..." --word-count 24
324
+ # Set your API key
325
+ zoa api set zoa_sk_your_key_here
101
326
 
102
- # Balance without prompts
103
- zoa balance --password "..." --chain base
327
+ # Use with curl
328
+ curl -H "Authorization: Bearer zoa_sk_your_key_here" https://api.zoa.fun/v1/chains
329
+ ```
104
330
 
105
- # Send without prompts
106
- zoa send --password "..." --to 0x... --amount 0.1 --chain base --yes
331
+ ### Endpoints
107
332
 
108
- # Export without prompts
109
- zoa export --password "..." --type key --chain evm
110
- zoa export --password "..." --type mnemonic
333
+ #### `GET /v1/chains`
334
+ List all supported blockchain networks.
335
+ ```bash
336
+ curl -H "Authorization: Bearer $API_KEY" https://api.zoa.fun/v1/chains
337
+ ```
338
+ ```json
339
+ {
340
+ "data": {
341
+ "chains": [
342
+ { "id": 1, "name": "Ethereum", "symbol": "ETH", "type": "evm" },
343
+ { "id": 8453, "name": "Base", "symbol": "ETH", "type": "evm" },
344
+ { "id": 56, "name": "BNB Smart Chain", "symbol": "BNB", "type": "evm" },
345
+ { "id": 42161, "name": "Arbitrum One", "symbol": "ETH", "type": "evm" },
346
+ { "id": 10, "name": "Optimism", "symbol": "ETH", "type": "evm" },
347
+ { "id": 0, "name": "Solana", "symbol": "SOL", "type": "non-evm" }
348
+ ]
349
+ }
350
+ }
111
351
  ```
112
352
 
113
- ## Supported Networks
353
+ #### `POST /v1/wallets`
354
+ Create a managed wallet. Requires `write` permission.
355
+ ```bash
356
+ curl -X POST -H "Authorization: Bearer $API_KEY" \
357
+ -H "Content-Type: application/json" \
358
+ -d '{"label": "Trading Bot"}' \
359
+ https://api.zoa.fun/v1/wallets
360
+ ```
114
361
 
115
- | Network | Symbol | Type |
116
- |---------|--------|------|
117
- | Ethereum | ETH | EVM |
118
- | Base | ETH | EVM |
119
- | BNB Smart Chain | BNB | EVM |
362
+ #### `GET /v1/wallets`
363
+ List all wallets associated with your API key.
364
+ ```bash
365
+ curl -H "Authorization: Bearer $API_KEY" https://api.zoa.fun/v1/wallets
366
+ ```
120
367
 
121
- | Arbitrum One | ETH | EVM |
122
- | Optimism | ETH | EVM |
123
- | Solana | SOL | Non-EVM |
124
- | Hyperliquid | HYPE | Non-EVM |
368
+ #### `GET /v1/wallets/{id}`
369
+ Get a specific wallet by UUID.
370
+ ```bash
371
+ curl -H "Authorization: Bearer $API_KEY" https://api.zoa.fun/v1/wallets/{id}
372
+ ```
125
373
 
126
- ## Configuration
374
+ #### `GET /v1/balances/{address}`
375
+ Get token balances for an address. Optionally filter by chain.
376
+ ```bash
377
+ curl -H "Authorization: Bearer $API_KEY" "https://api.zoa.fun/v1/balances/0x1234...?chain=8453"
378
+ ```
127
379
 
128
- ZOA stores configuration in `~/.zoa/`:
380
+ #### `GET /v1/balances/{address}/all`
381
+ Get balances across all supported chains.
382
+ ```bash
383
+ curl -H "Authorization: Bearer $API_KEY" https://api.zoa.fun/v1/balances/0x1234.../all
384
+ ```
129
385
 
386
+ #### `POST /v1/transfer`
387
+ Create a transfer. Requires `write` permission.
130
388
  ```bash
131
- # View config
132
- zoa config show
389
+ curl -X POST -H "Authorization: Bearer $API_KEY" \
390
+ -H "Content-Type: application/json" \
391
+ -d '{
392
+ "from_wallet_id": "uuid",
393
+ "to_address": "0x...",
394
+ "chain_id": 8453,
395
+ "amount": "0.01",
396
+ "token_address": null,
397
+ "priority": "standard"
398
+ }' \
399
+ https://api.zoa.fun/v1/transfer
400
+ ```
133
401
 
134
- # Set default chain
135
- zoa config set defaultChain base
402
+ #### `GET /v1/transactions/{address}`
403
+ Get transaction history for an address.
404
+ ```bash
405
+ curl -H "Authorization: Bearer $API_KEY" "https://api.zoa.fun/v1/transactions/0x1234...?chain=8453&limit=10&offset=0"
406
+ ```
136
407
 
137
- # Set API key
138
- zoa api set zoa_sk_your_key_here
408
+ #### `GET /v1/transactions/hash/{hash}`
409
+ Get a specific transaction by hash.
410
+ ```bash
411
+ curl -H "Authorization: Bearer $API_KEY" "https://api.zoa.fun/v1/transactions/hash/0xabc...?chain=8453"
412
+ ```
139
413
 
140
- # Set custom API URL
141
- zoa config set apiUrl https://api.zoa.fun
414
+ #### `GET /v1/prices`
415
+ Get cached token prices.
416
+ ```bash
417
+ curl -H "Authorization: Bearer $API_KEY" "https://api.zoa.fun/v1/prices?tokens=ethereum,solana,bitcoin"
142
418
  ```
143
419
 
144
- ## SDK
420
+ #### `GET /v1/prices/{token_id}`
421
+ Get price for a single token.
422
+ ```bash
423
+ curl -H "Authorization: Bearer $API_KEY" https://api.zoa.fun/v1/prices/ethereum
424
+ ```
145
425
 
146
- For programmatic usage in your applications, use the ZOA SDK:
426
+ #### `GET /v1/gas/{chain_id}`
427
+ Get gas estimates (slow/standard/fast) for a chain.
428
+ ```bash
429
+ curl -H "Authorization: Bearer $API_KEY" https://api.zoa.fun/v1/gas/8453
430
+ ```
431
+ ```json
432
+ {
433
+ "data": {
434
+ "chain_id": 8453,
435
+ "slow": { "gwei": "0.001", "estimated_seconds": 30 },
436
+ "standard": { "gwei": "0.002", "estimated_seconds": 15 },
437
+ "fast": { "gwei": "0.005", "estimated_seconds": 5 }
438
+ }
439
+ }
440
+ ```
147
441
 
148
- ```typescript
149
- import { ZoaWallet } from '@zoa/sdk';
442
+ #### Key Management
150
443
 
151
- // Create a new wallet
152
- const wallet = await ZoaWallet.create();
153
- console.log(wallet.evmAddress); // 0x...
154
- console.log(wallet.solanaAddress); // ABC...
444
+ ```bash
445
+ # Create API key (requires admin permission)
446
+ curl -X POST -H "Authorization: Bearer $ADMIN_KEY" \
447
+ -H "Content-Type: application/json" \
448
+ -d '{"name": "My Bot", "permissions": ["read", "write"], "rate_limit_per_min": 60}' \
449
+ https://api.zoa.fun/v1/keys
155
450
 
156
- // Sign data
157
- const sig = await wallet.sign(data, 'evm');
451
+ # List API keys
452
+ curl -H "Authorization: Bearer $ADMIN_KEY" https://api.zoa.fun/v1/keys
158
453
 
159
- // Export keys
160
- const pk = wallet.exportPrivateKey('evm');
454
+ # Revoke an API key
455
+ curl -X DELETE -H "Authorization: Bearer $ADMIN_KEY" https://api.zoa.fun/v1/keys/{id}
161
456
  ```
162
457
 
458
+ ## Supported Networks
459
+
460
+ | Network | Symbol | Chain ID | Type | Explorer |
461
+ |---------|--------|----------|------|----------|
462
+ | Ethereum | ETH | 1 | EVM | etherscan.io |
463
+ | Base | ETH | 8453 | EVM | basescan.org |
464
+ | BNB Smart Chain | BNB | 56 | EVM | bscscan.com |
465
+ | Arbitrum One | ETH | 42161 | EVM | arbiscan.io |
466
+ | Optimism | ETH | 10 | EVM | optimistic.etherscan.io |
467
+ | Solana | SOL | — | Non-EVM | solscan.io |
468
+ | Hyperliquid | HYPE | 999 | Non-EVM | — |
469
+
470
+ ### Known ERC-20 Tokens
471
+
472
+ Automatically detected in balance checks and available in the send token picker:
473
+
474
+ | Token | Ethereum | Base | Arbitrum | Optimism | BSC |
475
+ |-------|----------|------|----------|----------|-----|
476
+ | USDT | Yes | — | Yes | Yes | Yes |
477
+ | USDC | Yes | Yes | Yes | Yes | Yes |
478
+ | DAI | Yes | Yes | Yes | Yes | Yes |
479
+ | WBTC | Yes | — | Yes | Yes | Yes |
480
+ | LINK | Yes | — | — | — | — |
481
+ | UNI | Yes | — | — | — | — |
482
+ | WETH | — | Yes | — | — | — |
483
+
163
484
  ## Security
164
485
 
165
- - Wallet data is encrypted with AES-256-GCM (PBKDF2 600k iterations)
166
- - Private keys are held in memory only during the session
167
- - Vault is stored locally at `~/.zoa/vault.json`
168
- - No data is sent to any server unless you explicitly use API features
486
+ - **Encryption:** Wallet data is encrypted with AES-256-GCM using PBKDF2 (600k iterations) key derivation
487
+ - **Key isolation:** Private keys are held in memory only during the active session
488
+ - **Local storage:** Vault is stored locally at `~/.zoa/vault.json` — no cloud sync
489
+ - **No telemetry:** No data is sent to any server unless you explicitly use API features
490
+ - **API keys:** Scoped permissions (read/write/admin) with optional expiry and rate limits
491
+
492
+ ## Configuration
493
+
494
+ ZOA stores data in `~/.zoa/`:
495
+
496
+ | File | Description |
497
+ |------|-------------|
498
+ | `vault.json` | Encrypted wallet data |
499
+ | `config.json` | User preferences and settings |
500
+ | `wallets.json` | Wallet metadata (labels, colors, active wallet) |
501
+
502
+ ```bash
503
+ # View config
504
+ zoa config show
505
+
506
+ # Set default chain
507
+ zoa config set defaultChain base
508
+
509
+ # Set API URL
510
+ zoa config set apiUrl https://api.zoa.fun
511
+
512
+ # Set API key
513
+ zoa api set zoa_sk_your_key_here
514
+ ```
169
515
 
170
516
  ## License
171
517
 
package/dist/index.mjs CHANGED
@@ -36001,13 +36001,11 @@ var BaseEVMAdapter = class {
36001
36001
  if (!token || !result || result.status !== "success")
36002
36002
  continue;
36003
36003
  const balance = BigInt(result.result);
36004
- if (balance > 0n) {
36005
- balances.push({
36006
- token,
36007
- balance,
36008
- formatted: formatUnits(balance, token.decimals)
36009
- });
36010
- }
36004
+ balances.push({
36005
+ token,
36006
+ balance,
36007
+ formatted: formatUnits(balance, token.decimals)
36008
+ });
36011
36009
  }
36012
36010
  } catch {
36013
36011
  }
@@ -36072,17 +36070,35 @@ var BaseEVMAdapter = class {
36072
36070
  });
36073
36071
  }
36074
36072
  async signAndSendNativeTransfer(params) {
36073
+ this.initClient();
36075
36074
  const account = privateKeyToAccount(params.privateKey);
36075
+ const viemChain = this.getViemChain();
36076
+ const balance = await this.client.getBalance({ address: account.address });
36077
+ let sendValue = params.value;
36078
+ if (sendValue > 0n && sendValue >= balance) {
36079
+ try {
36080
+ const feeData = await this.client.estimateFeesPerGas();
36081
+ const maxFeePerGas = feeData.maxFeePerGas ?? 0n;
36082
+ const gasCost = 21000n * maxFeePerGas;
36083
+ if (balance <= gasCost) {
36084
+ throw new Error(`Insufficient balance. You have ${formatEther(balance)} ${this.config.nativeCurrency.symbol} which is not enough to cover gas fees.`);
36085
+ }
36086
+ sendValue = balance - gasCost;
36087
+ } catch (err3) {
36088
+ if (err3 instanceof Error && err3.message.includes("Insufficient balance"))
36089
+ throw err3;
36090
+ }
36091
+ }
36076
36092
  const walletClient = createWalletClient({
36077
36093
  account,
36078
- chain: this.getViemChain(),
36094
+ chain: viemChain,
36079
36095
  transport: http(this.config.rpcUrls[0])
36080
36096
  });
36081
36097
  const hash3 = await walletClient.sendTransaction({
36082
36098
  account,
36083
36099
  to: params.to,
36084
- value: params.value,
36085
- chain: this.getViemChain()
36100
+ value: sendValue,
36101
+ chain: viemChain
36086
36102
  });
36087
36103
  return { hash: hash3 };
36088
36104
  }
@@ -47984,12 +48000,13 @@ var SolanaAdapter = class {
47984
48000
  const secretBytes = Uint8Array.from((params.privateKey.startsWith("0x") ? params.privateKey.slice(2) : params.privateKey).match(/.{1,2}/g).map((byte) => Number.parseInt(byte, 16)));
47985
48001
  const keypair = Keypair.fromSecretKey(secretBytes);
47986
48002
  const balance = await this.connection.getBalance(keypair.publicKey);
47987
- const lamports = Number(params.value);
48003
+ let lamports = Number(params.value);
47988
48004
  const estimatedFee = 5e3;
47989
- if (balance < lamports + estimatedFee) {
47990
- const balSOL = (balance / LAMPORTS_PER_SOL).toFixed(6);
47991
- const needed = ((lamports + estimatedFee) / LAMPORTS_PER_SOL).toFixed(6);
47992
- throw new Error(`Insufficient SOL balance. You have ${balSOL} SOL but need ${needed} SOL (${(lamports / LAMPORTS_PER_SOL).toFixed(6)} SOL + ~0.000005 SOL tx fee).`);
48005
+ if (balance < estimatedFee + 1) {
48006
+ throw new Error(`Insufficient SOL balance. You have ${(balance / LAMPORTS_PER_SOL).toFixed(6)} SOL which is not enough to cover the transaction fee.`);
48007
+ }
48008
+ if (lamports + estimatedFee > balance) {
48009
+ lamports = balance - estimatedFee;
47993
48010
  }
47994
48011
  const transaction = new Transaction().add(SystemProgram.transfer({
47995
48012
  fromPubkey: keypair.publicKey,
@@ -49804,9 +49821,27 @@ function cleanErrorMessage(error) {
49804
49821
  return firstLine;
49805
49822
  return full.slice(0, 200);
49806
49823
  }
49824
+ function parseBatchString(batch) {
49825
+ return batch.split(",").map((entry) => {
49826
+ const parts = entry.trim().split(":");
49827
+ if (parts.length < 2) {
49828
+ throw new Error(`Invalid batch entry "${entry}". Format: address:amount[:token[:chain]]`);
49829
+ }
49830
+ return {
49831
+ to: parts[0],
49832
+ amount: parts[1],
49833
+ token: parts[2] || void 0,
49834
+ chain: parts[3] || void 0
49835
+ };
49836
+ });
49837
+ }
49807
49838
  function registerSendCommand(program2) {
49808
- program2.command("send").description("Send tokens to an address").option("--password <password>", "Wallet password (non-interactive)").option("--to <address>", "Recipient address").option("--amount <amount>", "Amount to send").option("--chain <chain>", "Chain to use (base, ethereum, solana, etc.)").option("--token <token>", "Token to send (symbol, e.g. USDT, USDC). Defaults to native.").option("--yes", "Skip confirmation prompt").action(async (opts) => {
49839
+ program2.command("send").description("Send tokens to an address").option("--password <password>", "Wallet password (non-interactive)").option("--to <address>", "Recipient address").option("--amount <amount>", "Amount to send").option("--chain <chain>", "Chain to use (base, ethereum, solana, etc.)").option("--token <token>", "Token to send (symbol, e.g. USDT, USDC). Defaults to native.").option("--max", "Send maximum available balance (auto-deducts gas)").option("--batch <transfers>", 'Batch send: comma-separated "address:amount[:token[:chain]]"').option("--yes", "Skip confirmation prompt").action(async (opts) => {
49809
49840
  try {
49841
+ if (opts.batch) {
49842
+ await handleBatchSend(opts);
49843
+ return;
49844
+ }
49810
49845
  let password;
49811
49846
  if (opts.password) {
49812
49847
  password = opts.password;
@@ -49885,10 +49920,11 @@ function registerSendCommand(program2) {
49885
49920
  } else if (tokenBalances.length === 1) {
49886
49921
  selectedToken = tokenBalances[0];
49887
49922
  } else {
49888
- const choices = tokenBalances.map((tb) => ({
49889
- name: `${tb.token.symbol.padEnd(8)} ${formatBalance(tb.formatted)} ${tb.token.name}`,
49890
- value: tb.token.address
49891
- }));
49923
+ const choices = tokenBalances.map((tb) => {
49924
+ const bal = formatBalance(tb.formatted);
49925
+ const label = tb.balance === 0n ? colors.muted(`${tb.token.symbol.padEnd(8)} 0.00 ${tb.token.name}`) : `${tb.token.symbol.padEnd(8)} ${bal.padEnd(10)} ${tb.token.name}`;
49926
+ return { name: label, value: tb.token.address };
49927
+ });
49892
49928
  const { tokenAddr } = await inquirer6.prompt([{
49893
49929
  type: "list",
49894
49930
  name: "tokenAddr",
@@ -49926,7 +49962,15 @@ function registerSendCommand(program2) {
49926
49962
  throw new Error(`Invalid EVM address: ${to}. Must be 0x followed by 40 hex characters.`);
49927
49963
  }
49928
49964
  let amount;
49929
- if (opts.amount) {
49965
+ if (opts.max) {
49966
+ amount = selectedToken.formatted;
49967
+ if (Number.parseFloat(amount) <= 0) {
49968
+ throw new Error(`No ${tokenSymbol} balance available to send.`);
49969
+ }
49970
+ if (!isJsonMode()) {
49971
+ console.log(colors.muted(` Using max balance: ${formatBalance(amount)} ${tokenSymbol}`));
49972
+ }
49973
+ } else if (opts.amount) {
49930
49974
  amount = opts.amount;
49931
49975
  } else {
49932
49976
  const { sendAmount } = await inquirer6.prompt([{
@@ -50074,6 +50118,163 @@ function registerSendCommand(program2) {
50074
50118
  }
50075
50119
  });
50076
50120
  }
50121
+ async function handleBatchSend(opts) {
50122
+ const transfers = parseBatchString(opts.batch);
50123
+ const defaultChain = opts.chain?.toLowerCase() || "base";
50124
+ for (const t of transfers) {
50125
+ const chain2 = t.chain || defaultChain;
50126
+ const chainId = CHAIN_MAP2[chain2];
50127
+ if (chainId === void 0)
50128
+ throw new Error(`Unsupported chain "${chain2}" in batch entry for ${t.to}`);
50129
+ const isSolana = chainId === ChainId.Solana;
50130
+ if (isSolana && !isValidSolanaAddress(t.to)) {
50131
+ throw new Error(`Invalid Solana address in batch: ${t.to}`);
50132
+ }
50133
+ if (!isSolana && !isValidEvmAddress(t.to)) {
50134
+ throw new Error(`Invalid EVM address in batch: ${t.to}`);
50135
+ }
50136
+ const num2 = Number.parseFloat(t.amount);
50137
+ if (Number.isNaN(num2) || num2 <= 0) {
50138
+ throw new Error(`Invalid amount "${t.amount}" in batch entry for ${t.to}`);
50139
+ }
50140
+ }
50141
+ let password;
50142
+ if (opts.password) {
50143
+ password = opts.password;
50144
+ } else {
50145
+ const { pwd } = await inquirer6.prompt([{
50146
+ type: "password",
50147
+ name: "pwd",
50148
+ message: "Enter your wallet password:",
50149
+ mask: "*"
50150
+ }]);
50151
+ password = pwd;
50152
+ }
50153
+ const spinner = createSpinner("Unlocking wallet...");
50154
+ if (!isJsonMode())
50155
+ spinner.start();
50156
+ let keyring2, account;
50157
+ try {
50158
+ ({ keyring: keyring2, account } = await loadActiveWallet(password));
50159
+ } catch (err3) {
50160
+ if (!isJsonMode())
50161
+ spinner.stop();
50162
+ throw err3;
50163
+ }
50164
+ if (!isJsonMode())
50165
+ spinner.stop();
50166
+ const results = [];
50167
+ if (!isJsonMode()) {
50168
+ console.log(colors.brandBold(" Batch Send"));
50169
+ console.log(colors.muted(` ${transfers.length} transfer(s) queued
50170
+ `));
50171
+ }
50172
+ for (let i = 0; i < transfers.length; i++) {
50173
+ const t = transfers[i];
50174
+ const chain2 = t.chain || defaultChain;
50175
+ const chainId = CHAIN_MAP2[chain2];
50176
+ const adapter = chainRegistry.get(chainId);
50177
+ const isSolana = chainId === ChainId.Solana;
50178
+ const from14 = isSolana ? account.solanaAddress : account.evmAddress;
50179
+ const transferLabel = `[${i + 1}/${transfers.length}] ${t.amount} ${t.token || adapter.config.nativeCurrency.symbol} \u2192 ${t.to.slice(0, 10)}...`;
50180
+ if (!isJsonMode()) {
50181
+ spinner.text = ` ${transferLabel}`;
50182
+ spinner.start();
50183
+ }
50184
+ try {
50185
+ let tokenBalances;
50186
+ try {
50187
+ tokenBalances = await adapter.getTokenBalances(from14);
50188
+ } catch {
50189
+ tokenBalances = [{
50190
+ token: {
50191
+ address: "native",
50192
+ symbol: adapter.config.nativeCurrency.symbol,
50193
+ name: adapter.config.nativeCurrency.name,
50194
+ decimals: adapter.config.nativeCurrency.decimals,
50195
+ chainId: adapter.config.chainId
50196
+ },
50197
+ balance: 0n,
50198
+ formatted: "0"
50199
+ }];
50200
+ }
50201
+ const tokenSymbol = t.token || adapter.config.nativeCurrency.symbol;
50202
+ const selectedToken = tokenBalances.find((tb) => tb.token.symbol.toLowerCase() === tokenSymbol.toLowerCase());
50203
+ if (!selectedToken)
50204
+ throw new Error(`Token ${tokenSymbol} not found on ${adapter.config.name}`);
50205
+ const isNativeTransfer = selectedToken.token.address === "native";
50206
+ const value = BigInt(Math.floor(Number.parseFloat(t.amount) * 10 ** selectedToken.token.decimals));
50207
+ if (value > selectedToken.balance) {
50208
+ throw new Error(`Insufficient ${tokenSymbol} balance. Have ${formatBalance(selectedToken.formatted)}, need ${t.amount}.`);
50209
+ }
50210
+ const chainFamily = isSolana ? "solana" : "evm";
50211
+ const privateKey = keyring2.exportPrivateKey(0, chainFamily);
50212
+ let hash3;
50213
+ if (isNativeTransfer) {
50214
+ if (!adapter.signAndSendNativeTransfer)
50215
+ throw new Error(`Native transfers not supported for ${adapter.config.name}`);
50216
+ const result = await adapter.signAndSendNativeTransfer({ privateKey, to: t.to, value });
50217
+ hash3 = result.hash;
50218
+ } else {
50219
+ if (!adapter.signAndSendTokenTransfer)
50220
+ throw new Error(`Token transfers not supported for ${adapter.config.name}`);
50221
+ const result = await adapter.signAndSendTokenTransfer({
50222
+ privateKey,
50223
+ tokenAddress: selectedToken.token.address,
50224
+ to: t.to,
50225
+ amount: value
50226
+ });
50227
+ hash3 = result.hash;
50228
+ }
50229
+ const explorerUrl = `${adapter.config.blockExplorerUrl}/tx/${hash3}`;
50230
+ if (!isJsonMode())
50231
+ spinner.stop();
50232
+ if (!isJsonMode()) {
50233
+ console.log(colors.success(` \u2713 ${transferLabel}`));
50234
+ console.log(colors.muted(` ${explorerUrl}
50235
+ `));
50236
+ }
50237
+ results.push({
50238
+ to: t.to,
50239
+ amount: t.amount,
50240
+ token: tokenSymbol,
50241
+ chain: adapter.config.name,
50242
+ status: "sent",
50243
+ hash: hash3,
50244
+ explorerUrl
50245
+ });
50246
+ } catch (err3) {
50247
+ if (!isJsonMode())
50248
+ spinner.stop();
50249
+ const errMsg = cleanErrorMessage(err3);
50250
+ if (!isJsonMode()) {
50251
+ console.log(colors.error(` \u2717 ${transferLabel}`));
50252
+ console.log(colors.muted(` ${errMsg}
50253
+ `));
50254
+ }
50255
+ results.push({
50256
+ to: t.to,
50257
+ amount: t.amount,
50258
+ token: t.token || chain2,
50259
+ chain: chain2,
50260
+ status: "failed",
50261
+ error: errMsg
50262
+ });
50263
+ }
50264
+ }
50265
+ const succeeded = results.filter((r) => r.status === "sent").length;
50266
+ const failed = results.filter((r) => r.status === "failed").length;
50267
+ output({ batch: true, results, summary: { total: results.length, succeeded, failed } }, () => {
50268
+ console.log();
50269
+ const summaryLines = [
50270
+ kvLine("Total", String(results.length)),
50271
+ kvLine("Succeeded", colors.success(String(succeeded))),
50272
+ kvLine("Failed", failed > 0 ? colors.error(String(failed)) : "0")
50273
+ ];
50274
+ console.log(successBox("Batch Complete", summaryLines));
50275
+ console.log();
50276
+ });
50277
+ }
50077
50278
 
50078
50279
  // dist/commands/wallet.js
50079
50280
  import chalk3 from "chalk";
@@ -50577,7 +50778,7 @@ var zoaGradient2 = gradient2([
50577
50778
  "#c77dff"
50578
50779
  ]);
50579
50780
  var subtleGradient = gradient2(["#6b7280", "#9ca3af", "#6b7280"]);
50580
- var VERSION = "0.3.3";
50781
+ var VERSION = "0.3.4";
50581
50782
  async function displayBanner() {
50582
50783
  const banner = figlet.textSync("ZOA-wallet", { font: "ANSI Shadow" });
50583
50784
  console.log();
@@ -50615,7 +50816,7 @@ program.hook("preAction", async (_thisCommand, actionCommand) => {
50615
50816
  }
50616
50817
  }
50617
50818
  });
50618
- program.name("zoa").description("ZOA Wallet \u2014 API-First Crypto Wallet for Power-traders, Developers, & AI Agents").version("0.3.3");
50819
+ program.name("zoa").description("ZOA Wallet \u2014 API-First Crypto Wallet for Power-traders, Developers, & AI Agents").version("0.3.4");
50619
50820
  registerWalletCommand(program);
50620
50821
  registerBalanceCommand(program);
50621
50822
  registerSendCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zoa-wallet",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "API-First Crypto Wallet CLI for Power-traders, Developers, & AI Agents. Manage multi-chain wallets from your terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",