@veil-cash/sdk 0.4.0 → 0.6.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.
@@ -6,9 +6,13 @@ import { Command } from 'commander';
6
6
  import { getQueueBalance, getPrivateBalance } from '../../balance.js';
7
7
  import { POOL_CONFIG } from '../../addresses.js';
8
8
  import { Keypair } from '../../keypair.js';
9
- import { getAddress } from '../wallet.js';
9
+ import { getWalletBalances } from '../wallet.js';
10
+ import { resolveAddress } from '../config.js';
10
11
  import { formatUnits } from 'viem';
11
12
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
13
+ import { clearProgress, createProgressReporter, maskValue, printFields, printHeader, printJson, printLine, printList, printSection } from '../output.js';
14
+ import { createPrivateBalanceCommand } from './private-balance.js';
15
+ import { createQueueBalanceCommand } from './queue-balance.js';
12
16
  import type { RelayPool } from '../../types.js';
13
17
 
14
18
  const SUPPORTED_POOLS: RelayPool[] = ['eth', 'usdc'];
@@ -89,11 +93,15 @@ export function createBalanceCommand(): Command {
89
93
  const balance = new Command('balance')
90
94
  .description('Show queue and private balances (all pools by default)')
91
95
  .option('--pool <pool>', 'Pool to check (eth, usdc, or all)', 'all')
92
- .option('--wallet-key <key>', 'Ethereum wallet key (or set WALLET_KEY env)')
93
- .option('--address <address>', 'Address to check (or derived from wallet key)')
94
- .option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
95
- .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
96
- .option('--quiet', 'Suppress progress output')
96
+ .option('--address <address>', 'Address to check (or derived from WALLET_KEY / SIGNER_ADDRESS)')
97
+ .option('--json', 'Output as JSON')
98
+ .addHelpText('after', `
99
+ Examples:
100
+ veil balance
101
+ veil balance --pool eth
102
+ veil balance queue --pool usdc
103
+ veil balance --json
104
+ `)
97
105
  .action(async (options) => {
98
106
  try {
99
107
  const poolArg = (options.pool || 'all').toLowerCase();
@@ -106,73 +114,153 @@ export function createBalanceCommand(): Command {
106
114
  const poolsToQuery: RelayPool[] = poolArg === 'all' ? [...SUPPORTED_POOLS] : [poolArg as RelayPool];
107
115
 
108
116
  // Get address
109
- let address: `0x${string}`;
110
- if (options.address) {
111
- address = options.address as `0x${string}`;
112
- } else {
113
- const walletKey = options.walletKey || process.env.WALLET_KEY;
114
- if (!walletKey) {
115
- throw new CLIError(ErrorCode.WALLET_KEY_MISSING, 'Must provide --address or --wallet-key (or set WALLET_KEY env)');
116
- }
117
- address = getAddress(walletKey as `0x${string}`);
117
+ const resolvedAddress = resolveAddress({ address: options.address }, { required: true });
118
+ if (!resolvedAddress) {
119
+ throw new CLIError(
120
+ ErrorCode.WALLET_KEY_MISSING,
121
+ 'Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env.',
122
+ );
118
123
  }
124
+ const address = resolvedAddress.address;
119
125
 
120
126
  // Get keypair for private balance
121
- const veilKey = options.veilKey || process.env.VEIL_KEY;
127
+ const veilKey = process.env.VEIL_KEY;
122
128
  const keypair = veilKey ? new Keypair(veilKey) : null;
123
129
 
124
- const rpcUrl = options.rpcUrl || process.env.RPC_URL;
130
+ const rpcUrl = process.env.RPC_URL;
125
131
 
126
- // Progress callback
127
- const onProgress = options.quiet
128
- ? undefined
129
- : (stage: string, detail?: string) => {
130
- const msg = detail ? `${stage}: ${detail}` : stage;
131
- process.stderr.write(`\r\x1b[K${msg}`);
132
- };
132
+ const onProgress = createProgressReporter();
133
133
 
134
134
  // Get deposit key if available
135
135
  const depositKey = process.env.DEPOSIT_KEY || (keypair ? keypair.depositKey() : null);
136
136
 
137
137
  // Single pool mode -- flat output (backwards compatible)
138
138
  if (poolsToQuery.length === 1) {
139
- const poolResult = await fetchPoolBalance(poolsToQuery[0], address, keypair, rpcUrl, onProgress);
139
+ const [poolResult, walletBalances] = await Promise.all([
140
+ fetchPoolBalance(poolsToQuery[0], address, keypair, rpcUrl, onProgress),
141
+ getWalletBalances(address, rpcUrl),
142
+ ]);
140
143
 
141
- // Clear progress line
142
- if (!options.quiet) process.stderr.write('\r\x1b[K');
144
+ clearProgress();
143
145
 
144
146
  const output = {
145
147
  address,
146
148
  depositKey: depositKey || null,
149
+ wallet: walletBalances,
147
150
  ...poolResult,
148
151
  };
149
152
 
150
- console.log(JSON.stringify(output, null, 2));
153
+ if (options.json) {
154
+ printJson(output);
155
+ return;
156
+ }
157
+
158
+ printCombinedBalanceHuman(output);
151
159
  return;
152
160
  }
153
161
 
154
162
  // All pools mode -- nested output
155
- const pools: Record<string, unknown>[] = [];
156
- for (const pool of poolsToQuery) {
157
- const poolResult = await fetchPoolBalance(pool, address, keypair, rpcUrl, onProgress);
158
- pools.push(poolResult);
159
- }
163
+ const poolPromises = poolsToQuery.map(pool =>
164
+ fetchPoolBalance(pool, address, keypair, rpcUrl, onProgress),
165
+ );
166
+ const [walletBalances, ...poolResults] = await Promise.all([
167
+ getWalletBalances(address, rpcUrl),
168
+ ...poolPromises,
169
+ ]);
160
170
 
161
- // Clear progress line
162
- if (!options.quiet) process.stderr.write('\r\x1b[K');
171
+ clearProgress();
163
172
 
164
173
  const output = {
165
174
  address,
166
175
  depositKey: depositKey || null,
167
- pools,
176
+ wallet: walletBalances,
177
+ pools: poolResults,
168
178
  };
169
179
 
170
- console.log(JSON.stringify(output, null, 2));
180
+ if (options.json) {
181
+ printJson(output);
182
+ return;
183
+ }
184
+
185
+ printMultiPoolBalanceHuman(output);
171
186
  } catch (error) {
172
- process.stderr.write('\r\x1b[K');
187
+ clearProgress();
173
188
  handleCLIError(error);
174
189
  }
175
190
  });
176
191
 
192
+ balance.addCommand(createQueueBalanceCommand('queue'));
193
+ balance.addCommand(createPrivateBalanceCommand('private'));
194
+
177
195
  return balance;
178
196
  }
197
+
198
+ function printPoolHuman(output: Record<string, unknown>): void {
199
+ const symbol = output.symbol as string;
200
+
201
+ printSection(`${output.pool}`);
202
+ printFields([
203
+ { label: 'Total', value: `${output.totalBalance} ${symbol}` },
204
+ ]);
205
+
206
+ const privateData = output.private as { balance?: string | null; balanceWei?: string; utxoCount?: number; note?: string; utxos?: Array<{ index: number; amount: string }> };
207
+ if (privateData.note) {
208
+ printFields([
209
+ { label: 'Private', value: privateData.note },
210
+ ]);
211
+ } else {
212
+ printFields([
213
+ { label: 'Private', value: `${privateData.balance} ${symbol}` },
214
+ ]);
215
+ }
216
+
217
+ const queueData = output.queue as { balance: string; balanceWei: string; count: number; deposits: Array<{ nonce: string | number; amount: string; status: string }> };
218
+ printFields([
219
+ { label: 'Queue', value: `${queueData.balance} ${symbol}` },
220
+ { label: 'Pending', value: queueData.count },
221
+ ]);
222
+ if (queueData.deposits.length > 0) {
223
+ printList(
224
+ queueData.deposits.map((d) => `nonce ${d.nonce}: ${d.amount} (${d.status})`)
225
+ );
226
+ }
227
+ }
228
+
229
+ function printCombinedBalanceHuman(output: Record<string, unknown>): void {
230
+ const wallet = output.wallet as { eth: string; usdc: string };
231
+
232
+ printHeader(`${output.pool} Balance`);
233
+ printFields([
234
+ { label: 'Address', value: output.address },
235
+ { label: 'Deposit key', value: typeof output.depositKey === 'string' ? maskValue(output.depositKey) : 'not set' },
236
+ ]);
237
+
238
+ printSection('Wallet (public)');
239
+ printFields([
240
+ { label: 'ETH', value: `${wallet.eth} ETH` },
241
+ { label: 'USDC', value: `${wallet.usdc} USDC` },
242
+ ]);
243
+
244
+ printPoolHuman(output);
245
+ printLine();
246
+ }
247
+
248
+ function printMultiPoolBalanceHuman(output: { address: string; depositKey: string | null; wallet: { eth: string; usdc: string }; pools: Record<string, unknown>[] }): void {
249
+ printHeader('Balances');
250
+ printFields([
251
+ { label: 'Address', value: output.address },
252
+ { label: 'Deposit key', value: output.depositKey ? maskValue(output.depositKey) : 'not set' },
253
+ ]);
254
+
255
+ printSection('Wallet (public)');
256
+ printFields([
257
+ { label: 'ETH', value: `${output.wallet.eth} ETH` },
258
+ { label: 'USDC', value: `${output.wallet.usdc} USDC` },
259
+ ]);
260
+
261
+ for (const poolResult of output.pools) {
262
+ printPoolHuman(poolResult);
263
+ }
264
+
265
+ printLine();
266
+ }
@@ -6,88 +6,118 @@ import { Command } from 'commander';
6
6
  import { buildDepositETHTx, buildDepositUSDCTx, buildApproveUSDCTx } from '../../deposit.js';
7
7
  import { sendTransaction, getAddress, getBalance } from '../wallet.js';
8
8
  import { getConfig } from '../config.js';
9
- import { parseEther, formatEther } from 'viem';
9
+ import { createPublicClient, http, parseEther, parseUnits, formatEther, formatUnits } from 'viem';
10
+ import { base } from 'viem/chains';
10
11
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
12
+ import { clearProgress, createProgressReporter, printFields, printHeader, printJson, printLine, txUrl } from '../output.js';
13
+ import { POOL_CONFIG, getAddresses } from '../../addresses.js';
14
+ import { ENTRY_ABI, ERC20_ABI } from '../../abi.js';
11
15
  import type { TransactionData } from '../../types.js';
12
16
 
13
- // Minimum deposits per asset (net after 0.3% fee)
14
- const DEPOSIT_FEE_PERCENT = 0.3;
15
- const MINIMUM_DEPOSITS: Record<string, number> = {
17
+ const MINIMUM_NET: Record<string, number> = {
16
18
  ETH: 0.01,
17
19
  USDC: 10,
18
20
  };
19
21
 
20
- function getMinimumWithFee(asset: string): number {
21
- const min = MINIMUM_DEPOSITS[asset] || 0;
22
- return min / (1 - DEPOSIT_FEE_PERCENT / 100);
22
+ /**
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
+ */
26
+ async function getGrossAmount(
27
+ netWei: bigint,
28
+ rpcUrl: string | undefined,
29
+ ): Promise<{ grossWei: bigint; feeWei: bigint }> {
30
+ const publicClient = createPublicClient({
31
+ chain: base,
32
+ transport: http(rpcUrl),
33
+ });
34
+
35
+ const grossWei = await publicClient.readContract({
36
+ address: getAddresses().entry,
37
+ abi: ENTRY_ABI,
38
+ functionName: 'getDepositAmountWithFee',
39
+ args: [netWei],
40
+ }) as bigint;
41
+
42
+ return { grossWei, feeWei: grossWei - netWei };
23
43
  }
24
44
 
25
45
  const SUPPORTED_ASSETS = ['ETH', 'USDC'];
26
46
 
27
- // Progress helper - writes to stderr so JSON output stays clean
28
- function progress(msg: string, quiet?: boolean) {
29
- if (!quiet) {
30
- process.stderr.write(`\r\x1b[K${msg}`);
31
- }
32
- }
33
-
34
47
  export function createDepositCommand(): Command {
35
48
  const deposit = new Command('deposit')
36
49
  .description('Deposit ETH or USDC into Veil')
37
50
  .argument('<asset>', 'Asset to deposit (ETH or USDC)')
38
- .argument('<amount>', 'Amount to deposit (e.g., 0.1)')
39
- .option('--deposit-key <key>', 'Your Veil deposit key (or set DEPOSIT_KEY env)')
40
- .option('--wallet-key <key>', 'Ethereum wallet key for signing (or set WALLET_KEY env)')
41
- .option('--rpc-url <url>', 'RPC URL (or set RPC_URL env)')
42
- .option('--unsigned', 'Output unsigned transaction payload (Bankr-compatible format)')
43
- .option('--quiet', 'Suppress progress output')
51
+ .argument('<amount>', 'Amount to deposit this is what arrives in your Veil balance')
52
+ .option('--unsigned', 'Output unsigned transaction payload instead of sending')
53
+ .option('--json', 'Output as JSON')
54
+ .addHelpText('after', `
55
+ 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.
57
+
58
+ 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
62
+ veil deposit ETH 0.1 --json
63
+ `)
44
64
  .action(async (asset: string, amount: string, options) => {
45
65
  try {
46
66
  const assetUpper = asset.toUpperCase();
47
67
 
48
- // Validate asset
49
68
  if (!SUPPORTED_ASSETS.includes(assetUpper)) {
50
69
  throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported asset: ${asset}. Supported: ${SUPPORTED_ASSETS.join(', ')}`);
51
70
  }
52
71
 
53
72
  const amountNum = parseFloat(amount);
54
- const minimumWithFee = getMinimumWithFee(assetUpper);
55
- const minimumNet = MINIMUM_DEPOSITS[assetUpper];
56
-
57
- // Check minimum deposit
58
- if (amountNum < minimumWithFee) {
73
+ const minimumNet = MINIMUM_NET[assetUpper];
74
+
75
+ if (amountNum < minimumNet) {
59
76
  throw new CLIError(
60
77
  ErrorCode.INVALID_AMOUNT,
61
- `Minimum deposit is ${minimumNet} ${assetUpper} (net). ` +
62
- `With ${DEPOSIT_FEE_PERCENT}% fee, send at least ${minimumWithFee.toFixed(assetUpper === 'ETH' ? 5 : 8)} ${assetUpper}.`
78
+ `Minimum deposit is ${minimumNet} ${assetUpper}.`
63
79
  );
64
80
  }
65
81
 
66
- // Get deposit key from option or env
67
- const depositKey = options.depositKey || process.env.DEPOSIT_KEY;
82
+ const rpcUrl = process.env.RPC_URL;
83
+ const poolConfig = POOL_CONFIG[assetUpper.toLowerCase() as 'eth' | 'usdc'];
84
+ const netWei = assetUpper === 'ETH'
85
+ ? parseEther(amount)
86
+ : parseUnits(amount, poolConfig.decimals);
87
+
88
+ const progress = createProgressReporter();
89
+ progress('Calculating fee...');
90
+
91
+ const { grossWei, feeWei } = await getGrossAmount(netWei, rpcUrl);
92
+ const grossStr = assetUpper === 'ETH'
93
+ ? formatEther(grossWei)
94
+ : formatUnits(grossWei, poolConfig.decimals);
95
+ const feeStr = assetUpper === 'ETH'
96
+ ? formatEther(feeWei)
97
+ : formatUnits(feeWei, poolConfig.decimals);
98
+
99
+ const depositKey = process.env.DEPOSIT_KEY;
68
100
  if (!depositKey) {
69
- throw new CLIError(ErrorCode.DEPOSIT_KEY_MISSING, 'Deposit key required. Use --deposit-key or set DEPOSIT_KEY in .env (run: veil init)');
101
+ throw new CLIError(ErrorCode.DEPOSIT_KEY_MISSING, 'DEPOSIT_KEY not set. Run "veil init" first.');
70
102
  }
71
103
 
72
- progress('Building transaction...', options.quiet);
104
+ progress('Building transaction...');
73
105
 
74
- // Build the deposit transaction
75
106
  let tx: TransactionData;
76
107
  let approveTx: TransactionData | null = null;
77
108
 
78
109
  if (assetUpper === 'USDC') {
79
- approveTx = buildApproveUSDCTx({ amount });
80
- tx = buildDepositUSDCTx({ depositKey, amount });
110
+ approveTx = buildApproveUSDCTx({ amount: grossStr });
111
+ tx = buildDepositUSDCTx({ depositKey, amount: grossStr });
81
112
  } else {
82
- tx = buildDepositETHTx({ depositKey, amount });
113
+ tx = buildDepositETHTx({ depositKey, amount: grossStr });
83
114
  }
84
115
 
85
- // Handle --unsigned mode (no wallet required, just build payload)
116
+ // --unsigned mode
86
117
  if (options.unsigned) {
87
- progress('', options.quiet); // Clear line
118
+ clearProgress();
88
119
  const payloads: Record<string, unknown>[] = [];
89
120
 
90
- // Include approval tx for ERC20 tokens
91
121
  if (approveTx) {
92
122
  payloads.push({
93
123
  step: 'approve',
@@ -106,52 +136,95 @@ export function createDepositCommand(): Command {
106
136
  chainId: 8453,
107
137
  });
108
138
 
109
- console.log(JSON.stringify(payloads.length === 1 ? payloads[0] : payloads, null, 2));
139
+ printJson(payloads.length === 1 ? payloads[0] : payloads);
110
140
  return;
111
141
  }
112
142
 
113
- // Regular mode: sign and send
114
143
  const config = getConfig(options);
115
144
  const address = getAddress(config.privateKey);
116
145
 
117
- // For ETH deposits, check ETH balance
118
146
  if (assetUpper === 'ETH') {
119
- progress('Checking balance...', options.quiet);
147
+ progress('Checking balance...');
120
148
  const balance = await getBalance(address, config.rpcUrl);
121
- const amountWei = parseEther(amount);
122
-
123
- if (balance < amountWei) {
124
- progress('', options.quiet);
125
- throw new CLIError(ErrorCode.INSUFFICIENT_BALANCE, `Insufficient ETH balance. Have: ${formatEther(balance)} ETH, Need: ${amount} ETH`);
149
+
150
+ if (balance < grossWei) {
151
+ clearProgress();
152
+ throw new CLIError(
153
+ ErrorCode.INSUFFICIENT_BALANCE,
154
+ `Insufficient ETH balance. Have: ${formatEther(balance)} ETH, Need: ${grossStr} ETH (${amount} + fee)`
155
+ );
126
156
  }
127
157
  }
128
158
 
129
- // Send approval transaction for ERC20 tokens
130
159
  if (approveTx) {
131
- progress(`Approving ${assetUpper}...`, options.quiet);
132
- await sendTransaction(config, approveTx);
160
+ progress(`Approving ${assetUpper}...`);
161
+ const approvalResult = await sendTransaction(config, approveTx);
162
+ if (assetUpper === 'USDC') {
163
+ const publicClient = createPublicClient({
164
+ chain: base,
165
+ transport: http(config.rpcUrl),
166
+ });
167
+ const addresses = getAddresses();
168
+ let allowance = await publicClient.readContract({
169
+ address: getAddresses().usdcToken,
170
+ abi: ERC20_ABI,
171
+ functionName: 'allowance',
172
+ args: [address, addresses.entry],
173
+ }) as bigint;
174
+ for (let confirmations = 2; allowance < grossWei && confirmations <= 3; confirmations++) {
175
+ await publicClient.waitForTransactionReceipt({
176
+ hash: approvalResult.hash,
177
+ confirmations,
178
+ });
179
+ allowance = await publicClient.readContract({
180
+ address: addresses.usdcToken,
181
+ abi: ERC20_ABI,
182
+ functionName: 'allowance',
183
+ args: [address, addresses.entry],
184
+ }) as bigint;
185
+ }
186
+ if (allowance < grossWei) {
187
+ throw new CLIError(
188
+ ErrorCode.CONTRACT_ERROR,
189
+ `USDC approval is not yet visible on RPC after confirmation. Allowance ${allowance.toString()} < required ${grossWei.toString()}.`
190
+ );
191
+ }
192
+ }
133
193
  }
134
194
 
135
- progress('Sending deposit transaction...', options.quiet);
136
-
137
- // Send the deposit transaction
195
+ progress('Sending deposit transaction...');
138
196
  const result = await sendTransaction(config, tx);
197
+ progress('Confirming...');
198
+ clearProgress();
139
199
 
140
- progress('Confirming...', options.quiet);
141
-
142
- // Clear progress line
143
- progress('', options.quiet);
144
-
145
- console.log(JSON.stringify({
200
+ const output = {
146
201
  success: result.receipt.status === 'success',
147
202
  hash: result.hash,
148
203
  asset: assetUpper,
149
204
  amount,
205
+ fee: feeStr,
206
+ totalSent: grossStr,
150
207
  blockNumber: result.receipt.blockNumber.toString(),
151
- gasUsed: result.receipt.gasUsed.toString(),
152
- }, null, 2));
208
+ };
209
+
210
+ if (options.json) {
211
+ printJson(output);
212
+ return;
213
+ }
214
+
215
+ printHeader('Deposit Submitted');
216
+ printFields([
217
+ { label: 'Asset', value: assetUpper },
218
+ { label: 'Amount', value: `${amount} ${assetUpper}` },
219
+ { label: 'Fee', value: `${feeStr} ${assetUpper} (0.3%)` },
220
+ { label: 'Total sent', value: `${grossStr} ${assetUpper}` },
221
+ { label: 'From', value: address },
222
+ { label: 'Transaction', value: txUrl(result.hash) },
223
+ { label: 'Block', value: result.receipt.blockNumber },
224
+ ]);
225
+ printLine();
153
226
  } catch (error) {
154
- progress('', options.quiet); // Clear progress line
227
+ clearProgress();
155
228
  handleCLIError(error);
156
229
  }
157
230
  });