@veil-cash/sdk 0.6.1 → 0.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veil-cash/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "SDK and CLI for interacting with Veil Cash privacy pools - keypair generation, deposits, and status checking",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  name: veil
3
- version: 0.6.0
3
+ version: 0.6.2
4
4
  description: >
5
5
  Veil CLI for private ETH and USDC transactions on Base. Use when the user wants
6
6
  to deposit, withdraw, or transfer assets privately, check private balances,
7
7
  manage Veil keypairs, register on-chain, manage deterministic subaccounts
8
- (forwarder deploy, sweep, recover), or build unsigned transaction payloads
9
- for an external signer (e.g. Bankr). All operations target Base (chain ID 8453).
8
+ (forwarder deploy, sweep, merge to main wallet, recover), or build unsigned
9
+ transaction payloads for an external signer (e.g. Bankr). All operations
10
+ target Base (chain ID 8453).
10
11
  author: veildotcash
11
12
  metadata:
12
13
  homepage: https://veil.cash
@@ -35,6 +36,7 @@ triggers:
35
36
  - pattern: withdraw privately
36
37
  - pattern: private transfer
37
38
  - pattern: subaccount
39
+ - pattern: subaccount merge
38
40
  - pattern: forwarder
39
41
  - pattern: stealth deposit
40
42
  ---
@@ -163,7 +165,7 @@ What do you want to do?
163
165
  |
164
166
  +-- Register deposit key on-chain → veil register [--unsigned]
165
167
  |
166
- +-- Deposit ETH or USDC → veil deposit <asset> <amount> [--unsigned]
168
+ +-- Deposit ETH or USDC → veil deposit <asset> <amount> [--unsigned --address 0x...]
167
169
  |
168
170
  +-- Check balances → veil balance [queue|private] [--pool eth|usdc]
169
171
  |
@@ -200,6 +202,7 @@ What do you want to do?
200
202
  | Subaccount address | `veil subaccount address --slot 0` |
201
203
  | Deploy forwarder | `veil subaccount deploy --slot 0` |
202
204
  | Sweep forwarder | `veil subaccount sweep --slot 0 --asset eth` |
205
+ | Merge subaccount to main | `veil subaccount merge --slot 0 --pool eth` |
203
206
  | Recover from forwarder | `veil subaccount recover --slot 0 --asset usdc --to 0xAddr --amount 25` |
204
207
 
205
208
  ---
@@ -327,7 +330,9 @@ Important:
327
330
  - If not yet registered, returns a normal `register` payload.
328
331
 
329
332
  Deposits treat the CLI amount as the **net** amount that lands in the pool.
330
- The `0.3%` protocol fee is calculated on-chain and added automatically.
333
+ Each address gets a configurable number of fee-free deposits per UTC day.
334
+ The CLI checks automatically — if free slots remain the fee is waived;
335
+ otherwise the `0.3%` protocol fee is calculated on-chain and added.
331
336
  After submission, deposits go through screening / queue processing before they
332
337
  are accepted into the private pool. This typically takes around `10-15 minutes`.
333
338
 
@@ -335,8 +340,8 @@ are accepted into the private pool. This typically takes around `10-15 minutes`.
335
340
  veil deposit ETH 0.1
336
341
  veil deposit USDC 100
337
342
  veil deposit ETH 0.1 --json
338
- veil deposit ETH 0.1 --unsigned
339
- veil deposit USDC 100 --unsigned
343
+ veil deposit ETH 0.1 --unsigned --address 0x...
344
+ SIGNER_ADDRESS=0x... veil deposit USDC 100 --unsigned
340
345
  ```
341
346
 
342
347
  Minimums:
@@ -428,11 +433,13 @@ Subaccounts are deterministic child slots derived from your main `VEIL_KEY`:
428
433
  `root key → slot → child key → child deposit key → forwarder`
429
434
 
430
435
  Base mainnet only. Slots are `0`–`2` (max 3 subaccounts). Deploy and sweep are
431
- relay-backed (no `WALLET_KEY` needed). Recovery submits a direct on-chain
432
- transaction and **requires `WALLET_KEY`** as a gas payer.
436
+ relay-backed (no `WALLET_KEY` needed). Merge transfers the subaccount's private
437
+ pool balance back to the main wallet via a ZK proof (relay-backed, no `WALLET_KEY`
438
+ needed). Recovery submits a direct on-chain transaction and **requires `WALLET_KEY`**
439
+ as a gas payer.
433
440
 
434
- Status reports the forwarder wallet balances and queue state only, not private
435
- pool attribution after queued funds are accepted.
441
+ Status reports the child slot's forwarder wallet balances, private pool
442
+ balances, and queue state.
436
443
 
437
444
  ### Derive and inspect
438
445
 
@@ -440,7 +447,7 @@ pool attribution after queued funds are accepted.
440
447
  veil subaccount derive --slot 0 # Full slot metadata
441
448
  veil subaccount derive --slot 0 --json
442
449
  veil subaccount address --slot 0 # Just the forwarder address
443
- veil subaccount status --slot 0 # Deployment, balances, queue state
450
+ veil subaccount status --slot 0 # Deployment, forwarder balances, private balances, queue state
444
451
  veil subaccount status --slot 0 --json
445
452
  ```
446
453
 
@@ -454,6 +461,18 @@ veil subaccount sweep --slot 0 --asset usdc # Sweep USDC into the pool
454
461
  veil subaccount sweep --slot 0 --asset eth --json
455
462
  ```
456
463
 
464
+ ### Merge subaccount to main wallet (relay-backed)
465
+
466
+ Merge transfers the subaccount's entire private pool balance back to the main
467
+ wallet. It builds a ZK proof transferring child UTXOs to the parent keypair and
468
+ submits via the relay. Only needs `VEIL_KEY`.
469
+
470
+ ```bash
471
+ veil subaccount merge --slot 0 --pool eth
472
+ veil subaccount merge --slot 0 --pool usdc
473
+ veil subaccount merge --slot 0 --pool eth --json
474
+ ```
475
+
457
476
  ### Recover (direct on-chain — requires WALLET_KEY)
458
477
 
459
478
  Recovery is for assets still sitting on the forwarder after refund or rejection.
@@ -470,6 +489,7 @@ Important:
470
489
  - `--asset` is `eth` or `usdc` (case-insensitive in the CLI)
471
490
  - `--slot` is `0`–`2`
472
491
  - Deploy and sweep only need `VEIL_KEY`
492
+ - Merge only needs `VEIL_KEY`
473
493
  - Recover needs both `VEIL_KEY` and `WALLET_KEY`
474
494
 
475
495
  ---
@@ -148,9 +148,11 @@ const priv = await getPrivateBalance({
148
148
  ```typescript
149
149
  import {
150
150
  deriveSubaccountSlot,
151
+ getSubaccountPrivateBalance,
151
152
  getSubaccountStatus,
152
153
  deploySubaccountForwarder,
153
154
  sweepSubaccountForwarder,
155
+ mergeSubaccount,
154
156
  buildSubaccountRecoveryTx,
155
157
  isSubaccountForwarderDeployed,
156
158
  MAX_SUBACCOUNT_SLOTS,
@@ -168,12 +170,20 @@ const deployed = await isSubaccountForwarderDeployed({
168
170
  forwarderAddress: slot.forwarderAddress,
169
171
  });
170
172
 
171
- // Full status (deployment, balances, queue state)
173
+ // Full status (deployment, forwarder balances, private balances, queue state)
172
174
  const status = await getSubaccountStatus({
173
175
  rootPrivateKey: '0xVEIL_KEY',
174
176
  slot: 0,
175
177
  });
176
- // status.deployed, status.balances.eth, status.balances.usdc, status.queues
178
+ // status.deployed, status.balances, status.privateBalances, status.queues
179
+
180
+ // Private pool balance for a single slot + pool
181
+ const privateBalance = await getSubaccountPrivateBalance({
182
+ rootPrivateKey: '0xVEIL_KEY',
183
+ slot: 0,
184
+ pool: 'eth',
185
+ });
186
+ // privateBalance.privateBalance, privateBalance.unspentCount, privateBalance.utxos
177
187
 
178
188
  // Deploy forwarder (relay-backed, no WALLET_KEY needed)
179
189
  const deployResult = await deploySubaccountForwarder({
@@ -188,6 +198,14 @@ const sweepResult = await sweepSubaccountForwarder({
188
198
  asset: 'eth', // 'eth' | 'usdc'
189
199
  });
190
200
 
201
+ // Merge subaccount's private balance back to main wallet (relay-backed)
202
+ const mergeResult = await mergeSubaccount({
203
+ rootPrivateKey: '0xVEIL_KEY',
204
+ slot: 0,
205
+ pool: 'eth', // 'eth' | 'usdc' (default: 'eth')
206
+ });
207
+ // mergeResult.success, mergeResult.transactionHash, mergeResult.amount, mergeResult.slot, mergeResult.pool
208
+
191
209
  // Build recovery transaction (for assets stuck on forwarder)
192
210
  const recovery = await buildSubaccountRecoveryTx({
193
211
  rootPrivateKey: '0xVEIL_KEY',
@@ -240,8 +258,8 @@ SIGNER_ADDRESS=0x... veil register --unsigned --force # Unsigned register/change
240
258
  veil register --unsigned --address 0x... # Unsigned register payload (explicit address)
241
259
  veil register --json # Register and output result as JSON
242
260
 
243
- veil deposit ETH 0.1 --unsigned # Unsigned ETH deposit payload
244
- veil deposit USDC 100 --unsigned # Unsigned USDC deposit payload(s)
261
+ veil deposit ETH 0.1 --unsigned --address 0x... # Unsigned ETH deposit payload
262
+ SIGNER_ADDRESS=0x... veil deposit USDC 100 --unsigned # Unsigned USDC deposit payload(s)
245
263
  veil deposit ETH 0.1 --json # Deposit and output result as JSON
246
264
 
247
265
  veil balance # All pool balances
@@ -256,12 +274,14 @@ veil balance private --json # Private balance as JSON
256
274
  veil subaccount derive --slot 0 # Derive slot metadata
257
275
  veil subaccount derive --slot 0 --json # Derive as JSON
258
276
  veil subaccount address --slot 0 # Print forwarder address
259
- veil subaccount status --slot 0 # Deployment, balances, queue state
277
+ veil subaccount status --slot 0 # Deployment, forwarder balances, private balances, queue state
260
278
  veil subaccount status --slot 0 --json # Status as JSON
261
279
  veil subaccount deploy --slot 0 # Deploy forwarder (relay-backed)
262
280
  veil subaccount deploy --slot 0 --json # Deploy as JSON
263
281
  veil subaccount sweep --slot 0 --asset eth # Sweep ETH into pool (relay-backed)
264
282
  veil subaccount sweep --slot 0 --asset usdc --json # Sweep USDC as JSON
283
+ veil subaccount merge --slot 0 --pool eth # Merge subaccount balance to main wallet
284
+ veil subaccount merge --slot 0 --pool usdc --json # Merge USDC as JSON
265
285
  veil subaccount recover --slot 0 --asset usdc --to 0x... --amount 25 # Recover assets (needs WALLET_KEY)
266
286
  veil subaccount recover --slot 0 --asset eth --to 0x... --amount 0.05 --json
267
287
  ```
@@ -287,10 +307,10 @@ Common codes: `VEIL_KEY_MISSING`, `WALLET_KEY_MISSING`, `DEPOSIT_KEY_MISSING`,
287
307
 
288
308
  | Asset | Minimum (net) | Notes |
289
309
  |-------|--------------|-------|
290
- | ETH | 0.01 | Fee (0.3%) added automatically via on-chain `getDepositAmountWithFee` |
291
- | USDC | 10 | Fee (0.3%) added automatically via on-chain `getDepositAmountWithFee` |
310
+ | ETH | 0.01 | Fee (0.3%) added automatically, or waived if daily free deposits remain |
311
+ | USDC | 10 | Fee (0.3%) added automatically, or waived if daily free deposits remain |
292
312
 
293
- The CLI amount is the **net** amount that lands in the pool. The fee is calculated on-chain and added to the transaction automatically — users do not need to account for it.
313
+ The CLI amount is the **net** amount that lands in the pool. The CLI checks `getDailyFreeRemaining` on the queue contract — if the user has free slots left today the fee is skipped; otherwise the 0.3% fee is calculated on-chain and added to the transaction automatically.
294
314
 
295
315
  ---
296
316
 
package/src/abi.ts CHANGED
@@ -218,6 +218,14 @@ export const QUEUE_ABI = [
218
218
  stateMutability: 'view',
219
219
  type: 'function',
220
220
  },
221
+ // Get remaining daily free deposits for an address (V3+)
222
+ {
223
+ inputs: [{ name: '_depositor', type: 'address' }],
224
+ name: 'getDailyFreeRemaining',
225
+ outputs: [{ name: 'remaining', type: 'uint256' }],
226
+ stateMutability: 'view',
227
+ type: 'function',
228
+ },
221
229
  ] as const;
222
230
 
223
231
  /**
package/src/balance.ts CHANGED
@@ -127,6 +127,54 @@ export async function getQueueBalance(options: {
127
127
  };
128
128
  }
129
129
 
130
+ /**
131
+ * Get remaining daily free deposits for an address.
132
+ * Returns 0 if the queue contract has not been upgraded to V3 yet
133
+ * or if the daily free feature is disabled.
134
+ *
135
+ * @param options - Query options
136
+ * @param options.address - Depositor address to check
137
+ * @param options.pool - Pool identifier ('eth' or 'usdc', default: 'eth')
138
+ * @param options.rpcUrl - Optional RPC URL
139
+ * @returns Number of free deposits remaining today
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const remaining = await getDailyFreeRemaining({
144
+ * address: '0x...',
145
+ * pool: 'eth',
146
+ * });
147
+ * console.log(`Free deposits left today: ${remaining}`);
148
+ * ```
149
+ */
150
+ export async function getDailyFreeRemaining(options: {
151
+ address: `0x${string}`;
152
+ pool?: RelayPool;
153
+ rpcUrl?: string;
154
+ }): Promise<number> {
155
+ const { address, pool = 'eth', rpcUrl } = options;
156
+ const queueAddress = getQueueAddress(pool);
157
+
158
+ const publicClient = createPublicClient({
159
+ chain: base,
160
+ transport: http(rpcUrl),
161
+ });
162
+
163
+ try {
164
+ const remaining = await publicClient.readContract({
165
+ address: queueAddress,
166
+ abi: QUEUE_ABI,
167
+ functionName: 'getDailyFreeRemaining',
168
+ args: [address],
169
+ }) as bigint;
170
+
171
+ return Number(remaining);
172
+ } catch {
173
+ // V2 contracts don't have this function — treat as 0 remaining
174
+ return 0;
175
+ }
176
+ }
177
+
130
178
  /**
131
179
  * Get private balance from the Pool contract
132
180
  * Decrypts all encrypted outputs, calculates nullifiers, and checks spent status
@@ -4,8 +4,9 @@
4
4
 
5
5
  import { Command } from 'commander';
6
6
  import { buildDepositETHTx, buildDepositUSDCTx, buildApproveUSDCTx } from '../../deposit.js';
7
+ import { getDailyFreeRemaining } from '../../balance.js';
7
8
  import { sendTransaction, getAddress, getBalance } from '../wallet.js';
8
- import { getConfig } from '../config.js';
9
+ import { getConfig, resolveAddress } from '../config.js';
9
10
  import { createPublicClient, http, parseEther, parseUnits, formatEther, formatUnits } from 'viem';
10
11
  import { base } from 'viem/chains';
11
12
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
@@ -13,6 +14,7 @@ import { clearProgress, createProgressReporter, printFields, printHeader, printJ
13
14
  import { POOL_CONFIG, getAddresses } from '../../addresses.js';
14
15
  import { ENTRY_ABI, ERC20_ABI } from '../../abi.js';
15
16
  import type { TransactionData } from '../../types.js';
17
+ import type { WalletConfig } from '../wallet.js';
16
18
 
17
19
  const MINIMUM_NET: Record<string, number> = {
18
20
  ETH: 0.01,
@@ -20,13 +22,22 @@ const MINIMUM_NET: Record<string, number> = {
20
22
  };
21
23
 
22
24
  /**
23
- * Query the Entry contract for the exact gross amount (net + fee).
24
- * This matches the contract's own fee math and avoids rounding mismatches.
25
+ * Compute the gross amount and fee for a deposit.
26
+ * Checks daily free deposit availability first if the user has
27
+ * free slots remaining the fee is waived and gross === net.
25
28
  */
26
29
  async function getGrossAmount(
27
30
  netWei: bigint,
31
+ depositor: `0x${string}`,
32
+ pool: 'eth' | 'usdc',
28
33
  rpcUrl: string | undefined,
29
- ): Promise<{ grossWei: bigint; feeWei: bigint }> {
34
+ ): Promise<{ grossWei: bigint; feeWei: bigint; dailyFreeUsed: boolean; dailyFreeRemaining: number }> {
35
+ const freeRemaining = await getDailyFreeRemaining({ address: depositor, pool, rpcUrl });
36
+
37
+ if (freeRemaining > 0) {
38
+ return { grossWei: netWei, feeWei: 0n, dailyFreeUsed: true, dailyFreeRemaining: freeRemaining - 1 };
39
+ }
40
+
30
41
  const publicClient = createPublicClient({
31
42
  chain: base,
32
43
  transport: http(rpcUrl),
@@ -39,7 +50,7 @@ async function getGrossAmount(
39
50
  args: [netWei],
40
51
  }) as bigint;
41
52
 
42
- return { grossWei, feeWei: grossWei - netWei };
53
+ return { grossWei, feeWei: grossWei - netWei, dailyFreeUsed: false, dailyFreeRemaining: 0 };
43
54
  }
44
55
 
45
56
  const SUPPORTED_ASSETS = ['ETH', 'USDC'];
@@ -49,16 +60,19 @@ export function createDepositCommand(): Command {
49
60
  .description('Deposit ETH or USDC into Veil')
50
61
  .argument('<asset>', 'Asset to deposit (ETH or USDC)')
51
62
  .argument('<amount>', 'Amount to deposit — this is what arrives in your Veil balance')
63
+ .option('--address <address>', 'Signer address (required in --unsigned mode unless SIGNER_ADDRESS or WALLET_KEY is set)')
52
64
  .option('--unsigned', 'Output unsigned transaction payload instead of sending')
53
65
  .option('--json', 'Output as JSON')
54
66
  .addHelpText('after', `
55
67
  The amount you specify is the net amount that lands in your Veil balance.
56
- The 0.3% protocol fee is automatically added on top.
68
+ A 0.3% protocol fee is normally added on top, but each address gets
69
+ free daily deposits (fee waived). The CLI checks automatically.
57
70
 
58
71
  Examples:
59
- veil deposit ETH 0.1 # deposits 0.1 ETH (sends ~0.1003 ETH)
60
- veil deposit USDC 100 # deposits 100 USDC (sends ~100.30 USDC)
61
- veil deposit ETH 0.1 --unsigned
72
+ veil deposit ETH 0.1 # deposits 0.1 ETH (free or ~0.1003 ETH)
73
+ veil deposit USDC 100 # deposits 100 USDC (free or ~100.30 USDC)
74
+ veil deposit ETH 0.1 --unsigned --address 0x...
75
+ SIGNER_ADDRESS=0x... veil deposit ETH 0.1 --unsigned
62
76
  veil deposit ETH 0.1 --json
63
77
  `)
64
78
  .action(async (asset: string, amount: string, options) => {
@@ -80,15 +94,41 @@ Examples:
80
94
  }
81
95
 
82
96
  const rpcUrl = process.env.RPC_URL;
83
- const poolConfig = POOL_CONFIG[assetUpper.toLowerCase() as 'eth' | 'usdc'];
97
+ const pool = assetUpper.toLowerCase() as 'eth' | 'usdc';
98
+ const poolConfig = POOL_CONFIG[pool];
84
99
  const netWei = assetUpper === 'ETH'
85
100
  ? parseEther(amount)
86
101
  : parseUnits(amount, poolConfig.decimals);
87
102
 
88
103
  const progress = createProgressReporter();
89
- progress('Calculating fee...');
90
104
 
91
- const { grossWei, feeWei } = await getGrossAmount(netWei, rpcUrl);
105
+ let config: WalletConfig | null = null;
106
+ let address: `0x${string}`;
107
+ let feeRpcUrl = rpcUrl;
108
+
109
+ if (options.unsigned) {
110
+ const resolved = resolveAddress({ address: options.address }, { required: true });
111
+ if (!resolved) {
112
+ throw new CLIError(
113
+ ErrorCode.WALLET_KEY_MISSING,
114
+ 'Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env.',
115
+ );
116
+ }
117
+ address = resolved.address;
118
+ } else {
119
+ config = getConfig(options);
120
+ address = getAddress(config.privateKey);
121
+ feeRpcUrl = config.rpcUrl;
122
+ }
123
+
124
+ progress('Checking deposit fee...');
125
+
126
+ const { grossWei, feeWei, dailyFreeUsed, dailyFreeRemaining } = await getGrossAmount(
127
+ netWei,
128
+ address,
129
+ pool,
130
+ feeRpcUrl,
131
+ );
92
132
  const grossStr = assetUpper === 'ETH'
93
133
  ? formatEther(grossWei)
94
134
  : formatUnits(grossWei, poolConfig.decimals);
@@ -140,8 +180,9 @@ Examples:
140
180
  return;
141
181
  }
142
182
 
143
- const config = getConfig(options);
144
- const address = getAddress(config.privateKey);
183
+ if (!config) {
184
+ throw new CLIError(ErrorCode.WALLET_KEY_MISSING, 'WALLET_KEY env var required. Set it before running this command.');
185
+ }
145
186
 
146
187
  if (assetUpper === 'ETH') {
147
188
  progress('Checking balance...');
@@ -203,6 +244,7 @@ Examples:
203
244
  asset: assetUpper,
204
245
  amount,
205
246
  fee: feeStr,
247
+ dailyFreeUsed,
206
248
  totalSent: grossStr,
207
249
  blockNumber: result.receipt.blockNumber.toString(),
208
250
  };
@@ -212,11 +254,15 @@ Examples:
212
254
  return;
213
255
  }
214
256
 
257
+ const feeLabel = dailyFreeUsed
258
+ ? `0 ${assetUpper} (free — ${dailyFreeRemaining} remaining today)`
259
+ : `${feeStr} ${assetUpper} (0.3%)`;
260
+
215
261
  printHeader('Deposit Submitted');
216
262
  printFields([
217
263
  { label: 'Asset', value: assetUpper },
218
264
  { label: 'Amount', value: `${amount} ${assetUpper}` },
219
- { label: 'Fee', value: `${feeStr} ${assetUpper} (0.3%)` },
265
+ { label: 'Fee', value: feeLabel },
220
266
  { label: 'Total sent', value: `${grossStr} ${assetUpper}` },
221
267
  { label: 'From', value: address },
222
268
  { label: 'Transaction', value: txUrl(result.hash) },
@@ -7,13 +7,14 @@ import {
7
7
  getSubaccountStatus,
8
8
  isSubaccountForwarderDeployed,
9
9
  MAX_SUBACCOUNT_SLOTS,
10
+ mergeSubaccount,
10
11
  sweepSubaccountForwarder,
11
12
  } from '../../subaccount.js';
12
13
  import { getConfig } from '../config.js';
13
14
  import { CLIError, ErrorCode, handleCLIError } from '../errors.js';
14
15
  import { printFields, printHeader, printJson, printLine, printList, printSection, txUrl } from '../output.js';
15
16
  import { sendTransaction } from '../wallet.js';
16
- import type { SubaccountAsset } from '../../types.js';
17
+ import type { SubaccountAsset, RelayPool } from '../../types.js';
17
18
 
18
19
  function parseSlotValue(raw: string): number {
19
20
  const normalized = raw.trim();
@@ -51,6 +52,14 @@ function parseAsset(raw: string): SubaccountAsset {
51
52
  return asset;
52
53
  }
53
54
 
55
+ function parsePool(raw: string): RelayPool {
56
+ const pool = raw.toLowerCase();
57
+ if (pool !== 'eth' && pool !== 'usdc') {
58
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported pool: ${raw}. Supported: eth, usdc`);
59
+ }
60
+ return pool;
61
+ }
62
+
54
63
  function printQueueHuman(
55
64
  title: string,
56
65
  queue: {
@@ -81,6 +90,7 @@ Examples:
81
90
  veil subaccount status --slot 0
82
91
  veil subaccount deploy --slot 0
83
92
  veil subaccount sweep --slot 0 --asset eth
93
+ veil subaccount merge --slot 0 --pool eth
84
94
  veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
85
95
  veil subaccount address --slot 0
86
96
  `);
@@ -130,7 +140,7 @@ Examples:
130
140
 
131
141
  subaccount
132
142
  .command('status')
133
- .description('Show subaccount deployment, balances, and queue state')
143
+ .description('Show subaccount deployment, forwarder balances, private balances, and queue state')
134
144
  .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
135
145
  .option('--json', 'Output as JSON')
136
146
  .action(async (options) => {
@@ -162,6 +172,18 @@ Examples:
162
172
  { label: 'USDC', value: `${status.balances.usdc.balance} USDC` },
163
173
  ]);
164
174
 
175
+ printSection('Private Pool Balances');
176
+ printFields([
177
+ {
178
+ label: 'ETH',
179
+ value: `${status.privateBalances.eth.privateBalance} ETH (${status.privateBalances.eth.unspentCount} unspent / ${status.privateBalances.eth.spentCount} spent / ${status.privateBalances.eth.utxoCount} total UTXOs)`,
180
+ },
181
+ {
182
+ label: 'USDC',
183
+ value: `${status.privateBalances.usdc.privateBalance} USDC (${status.privateBalances.usdc.unspentCount} unspent / ${status.privateBalances.usdc.spentCount} spent / ${status.privateBalances.usdc.utxoCount} total UTXOs)`,
184
+ },
185
+ ]);
186
+
165
187
  printQueueHuman('ETH Queue', status.queues.eth);
166
188
  printQueueHuman('USDC Queue', status.queues.usdc);
167
189
  printLine();
@@ -255,6 +277,64 @@ Examples:
255
277
  }
256
278
  });
257
279
 
280
+ subaccount
281
+ .command('merge')
282
+ .description('Merge a subaccount\'s private pool balance back to the main wallet')
283
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
284
+ .option('--pool <pool>', 'Pool to merge (eth or usdc)', parsePool, 'eth' as RelayPool)
285
+ .option('--json', 'Output as JSON')
286
+ .action(async (options) => {
287
+ try {
288
+ const rootPrivateKey = getRequiredVeilKey();
289
+ const result = await mergeSubaccount({
290
+ rootPrivateKey,
291
+ slot: options.slot,
292
+ pool: options.pool,
293
+ rpcUrl: process.env.RPC_URL,
294
+ relayUrl: process.env.RELAY_URL,
295
+ onProgress: options.json
296
+ ? undefined
297
+ : (stage, detail) => {
298
+ const msg = detail ? `${stage} ${detail}` : stage;
299
+ process.stderr.write(`\r\x1b[K${msg}`);
300
+ },
301
+ });
302
+
303
+ if (!options.json) {
304
+ process.stderr.write('\r\x1b[K');
305
+ }
306
+
307
+ const output = {
308
+ success: result.success,
309
+ slot: result.slot,
310
+ pool: result.pool,
311
+ amount: result.amount,
312
+ transactionHash: result.transactionHash,
313
+ blockNumber: result.blockNumber,
314
+ };
315
+
316
+ if (options.json) {
317
+ printJson(output);
318
+ return;
319
+ }
320
+
321
+ printHeader('Subaccount Merge Submitted');
322
+ printFields([
323
+ { label: 'Slot', value: result.slot },
324
+ { label: 'Pool', value: result.pool.toUpperCase() },
325
+ { label: 'Amount', value: result.amount },
326
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
327
+ { label: 'Block', value: result.blockNumber },
328
+ ]);
329
+ printLine();
330
+ } catch (error) {
331
+ if (!options.json) {
332
+ process.stderr.write('\r\x1b[K');
333
+ }
334
+ handleCLIError(error);
335
+ }
336
+ });
337
+
258
338
  subaccount
259
339
  .command('recover')
260
340
  .description('Recover assets sitting on the subaccount forwarder with a direct withdraw transaction')
package/src/cli/index.ts CHANGED
@@ -38,7 +38,7 @@ const program = new Command();
38
38
  program
39
39
  .name('veil')
40
40
  .description('CLI for Veil Cash privacy pools on Base')
41
- .version('0.6.1')
41
+ .version('0.6.2')
42
42
  .addHelpText('after', `
43
43
  Getting started:
44
44
  veil init
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export {
53
53
  export {
54
54
  getQueueBalance,
55
55
  getPrivateBalance,
56
+ getDailyFreeRemaining,
56
57
  } from './balance.js';
57
58
  export type { ProgressCallback } from './balance.js';
58
59
 
@@ -139,12 +140,14 @@ export {
139
140
  isSubaccountForwarderDeployed,
140
141
  deploySubaccountForwarder,
141
142
  sweepSubaccountForwarder,
143
+ getSubaccountPrivateBalance,
142
144
  getSubaccountStatus,
143
145
  buildSubaccountWithdrawTypedData,
144
146
  signSubaccountWithdraw,
145
147
  isSubaccountWithdrawNonceUsed,
146
148
  findNextSubaccountWithdrawNonce,
147
149
  buildSubaccountRecoveryTx,
150
+ mergeSubaccount,
148
151
  } from './subaccount.js';
149
152
 
150
153
  // Utilities
@@ -197,8 +200,12 @@ export type {
197
200
  SubaccountRelayResult,
198
201
  SubaccountAssetBalance,
199
202
  SubaccountBalances,
203
+ SubaccountPrivateBalanceStatus,
204
+ SubaccountPrivateBalances,
200
205
  SubaccountQueueStatus,
201
206
  SubaccountStatusResult,
202
207
  SubaccountWithdrawTypedData,
203
208
  SubaccountRecoveryResult,
209
+ SubaccountMergeOptions,
210
+ SubaccountMergeResult,
204
211
  } from './types.js';