@veil-cash/sdk 0.2.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veil-cash/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",
package/src/abi.ts CHANGED
@@ -62,6 +62,24 @@ export const ENTRY_ABI = [
62
62
  type: 'function',
63
63
  },
64
64
 
65
+ // Change deposit key (must already be registered)
66
+ {
67
+ inputs: [
68
+ {
69
+ components: [
70
+ { name: 'owner', type: 'address' },
71
+ { name: 'depositKey', type: 'bytes' },
72
+ ],
73
+ name: '_account',
74
+ type: 'tuple',
75
+ },
76
+ ],
77
+ name: 'changeDepositKey',
78
+ outputs: [],
79
+ stateMutability: 'nonpayable',
80
+ type: 'function',
81
+ },
82
+
65
83
  // Queue ETH deposit
66
84
  {
67
85
  inputs: [{ name: '_depositKey', type: 'bytes' }],
@@ -83,6 +101,18 @@ export const ENTRY_ABI = [
83
101
  type: 'function',
84
102
  },
85
103
 
104
+ // Queue cbBTC deposit
105
+ {
106
+ inputs: [
107
+ { name: '_amount', type: 'uint256' },
108
+ { name: '_depositKey', type: 'bytes' },
109
+ ],
110
+ name: 'queueBTC',
111
+ outputs: [],
112
+ stateMutability: 'nonpayable',
113
+ type: 'function',
114
+ },
115
+
86
116
  // Read deposit keys
87
117
  {
88
118
  inputs: [{ name: '', type: 'address' }],
package/src/addresses.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Contract addresses for Veil on Base
3
3
  */
4
4
 
5
- import type { NetworkAddresses } from './types.js';
5
+ import type { NetworkAddresses, RelayPool } from './types.js';
6
6
 
7
7
  /**
8
8
  * Contract addresses for Base mainnet
@@ -14,6 +14,9 @@ export const ADDRESSES: NetworkAddresses = {
14
14
  usdcPool: '0x5c50d58E49C59d112680c187De2Bf989d2a91242',
15
15
  usdcQueue: '0x5530241b24504bF05C9a22e95A1F5458888e6a9B',
16
16
  usdcToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
17
+ cbbtcPool: '0x51A021da774b4bBB59B47f7CB4ccd631337680BA',
18
+ cbbtcQueue: '0x977741CaDF8D1431c4816C0993D32b02094cD35C',
19
+ cbbtcToken: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf',
17
20
  chainId: 8453,
18
21
  relayUrl: 'https://veil-relay.up.railway.app',
19
22
  } as const;
@@ -34,6 +37,12 @@ export const POOL_CONFIG = {
34
37
  symbol: 'USDC',
35
38
  name: 'USD Coin',
36
39
  },
40
+ cbbtc: {
41
+ decimals: 8,
42
+ displayDecimals: 6,
43
+ symbol: 'cbBTC',
44
+ name: 'Coinbase Bitcoin',
45
+ },
37
46
  } as const;
38
47
 
39
48
  /**
@@ -44,6 +53,36 @@ export function getAddresses(): NetworkAddresses {
44
53
  return ADDRESSES;
45
54
  }
46
55
 
56
+ /**
57
+ * Get the pool contract address for a given pool
58
+ * @param pool - Pool identifier ('eth', 'usdc', or 'cbbtc')
59
+ * @returns Pool contract address
60
+ */
61
+ export function getPoolAddress(pool: RelayPool): `0x${string}` {
62
+ const addresses = getAddresses();
63
+ switch (pool) {
64
+ case 'eth': return addresses.ethPool;
65
+ case 'usdc': return addresses.usdcPool;
66
+ case 'cbbtc': return addresses.cbbtcPool;
67
+ default: throw new Error(`Unknown pool: ${pool}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get the queue contract address for a given pool
73
+ * @param pool - Pool identifier ('eth', 'usdc', or 'cbbtc')
74
+ * @returns Queue contract address
75
+ */
76
+ export function getQueueAddress(pool: RelayPool): `0x${string}` {
77
+ const addresses = getAddresses();
78
+ switch (pool) {
79
+ case 'eth': return addresses.ethQueue;
80
+ case 'usdc': return addresses.usdcQueue;
81
+ case 'cbbtc': return addresses.cbbtcQueue;
82
+ default: throw new Error(`Unknown pool: ${pool}`);
83
+ }
84
+ }
85
+
47
86
  /**
48
87
  * Get Relay URL
49
88
  * @returns Relay URL for Base mainnet
package/src/balance.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  * Query queue and private balances directly from blockchain
4
4
  */
5
5
 
6
- import { createPublicClient, http, formatEther } from 'viem';
6
+ import { createPublicClient, http, formatUnits } from 'viem';
7
7
  import { base } from 'viem/chains';
8
- import { getAddresses } from './addresses.js';
8
+ import { getPoolAddress, getQueueAddress, POOL_CONFIG } from './addresses.js';
9
9
  import { QUEUE_ABI, POOL_ABI } from './abi.js';
10
10
  import { Keypair } from './keypair.js';
11
11
  import { Utxo } from './utxo.js';
@@ -15,6 +15,7 @@ import type {
15
15
  PendingDeposit,
16
16
  PrivateBalanceResult,
17
17
  UtxoInfo,
18
+ RelayPool,
18
19
  } from './types.js';
19
20
 
20
21
  // Deposit status enum from Queue contract
@@ -44,6 +45,7 @@ export type ProgressCallback = (stage: string, detail?: string) => void;
44
45
  * ```typescript
45
46
  * const result = await getQueueBalance({
46
47
  * address: '0x...',
48
+ * pool: 'eth',
47
49
  * onProgress: (stage, detail) => console.log(stage, detail),
48
50
  * });
49
51
  *
@@ -53,11 +55,13 @@ export type ProgressCallback = (stage: string, detail?: string) => void;
53
55
  */
54
56
  export async function getQueueBalance(options: {
55
57
  address: `0x${string}`;
58
+ pool?: RelayPool;
56
59
  rpcUrl?: string;
57
60
  onProgress?: ProgressCallback;
58
61
  }): Promise<QueueBalanceResult> {
59
- const { address, rpcUrl, onProgress } = options;
60
- const addresses = getAddresses();
62
+ const { address, pool = 'eth', rpcUrl, onProgress } = options;
63
+ const queueAddress = getQueueAddress(pool);
64
+ const poolConfig = POOL_CONFIG[pool];
61
65
 
62
66
  // Create public client
63
67
  const publicClient = createPublicClient({
@@ -68,7 +72,7 @@ export async function getQueueBalance(options: {
68
72
  // Get all pending deposit nonces
69
73
  onProgress?.('Fetching pending deposits...');
70
74
  const pendingNonces = await publicClient.readContract({
71
- address: addresses.ethQueue,
75
+ address: queueAddress,
72
76
  abi: QUEUE_ABI,
73
77
  functionName: 'getPendingDeposits',
74
78
  }) as bigint[];
@@ -83,7 +87,7 @@ export async function getQueueBalance(options: {
83
87
  onProgress?.('Checking deposit', `${i + 1}/${pendingNonces.length}`);
84
88
 
85
89
  const deposit = await publicClient.readContract({
86
- address: addresses.ethQueue,
90
+ address: queueAddress,
87
91
  abi: QUEUE_ABI,
88
92
  functionName: 'getDeposit',
89
93
  args: [nonce],
@@ -103,7 +107,7 @@ export async function getQueueBalance(options: {
103
107
  pendingDeposits.push({
104
108
  nonce: nonce.toString(),
105
109
  status: DEPOSIT_STATUS_MAP[deposit.status as keyof typeof DEPOSIT_STATUS_MAP] || 'pending',
106
- amount: formatEther(deposit.amountIn),
110
+ amount: formatUnits(deposit.amountIn, poolConfig.decimals),
107
111
  amountWei: deposit.amountIn.toString(),
108
112
  timestamp: new Date(Number(deposit.timestamp) * 1000).toISOString(),
109
113
  });
@@ -116,7 +120,7 @@ export async function getQueueBalance(options: {
116
120
 
117
121
  return {
118
122
  address,
119
- queueBalance: formatEther(totalQueueBalance),
123
+ queueBalance: formatUnits(totalQueueBalance, poolConfig.decimals),
120
124
  queueBalanceWei: totalQueueBalance.toString(),
121
125
  pendingDeposits,
122
126
  pendingCount: pendingDeposits.length,
@@ -138,6 +142,7 @@ export async function getQueueBalance(options: {
138
142
  * const keypair = new Keypair(process.env.VEIL_KEY);
139
143
  * const result = await getPrivateBalance({
140
144
  * keypair,
145
+ * pool: 'eth',
141
146
  * onProgress: (stage, detail) => console.log(stage, detail),
142
147
  * });
143
148
  *
@@ -147,11 +152,13 @@ export async function getQueueBalance(options: {
147
152
  */
148
153
  export async function getPrivateBalance(options: {
149
154
  keypair: Keypair;
155
+ pool?: RelayPool;
150
156
  rpcUrl?: string;
151
157
  onProgress?: ProgressCallback;
152
158
  }): Promise<PrivateBalanceResult> {
153
- const { keypair, rpcUrl, onProgress } = options;
154
- const addresses = getAddresses();
159
+ const { keypair, pool = 'eth', rpcUrl, onProgress } = options;
160
+ const poolAddress = getPoolAddress(pool);
161
+ const poolConfig = POOL_CONFIG[pool];
155
162
 
156
163
  if (!keypair.privkey) {
157
164
  throw new Error('Keypair must have a private key to calculate private balance');
@@ -166,7 +173,7 @@ export async function getPrivateBalance(options: {
166
173
  // 1. Get total count of encrypted outputs
167
174
  onProgress?.('Fetching pool index...');
168
175
  const nextIndex = await publicClient.readContract({
169
- address: addresses.ethPool,
176
+ address: poolAddress,
170
177
  abi: POOL_ABI,
171
178
  functionName: 'nextIndex',
172
179
  }) as number;
@@ -194,7 +201,7 @@ export async function getPrivateBalance(options: {
194
201
  onProgress?.('Fetching encrypted outputs', `batch ${batchNum}/${totalBatches} (${start}-${end})`);
195
202
 
196
203
  const batch = await publicClient.readContract({
197
- address: addresses.ethPool,
204
+ address: poolAddress,
198
205
  abi: POOL_ABI,
199
206
  functionName: 'getEncryptedOutputs',
200
207
  args: [BigInt(start), BigInt(end)],
@@ -234,7 +241,7 @@ export async function getPrivateBalance(options: {
234
241
  const nullifierHex = toFixedHex(nullifier) as `0x${string}`;
235
242
 
236
243
  const isSpent = await publicClient.readContract({
237
- address: addresses.ethPool,
244
+ address: poolAddress,
238
245
  abi: POOL_ABI,
239
246
  functionName: 'isSpent',
240
247
  args: [nullifierHex],
@@ -242,7 +249,7 @@ export async function getPrivateBalance(options: {
242
249
 
243
250
  utxoInfos.push({
244
251
  index,
245
- amount: formatEther(utxo.amount),
252
+ amount: formatUnits(utxo.amount, poolConfig.decimals),
246
253
  amountWei: utxo.amount.toString(),
247
254
  isSpent,
248
255
  });
@@ -256,7 +263,7 @@ export async function getPrivateBalance(options: {
256
263
  }
257
264
 
258
265
  return {
259
- privateBalance: formatEther(totalBalance),
266
+ privateBalance: formatUnits(totalBalance, poolConfig.decimals),
260
267
  privateBalanceWei: totalBalance.toString(),
261
268
  utxoCount: decryptedUtxos.length,
262
269
  spentCount,
@@ -4,14 +4,91 @@
4
4
 
5
5
  import { Command } from 'commander';
6
6
  import { getQueueBalance, getPrivateBalance } from '../../balance.js';
7
+ import { POOL_CONFIG } from '../../addresses.js';
7
8
  import { Keypair } from '../../keypair.js';
8
9
  import { getAddress } from '../wallet.js';
9
- import { formatEther } from 'viem';
10
+ import { formatUnits } from 'viem';
10
11
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
12
+ import type { RelayPool } from '../../types.js';
13
+
14
+ const SUPPORTED_POOLS: RelayPool[] = ['eth', 'usdc', 'cbbtc'];
15
+
16
+ /**
17
+ * Fetch balance for a single pool and return structured output
18
+ */
19
+ async function fetchPoolBalance(
20
+ pool: RelayPool,
21
+ address: `0x${string}`,
22
+ keypair: Keypair | null,
23
+ rpcUrl: string | undefined,
24
+ onProgress: ((stage: string, detail?: string) => void) | undefined,
25
+ ): Promise<Record<string, unknown>> {
26
+ const poolConfig = POOL_CONFIG[pool];
27
+
28
+ // Get queue balance
29
+ const poolProgress = onProgress
30
+ ? (stage: string, detail?: string) => onProgress(`[${pool.toUpperCase()}] ${stage}`, detail)
31
+ : undefined;
32
+
33
+ const queueResult = await getQueueBalance({ address, pool, rpcUrl, onProgress: poolProgress });
34
+
35
+ // Get private balance if keypair available
36
+ let privateResult = null;
37
+ if (keypair) {
38
+ privateResult = await getPrivateBalance({ keypair, pool, rpcUrl, onProgress: poolProgress });
39
+ }
40
+
41
+ // Calculate total balance
42
+ const queueBalanceWei = BigInt(queueResult.queueBalanceWei);
43
+ const privateBalanceWei = privateResult ? BigInt(privateResult.privateBalanceWei) : 0n;
44
+ const totalBalanceWei = queueBalanceWei + privateBalanceWei;
45
+
46
+ const result: Record<string, unknown> = {
47
+ pool: pool.toUpperCase(),
48
+ symbol: poolConfig.symbol,
49
+ totalBalance: formatUnits(totalBalanceWei, poolConfig.decimals),
50
+ totalBalanceWei: totalBalanceWei.toString(),
51
+ };
52
+
53
+ // Private balance
54
+ if (privateResult) {
55
+ const unspentUtxos = privateResult.utxos.filter(u => !u.isSpent);
56
+ result.private = {
57
+ balance: privateResult.privateBalance,
58
+ balanceWei: privateResult.privateBalanceWei,
59
+ utxoCount: privateResult.unspentCount,
60
+ utxos: unspentUtxos.map(u => ({
61
+ index: u.index,
62
+ amount: u.amount,
63
+ })),
64
+ };
65
+ } else {
66
+ result.private = {
67
+ balance: null,
68
+ note: 'Set VEIL_KEY to see private balance',
69
+ };
70
+ }
71
+
72
+ // Queue details
73
+ result.queue = {
74
+ balance: queueResult.queueBalance,
75
+ balanceWei: queueResult.queueBalanceWei,
76
+ count: queueResult.pendingCount,
77
+ deposits: queueResult.pendingDeposits.map(d => ({
78
+ nonce: d.nonce,
79
+ amount: d.amount,
80
+ status: d.status,
81
+ timestamp: d.timestamp,
82
+ })),
83
+ };
84
+
85
+ return result;
86
+ }
11
87
 
12
88
  export function createBalanceCommand(): Command {
13
89
  const balance = new Command('balance')
14
- .description('Show queue and private balances')
90
+ .description('Show queue and private balances (all pools by default)')
91
+ .option('--pool <pool>', 'Pool to check (eth, usdc, cbbtc, or all)', 'all')
15
92
  .option('--wallet-key <key>', 'Ethereum wallet key (or set WALLET_KEY env)')
16
93
  .option('--address <address>', 'Address to check (or derived from wallet key)')
17
94
  .option('--veil-key <key>', 'Veil private key (or set VEIL_KEY env)')
@@ -19,6 +96,15 @@ export function createBalanceCommand(): Command {
19
96
  .option('--quiet', 'Suppress progress output')
20
97
  .action(async (options) => {
21
98
  try {
99
+ const poolArg = (options.pool || 'all').toLowerCase();
100
+
101
+ // Validate pool
102
+ if (poolArg !== 'all' && !SUPPORTED_POOLS.includes(poolArg as RelayPool)) {
103
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported pool: ${options.pool}. Supported: ${SUPPORTED_POOLS.join(', ')}, all`);
104
+ }
105
+
106
+ const poolsToQuery: RelayPool[] = poolArg === 'all' ? [...SUPPORTED_POOLS] : [poolArg as RelayPool];
107
+
22
108
  // Get address
23
109
  let address: `0x${string}`;
24
110
  if (options.address) {
@@ -45,66 +131,40 @@ export function createBalanceCommand(): Command {
45
131
  process.stderr.write(`\r\x1b[K${msg}`);
46
132
  };
47
133
 
48
- // Get queue balance
49
- const queueResult = await getQueueBalance({ address, rpcUrl, onProgress });
134
+ // Get deposit key if available
135
+ const depositKey = process.env.DEPOSIT_KEY || (keypair ? keypair.depositKey() : null);
50
136
 
51
- // Get private balance if keypair available
52
- let privateResult = null;
53
- if (keypair) {
54
- privateResult = await getPrivateBalance({ keypair, rpcUrl, onProgress });
55
- }
137
+ // Single pool mode -- flat output (backwards compatible)
138
+ if (poolsToQuery.length === 1) {
139
+ const poolResult = await fetchPoolBalance(poolsToQuery[0], address, keypair, rpcUrl, onProgress);
56
140
 
57
- // Clear progress line
58
- if (!options.quiet) {
59
- process.stderr.write('\r\x1b[K');
141
+ // Clear progress line
142
+ if (!options.quiet) process.stderr.write('\r\x1b[K');
143
+
144
+ const output = {
145
+ address,
146
+ depositKey: depositKey || null,
147
+ ...poolResult,
148
+ };
149
+
150
+ console.log(JSON.stringify(output, null, 2));
151
+ return;
60
152
  }
61
153
 
62
- // Calculate total balance
63
- const queueBalanceWei = BigInt(queueResult.queueBalanceWei);
64
- const privateBalanceWei = privateResult ? BigInt(privateResult.privateBalanceWei) : 0n;
65
- const totalBalanceWei = queueBalanceWei + privateBalanceWei;
154
+ // 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
+ }
66
160
 
67
- // Get deposit key if available
68
- const depositKey = process.env.DEPOSIT_KEY || (keypair ? keypair.depositKey() : null);
161
+ // Clear progress line
162
+ if (!options.quiet) process.stderr.write('\r\x1b[K');
69
163
 
70
- // Build output structure
71
- const output: Record<string, unknown> = {
164
+ const output = {
72
165
  address,
73
166
  depositKey: depositKey || null,
74
- totalBalance: formatEther(totalBalanceWei),
75
- totalBalanceWei: totalBalanceWei.toString(),
76
- };
77
-
78
- // Private balance first
79
- if (privateResult) {
80
- const unspentUtxos = privateResult.utxos.filter(u => !u.isSpent);
81
- output.private = {
82
- balance: privateResult.privateBalance,
83
- balanceWei: privateResult.privateBalanceWei,
84
- utxoCount: privateResult.unspentCount,
85
- utxos: unspentUtxos.map(u => ({
86
- index: u.index,
87
- amount: u.amount,
88
- })),
89
- };
90
- } else {
91
- output.private = {
92
- balance: null,
93
- note: 'Set VEIL_KEY to see private balance',
94
- };
95
- }
96
-
97
- // Queue details second
98
- output.queue = {
99
- balance: queueResult.queueBalance,
100
- balanceWei: queueResult.queueBalanceWei,
101
- count: queueResult.pendingCount,
102
- deposits: queueResult.pendingDeposits.map(d => ({
103
- nonce: d.nonce,
104
- amount: d.amount,
105
- status: d.status,
106
- timestamp: d.timestamp,
107
- })),
167
+ pools,
108
168
  };
109
169
 
110
170
  console.log(JSON.stringify(output, null, 2));
@@ -3,17 +3,27 @@
3
3
  */
4
4
 
5
5
  import { Command } from 'commander';
6
- import { buildDepositETHTx } from '../../deposit.js';
6
+ import { buildDepositETHTx, buildDepositUSDCTx, buildApproveUSDCTx, buildDepositCBBTCTx, buildApproveCBBTCTx } from '../../deposit.js';
7
7
  import { sendTransaction, getAddress, getBalance } from '../wallet.js';
8
8
  import { getConfig } from '../config.js';
9
9
  import { parseEther, formatEther } from 'viem';
10
10
  import { handleCLIError, CLIError, ErrorCode } from '../errors.js';
11
+ import type { TransactionData } from '../../types.js';
11
12
 
12
- // Minimum deposit: 0.01 ETH (net after 0.3% fee)
13
- // To deposit 0.01 ETH net, you need to send: 0.01 / (1 - 0.003) ≈ 0.01003 ETH
14
- const MINIMUM_DEPOSIT_ETH = 0.01;
13
+ // Minimum deposits per asset (net after 0.3% fee)
15
14
  const DEPOSIT_FEE_PERCENT = 0.3;
16
- const MINIMUM_DEPOSIT_WITH_FEE = MINIMUM_DEPOSIT_ETH / (1 - DEPOSIT_FEE_PERCENT / 100);
15
+ const MINIMUM_DEPOSITS: Record<string, number> = {
16
+ ETH: 0.01,
17
+ USDC: 10,
18
+ CBBTC: 0.0001,
19
+ };
20
+
21
+ function getMinimumWithFee(asset: string): number {
22
+ const min = MINIMUM_DEPOSITS[asset] || 0;
23
+ return min / (1 - DEPOSIT_FEE_PERCENT / 100);
24
+ }
25
+
26
+ const SUPPORTED_ASSETS = ['ETH', 'USDC', 'CBBTC'];
17
27
 
18
28
  // Progress helper - writes to stderr so JSON output stays clean
19
29
  function progress(msg: string, quiet?: boolean) {
@@ -24,8 +34,8 @@ function progress(msg: string, quiet?: boolean) {
24
34
 
25
35
  export function createDepositCommand(): Command {
26
36
  const deposit = new Command('deposit')
27
- .description('Deposit ETH into Veil')
28
- .argument('<asset>', 'Asset to deposit (ETH)')
37
+ .description('Deposit ETH, USDC, or cbBTC into Veil')
38
+ .argument('<asset>', 'Asset to deposit (ETH, USDC, or CBBTC)')
29
39
  .argument('<amount>', 'Amount to deposit (e.g., 0.1)')
30
40
  .option('--deposit-key <key>', 'Your Veil deposit key (or set DEPOSIT_KEY env)')
31
41
  .option('--wallet-key <key>', 'Ethereum wallet key for signing (or set WALLET_KEY env)')
@@ -34,19 +44,23 @@ export function createDepositCommand(): Command {
34
44
  .option('--quiet', 'Suppress progress output')
35
45
  .action(async (asset: string, amount: string, options) => {
36
46
  try {
37
- // Validate asset is ETH
38
- if (asset.toUpperCase() !== 'ETH') {
39
- throw new CLIError(ErrorCode.INVALID_AMOUNT, 'Only ETH is supported');
47
+ const assetUpper = asset.toUpperCase();
48
+
49
+ // Validate asset
50
+ if (!SUPPORTED_ASSETS.includes(assetUpper)) {
51
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported asset: ${asset}. Supported: ${SUPPORTED_ASSETS.join(', ')}`);
40
52
  }
41
53
 
42
54
  const amountNum = parseFloat(amount);
55
+ const minimumWithFee = getMinimumWithFee(assetUpper);
56
+ const minimumNet = MINIMUM_DEPOSITS[assetUpper];
43
57
 
44
58
  // Check minimum deposit
45
- if (amountNum < MINIMUM_DEPOSIT_WITH_FEE) {
59
+ if (amountNum < minimumWithFee) {
46
60
  throw new CLIError(
47
61
  ErrorCode.INVALID_AMOUNT,
48
- `Minimum deposit is ${MINIMUM_DEPOSIT_ETH} ETH (net). ` +
49
- `With ${DEPOSIT_FEE_PERCENT}% fee, send at least ${MINIMUM_DEPOSIT_WITH_FEE.toFixed(5)} ETH.`
62
+ `Minimum deposit is ${minimumNet} ${assetUpper} (net). ` +
63
+ `With ${DEPOSIT_FEE_PERCENT}% fee, send at least ${minimumWithFee.toFixed(assetUpper === 'ETH' ? 5 : 8)} ${assetUpper}.`
50
64
  );
51
65
  }
52
66
 
@@ -58,24 +72,45 @@ export function createDepositCommand(): Command {
58
72
 
59
73
  progress('Building transaction...', options.quiet);
60
74
 
61
- // Build the transaction
62
- const tx = buildDepositETHTx({
63
- depositKey,
64
- amount,
65
- });
75
+ // Build the deposit transaction
76
+ let tx: TransactionData;
77
+ let approveTx: TransactionData | null = null;
78
+
79
+ if (assetUpper === 'USDC') {
80
+ approveTx = buildApproveUSDCTx({ amount });
81
+ tx = buildDepositUSDCTx({ depositKey, amount });
82
+ } else if (assetUpper === 'CBBTC') {
83
+ approveTx = buildApproveCBBTCTx({ amount });
84
+ tx = buildDepositCBBTCTx({ depositKey, amount });
85
+ } else {
86
+ tx = buildDepositETHTx({ depositKey, amount });
87
+ }
66
88
 
67
89
  // Handle --unsigned mode (no wallet required, just build payload)
68
90
  if (options.unsigned) {
69
91
  progress('', options.quiet); // Clear line
70
- // Output Bankr-compatible format
71
- const payload = {
92
+ const payloads: Record<string, unknown>[] = [];
93
+
94
+ // Include approval tx for ERC20 tokens
95
+ if (approveTx) {
96
+ payloads.push({
97
+ step: 'approve',
98
+ to: approveTx.to,
99
+ data: approveTx.data,
100
+ value: '0',
101
+ chainId: 8453,
102
+ });
103
+ }
104
+
105
+ payloads.push({
106
+ step: 'deposit',
72
107
  to: tx.to,
73
108
  data: tx.data,
74
109
  value: tx.value ? tx.value.toString() : '0',
75
- chainId: 8453, // Base mainnet
76
- };
110
+ chainId: 8453,
111
+ });
77
112
 
78
- console.log(JSON.stringify(payload, null, 2));
113
+ console.log(JSON.stringify(payloads.length === 1 ? payloads[0] : payloads, null, 2));
79
114
  return;
80
115
  }
81
116
 
@@ -83,20 +118,27 @@ export function createDepositCommand(): Command {
83
118
  const config = getConfig(options);
84
119
  const address = getAddress(config.privateKey);
85
120
 
86
- progress('Checking balance...', options.quiet);
121
+ // For ETH deposits, check ETH balance
122
+ if (assetUpper === 'ETH') {
123
+ progress('Checking balance...', options.quiet);
124
+ const balance = await getBalance(address, config.rpcUrl);
125
+ const amountWei = parseEther(amount);
126
+
127
+ if (balance < amountWei) {
128
+ progress('', options.quiet);
129
+ throw new CLIError(ErrorCode.INSUFFICIENT_BALANCE, `Insufficient ETH balance. Have: ${formatEther(balance)} ETH, Need: ${amount} ETH`);
130
+ }
131
+ }
87
132
 
88
- // Check balance
89
- const balance = await getBalance(address, config.rpcUrl);
90
- const amountWei = parseEther(amount);
91
-
92
- if (balance < amountWei) {
93
- progress('', options.quiet);
94
- throw new CLIError(ErrorCode.INSUFFICIENT_BALANCE, `Insufficient ETH balance. Have: ${formatEther(balance)} ETH, Need: ${amount} ETH`);
133
+ // Send approval transaction for ERC20 tokens
134
+ if (approveTx) {
135
+ progress(`Approving ${assetUpper}...`, options.quiet);
136
+ await sendTransaction(config, approveTx);
95
137
  }
96
138
 
97
- progress('Sending transaction...', options.quiet);
139
+ progress('Sending deposit transaction...', options.quiet);
98
140
 
99
- // Send the transaction
141
+ // Send the deposit transaction
100
142
  const result = await sendTransaction(config, tx);
101
143
 
102
144
  progress('Confirming...', options.quiet);
@@ -107,6 +149,7 @@ export function createDepositCommand(): Command {
107
149
  console.log(JSON.stringify({
108
150
  success: result.receipt.status === 'success',
109
151
  hash: result.hash,
152
+ asset: assetUpper,
110
153
  amount,
111
154
  blockNumber: result.receipt.blockNumber.toString(),
112
155
  gasUsed: result.receipt.gasUsed.toString(),