@veil-cash/sdk 0.3.0 → 0.5.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/src/cli/config.ts CHANGED
@@ -2,22 +2,50 @@
2
2
  * CLI configuration handling
3
3
  */
4
4
 
5
+ import { CLIError, ErrorCode } from './errors.js';
6
+ import { getAddress } from './wallet.js';
5
7
  import type { WalletConfig } from './wallet.js';
6
8
 
7
9
  export interface CLIOptions {
8
- walletKey?: string;
9
10
  rpcUrl?: string;
10
11
  }
11
12
 
13
+ export interface AddressOptions {
14
+ address?: string;
15
+ }
16
+
17
+ export interface ResolvedAddress {
18
+ address: `0x${string}`;
19
+ source: 'flag' | 'wallet-key' | 'signer-address';
20
+ }
21
+
22
+ function validateAddress(address: string, sourceLabel: string): `0x${string}` {
23
+ const normalized = address.trim();
24
+ if (!/^0x[a-fA-F0-9]{40}$/.test(normalized)) {
25
+ throw new CLIError(ErrorCode.INVALID_ADDRESS, `${sourceLabel} must be a valid 0x-prefixed Ethereum address.`);
26
+ }
27
+ return normalized as `0x${string}`;
28
+ }
29
+
30
+ function ensureAddressEnvConsistency(): void {
31
+ if (process.env.WALLET_KEY && process.env.SIGNER_ADDRESS) {
32
+ throw new CLIError(
33
+ ErrorCode.CONFIG_CONFLICT,
34
+ 'WALLET_KEY and SIGNER_ADDRESS are mutually exclusive. Set only one.',
35
+ );
36
+ }
37
+ }
38
+
12
39
  /**
13
40
  * Get wallet configuration from CLI options and environment variables
14
41
  */
15
42
  export function getConfig(options: CLIOptions): WalletConfig {
16
- const walletKey = options.walletKey || process.env.WALLET_KEY;
43
+ ensureAddressEnvConsistency();
44
+ const walletKey = process.env.WALLET_KEY;
17
45
  const rpcUrl = options.rpcUrl || process.env.RPC_URL || 'https://mainnet.base.org';
18
46
 
19
47
  if (!walletKey) {
20
- throw new Error('Wallet key required. Use --wallet-key or set WALLET_KEY environment variable.');
48
+ throw new Error('WALLET_KEY env var required. Set it before running this command.');
21
49
  }
22
50
 
23
51
  // Validate wallet key format
@@ -38,6 +66,58 @@ export function getVeilKey(options: { veilKey?: string }): string | undefined {
38
66
  return options.veilKey || process.env.VEIL_KEY;
39
67
  }
40
68
 
69
+ /**
70
+ * Resolve an address for query / unsigned flows.
71
+ * Prefers explicit --address, then derives from WALLET_KEY, then falls back to SIGNER_ADDRESS.
72
+ */
73
+ export function resolveAddress(
74
+ options: AddressOptions = {},
75
+ config: { required?: boolean; allowInvalidWalletKey?: boolean } = {},
76
+ ): ResolvedAddress | null {
77
+ ensureAddressEnvConsistency();
78
+
79
+ if (options.address) {
80
+ return {
81
+ address: validateAddress(options.address, '--address'),
82
+ source: 'flag',
83
+ };
84
+ }
85
+
86
+ const walletKey = process.env.WALLET_KEY;
87
+ if (walletKey) {
88
+ try {
89
+ return {
90
+ address: getAddress(walletKey as `0x${string}`),
91
+ source: 'wallet-key',
92
+ };
93
+ } catch {
94
+ if (!config.allowInvalidWalletKey) {
95
+ throw new CLIError(
96
+ ErrorCode.WALLET_KEY_MISSING,
97
+ 'Invalid WALLET_KEY format. Must be a 0x-prefixed 64-character hex string.',
98
+ );
99
+ }
100
+ }
101
+ }
102
+
103
+ const signerAddress = process.env.SIGNER_ADDRESS;
104
+ if (signerAddress) {
105
+ return {
106
+ address: validateAddress(signerAddress, 'SIGNER_ADDRESS'),
107
+ source: 'signer-address',
108
+ };
109
+ }
110
+
111
+ if (config.required) {
112
+ throw new CLIError(
113
+ ErrorCode.WALLET_KEY_MISSING,
114
+ 'Must provide --address, set SIGNER_ADDRESS, or set WALLET_KEY env.',
115
+ );
116
+ }
117
+
118
+ return null;
119
+ }
120
+
41
121
  /**
42
122
  * Load environment variables from .env.veil and .env files
43
123
  */
@@ -46,10 +126,10 @@ export function loadEnv(): void {
46
126
  // Dynamic import to avoid bundling issues
47
127
  // eslint-disable-next-line @typescript-eslint/no-require-imports
48
128
  const dotenv = require('dotenv');
49
-
129
+
50
130
  // Load .env.veil first (Veil-specific config)
51
131
  dotenv.config({ path: '.env.veil', quiet: true });
52
-
132
+
53
133
  // Then load .env (for WALLET_KEY, RPC_URL if not in .env.veil)
54
134
  dotenv.config({ quiet: true });
55
135
  } catch {
package/src/cli/errors.ts CHANGED
@@ -9,6 +9,7 @@ export const ErrorCode = {
9
9
  VEIL_KEY_MISSING: 'VEIL_KEY_MISSING',
10
10
  WALLET_KEY_MISSING: 'WALLET_KEY_MISSING',
11
11
  DEPOSIT_KEY_MISSING: 'DEPOSIT_KEY_MISSING',
12
+ CONFIG_CONFLICT: 'CONFIG_CONFLICT',
12
13
  INVALID_ADDRESS: 'INVALID_ADDRESS',
13
14
  INVALID_AMOUNT: 'INVALID_AMOUNT',
14
15
  INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
@@ -51,6 +52,9 @@ function inferErrorCode(message: string): ErrorCodeType {
51
52
  if (msg.includes('deposit_key') || msg.includes('deposit key')) {
52
53
  return ErrorCode.DEPOSIT_KEY_MISSING;
53
54
  }
55
+ if (msg.includes('mutually exclusive') || msg.includes('config conflict') || msg.includes('signer_address')) {
56
+ return ErrorCode.CONFIG_CONFLICT;
57
+ }
54
58
  if (msg.includes('invalid') && msg.includes('address')) {
55
59
  return ErrorCode.INVALID_ADDRESS;
56
60
  }
package/src/cli/index.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  * veil register # Register on-chain
9
9
  * veil deposit ETH 0.1 # Deposit ETH
10
10
  * veil balance # Show all balances
11
- * veil queue-balance # Show pending queue deposits
12
- * veil private-balance # Show private balance
11
+ * veil balance queue # Show pending queue deposits
12
+ * veil balance private # Show private balance
13
13
  * veil withdraw ETH 0.1 0x... # Withdraw to public address
14
14
  * veil transfer ETH 0.1 0x... # Transfer privately
15
15
  * veil merge ETH 0.5 # Merge UTXOs (self-transfer)
@@ -36,7 +36,14 @@ const program = new Command();
36
36
  program
37
37
  .name('veil')
38
38
  .description('CLI for Veil Cash privacy pools on Base')
39
- .version('0.3.0');
39
+ .version('0.5.0')
40
+ .addHelpText('after', `
41
+ Getting started:
42
+ veil init
43
+ veil register
44
+ veil deposit ETH 0.1
45
+ veil balance
46
+ `);
40
47
 
41
48
  // Add commands
42
49
  program.addCommand(createInitCommand());
@@ -44,12 +51,23 @@ program.addCommand(createKeypairCommand());
44
51
  program.addCommand(createRegisterCommand());
45
52
  program.addCommand(createDepositCommand());
46
53
  program.addCommand(createBalanceCommand());
47
- program.addCommand(createQueueBalanceCommand());
48
- program.addCommand(createPrivateBalanceCommand());
54
+ program.addCommand(createQueueBalanceCommand(), { hidden: true });
55
+ program.addCommand(createPrivateBalanceCommand(), { hidden: true });
49
56
  program.addCommand(createWithdrawCommand());
50
57
  program.addCommand(createTransferCommand());
51
58
  program.addCommand(createMergeCommand());
52
59
  program.addCommand(createStatusCommand());
53
60
 
61
+ const knownTopLevelCommands = new Set([
62
+ ...program.commands.map((command) => command.name()),
63
+ 'help',
64
+ ]);
65
+
66
+ const firstArg = process.argv[2];
67
+ if (firstArg && !firstArg.startsWith('-') && !knownTopLevelCommands.has(firstArg)) {
68
+ console.error(`error: unknown command '${firstArg}'`);
69
+ process.exit(1);
70
+ }
71
+
54
72
  // Parse and execute
55
73
  program.parse();
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared CLI output helpers.
3
+ */
4
+
5
+ const DIM = '\x1b[2m';
6
+ const BOLD = '\x1b[1m';
7
+ const RESET = '\x1b[0m';
8
+
9
+ export interface JsonOutputOption {
10
+ json?: boolean;
11
+ }
12
+
13
+ export function printJson(value: unknown): void {
14
+ console.log(JSON.stringify(value, null, 2));
15
+ }
16
+
17
+ export function printLine(value = ''): void {
18
+ console.log(value);
19
+ }
20
+
21
+ export function printHeader(title: string): void {
22
+ console.log();
23
+ console.log(`${BOLD}${title}${RESET}`);
24
+ console.log(`${DIM}${'─'.repeat(40)}${RESET}`);
25
+ }
26
+
27
+ export function printSection(title: string): void {
28
+ console.log();
29
+ console.log(`${BOLD}${title}${RESET}`);
30
+ }
31
+
32
+ export function printDivider(): void {
33
+ console.log();
34
+ console.log(`${DIM}${'─'.repeat(40)}${RESET}`);
35
+ }
36
+
37
+ export function printFields(fields: Array<{ label: string; value: unknown }>): void {
38
+ const visibleFields = fields.filter((field) => field.value !== undefined);
39
+ const width = visibleFields.reduce((max, field) => Math.max(max, field.label.length), 0);
40
+
41
+ for (const field of visibleFields) {
42
+ const label = `${DIM}${field.label.padEnd(width)}${RESET}`;
43
+ console.log(` ${label} ${formatValue(field.value)}`);
44
+ }
45
+ }
46
+
47
+ export function printList(items: string[]): void {
48
+ if (items.length === 0) {
49
+ console.log(` ${DIM}(none)${RESET}`);
50
+ return;
51
+ }
52
+
53
+ for (const item of items) {
54
+ console.log(` ${item}`);
55
+ }
56
+ }
57
+
58
+ export function maskValue(value: string, start = 10, end = 8): string {
59
+ if (value.length <= start + end + 3) {
60
+ return value;
61
+ }
62
+
63
+ return `${value.slice(0, start)}...${value.slice(-end)}`;
64
+ }
65
+
66
+ export function txUrl(hash: string): string {
67
+ return `https://basescan.org/tx/${hash}`;
68
+ }
69
+
70
+ export function clearProgress(): void {
71
+ process.stderr.write('\r\x1b[K');
72
+ }
73
+
74
+ export function createProgressReporter(): (stage: string, detail?: string) => void {
75
+ return (stage: string, detail?: string) => {
76
+ const message = detail ? `${stage}: ${detail}` : stage;
77
+ process.stderr.write(`\r\x1b[K${message}`);
78
+ };
79
+ }
80
+
81
+ function formatValue(value: unknown): string {
82
+ if (value === null) return 'null';
83
+ if (value === undefined) return '';
84
+ if (typeof value === 'boolean') return value ? 'yes' : 'no';
85
+ if (typeof value === 'object') return JSON.stringify(value);
86
+ return String(value);
87
+ }
package/src/cli/wallet.ts CHANGED
@@ -9,12 +9,13 @@ import {
9
9
  decodeErrorResult,
10
10
  BaseError,
11
11
  ContractFunctionRevertedError,
12
+ formatUnits,
12
13
  } from 'viem';
13
14
  import { privateKeyToAccount } from 'viem/accounts';
14
15
  import { base } from 'viem/chains';
15
16
  import type { TransactionData } from '../types.js';
16
- import { ENTRY_ABI } from '../abi.js';
17
- import { getAddresses } from '../addresses.js';
17
+ import { ENTRY_ABI, ERC20_ABI } from '../abi.js';
18
+ import { getAddresses, POOL_CONFIG, ADDRESSES } from '../addresses.js';
18
19
 
19
20
  export interface WalletConfig {
20
21
  privateKey: `0x${string}`;
@@ -63,14 +64,25 @@ export function createWallet(config: WalletConfig): any {
63
64
  * Known custom error selectors (first 4 bytes of keccak256 hash of error signature)
64
65
  */
65
66
  const ERROR_SELECTORS: Record<string, string> = {
67
+ '0xd92e233d': 'ZeroAddress',
68
+ '0xe9e26e4e': 'OnlyOwnerCanRegister',
69
+ '0x1f2a2005': 'ZeroAmount',
66
70
  '0x3ee5aeb5': 'DepositsDisabled',
71
+ '0x4cba3441': 'InvalidDepositKey',
72
+ '0xc64891a5': 'NotRelayer',
73
+ '0xfcc00b30': 'NoETHBalance',
74
+ '0x7b7b36da': 'NoTokenBalance',
75
+ '0x7ffddb78': 'TokenApproveFailed',
76
+ '0xb12d13eb': 'ETHTransferFailed',
77
+ '0x1f6d5aef': 'NonceUsed',
78
+ '0x82b42900': 'Unauthorized',
79
+ '0x1ab7da6b': 'DeadlineExpired',
67
80
  '0x8e8f6d6f': 'MinimumDepositNotMet',
68
81
  '0x5cd83192': 'NotAllowedToDeposit',
69
82
  '0x6f5e8818': 'UserNotRegistered',
70
83
  '0x8a2ef116': 'UserAlreadyRegistered',
71
84
  '0x0dc149f0': 'InvalidDepositKey',
72
85
  '0x584a7938': 'InvalidDepositKeyForUser',
73
- '0xd92e233d': 'OnlyOwnerCanRegister',
74
86
  };
75
87
 
76
88
  /**
@@ -106,11 +118,17 @@ function decodeCustomError(error: unknown): string | null {
106
118
 
107
119
  if (possibleData && typeof possibleData === 'string' && possibleData.startsWith('0x')) {
108
120
  try {
109
- const decoded = decodeErrorResult({
110
- abi: ENTRY_ABI,
111
- data: possibleData as `0x${string}`,
112
- });
113
- return decoded.errorName;
121
+ for (const abi of [ENTRY_ABI] as const) {
122
+ try {
123
+ const decoded = decodeErrorResult({
124
+ abi,
125
+ data: possibleData as `0x${string}`,
126
+ });
127
+ return decoded.errorName;
128
+ } catch {
129
+ // Try the next ABI
130
+ }
131
+ }
114
132
  } catch {
115
133
  // Try selector lookup
116
134
  const selector = possibleData.slice(0, 10).toLowerCase();
@@ -151,16 +169,26 @@ export async function sendTransaction(
151
169
  }
152
170
 
153
171
  // Send the transaction
154
- const hash = await walletClient.sendTransaction({
155
- account,
156
- chain,
157
- to: tx.to,
158
- data: tx.data,
159
- value: tx.value,
160
- });
172
+ let hash: `0x${string}`;
173
+ try {
174
+ hash = await walletClient.sendTransaction({
175
+ account,
176
+ chain,
177
+ to: tx.to,
178
+ data: tx.data,
179
+ value: tx.value,
180
+ });
181
+ } catch (error) {
182
+ throw error;
183
+ }
161
184
 
162
185
  // Wait for confirmation
163
- const receipt = await publicClient.waitForTransactionReceipt({ hash });
186
+ let receipt;
187
+ try {
188
+ receipt = await publicClient.waitForTransactionReceipt({ hash });
189
+ } catch (error) {
190
+ throw error;
191
+ }
164
192
 
165
193
  return {
166
194
  hash,
@@ -196,6 +224,37 @@ export async function getBalance(
196
224
  return publicClient.getBalance({ address });
197
225
  }
198
226
 
227
+ /**
228
+ * Get public wallet balances (ETH + USDC) for an address
229
+ */
230
+ export async function getWalletBalances(
231
+ address: `0x${string}`,
232
+ rpcUrl?: string
233
+ ): Promise<{ eth: string; ethWei: string; usdc: string; usdcWei: string }> {
234
+ const transport = http(rpcUrl);
235
+ const publicClient = createPublicClient({
236
+ chain: base,
237
+ transport,
238
+ });
239
+
240
+ const [ethBalance, usdcBalance] = await Promise.all([
241
+ publicClient.getBalance({ address }),
242
+ publicClient.readContract({
243
+ address: ADDRESSES.usdcToken as `0x${string}`,
244
+ abi: ERC20_ABI,
245
+ functionName: 'balanceOf',
246
+ args: [address],
247
+ }) as Promise<bigint>,
248
+ ]);
249
+
250
+ return {
251
+ eth: formatUnits(ethBalance, POOL_CONFIG.eth.decimals),
252
+ ethWei: ethBalance.toString(),
253
+ usdc: formatUnits(usdcBalance, POOL_CONFIG.usdc.decimals),
254
+ usdcWei: usdcBalance.toString(),
255
+ };
256
+ }
257
+
199
258
  /**
200
259
  * Check if an address is already registered (has a deposit key on-chain)
201
260
  */
package/src/deposit.ts CHANGED
@@ -181,65 +181,7 @@ export function buildDepositUSDCTx(options: {
181
181
  }
182
182
 
183
183
  /**
184
- * Build a transaction to approve cbBTC for deposit
185
- * Must be called before depositCBBTC if allowance is insufficient
186
- *
187
- * @param options - Approval options
188
- * @param options.amount - Amount to approve (human readable, e.g., '0.5')
189
- * @returns Transaction data
190
- */
191
- export function buildApproveCBBTCTx(options: {
192
- amount: string;
193
- }): TransactionData {
194
- const { amount } = options;
195
- const addresses = getAddresses();
196
-
197
- const amountWei = parseUnits(amount, POOL_CONFIG.cbbtc.decimals);
198
-
199
- const data = encodeFunctionData({
200
- abi: ERC20_ABI,
201
- functionName: 'approve',
202
- args: [addresses.entry, amountWei],
203
- });
204
-
205
- return {
206
- to: addresses.cbbtcToken,
207
- data,
208
- };
209
- }
210
-
211
- /**
212
- * Build a transaction to deposit cbBTC
213
- * Note: You must approve cbBTC first using buildApproveCBBTCTx
214
- *
215
- * @param options - Deposit options
216
- * @param options.depositKey - Deposit key from Keypair.depositKey()
217
- * @param options.amount - Amount to deposit (human readable, e.g., '0.5')
218
- * @returns Transaction data
219
- */
220
- export function buildDepositCBBTCTx(options: {
221
- depositKey: string;
222
- amount: string;
223
- }): TransactionData {
224
- const { depositKey, amount } = options;
225
- const addresses = getAddresses();
226
-
227
- const amountWei = parseUnits(amount, POOL_CONFIG.cbbtc.decimals);
228
-
229
- const data = encodeFunctionData({
230
- abi: ENTRY_ABI,
231
- functionName: 'queueBTC',
232
- args: [amountWei, depositKey as `0x${string}`],
233
- });
234
-
235
- return {
236
- to: addresses.entry,
237
- data,
238
- };
239
- }
240
-
241
- /**
242
- * Build a deposit transaction (ETH, USDC, or cbBTC)
184
+ * Build a deposit transaction (ETH or USDC)
243
185
  * Convenience function that routes to the correct builder
244
186
  *
245
187
  * @param options - Deposit options
@@ -260,13 +202,6 @@ export function buildDepositCBBTCTx(options: {
260
202
  * amount: '100',
261
203
  * token: 'USDC',
262
204
  * });
263
- *
264
- * // cbBTC deposit (remember to approve first!)
265
- * const cbbtcTx = buildDepositTx({
266
- * depositKey: keypair.depositKey(),
267
- * amount: '0.5',
268
- * token: 'CBBTC',
269
- * });
270
205
  * ```
271
206
  */
272
207
  export function buildDepositTx(options: {
@@ -279,10 +214,6 @@ export function buildDepositTx(options: {
279
214
  if (token === 'USDC') {
280
215
  return buildDepositUSDCTx(rest);
281
216
  }
282
-
283
- if (token === 'CBBTC') {
284
- return buildDepositCBBTCTx(rest);
285
- }
286
217
 
287
218
  return buildDepositETHTx(rest);
288
219
  }
package/src/index.ts CHANGED
@@ -46,8 +46,6 @@ export {
46
46
  buildDepositETHTx,
47
47
  buildDepositUSDCTx,
48
48
  buildApproveUSDCTx,
49
- buildDepositCBBTCTx,
50
- buildApproveCBBTCTx,
51
49
  buildDepositTx,
52
50
  } from './deposit.js';
53
51
 
@@ -118,7 +116,12 @@ export {
118
116
  } from './relay.js';
119
117
 
120
118
  // ABIs
121
- export { ENTRY_ABI, ERC20_ABI, QUEUE_ABI, POOL_ABI } from './abi.js';
119
+ export {
120
+ ENTRY_ABI,
121
+ ERC20_ABI,
122
+ QUEUE_ABI,
123
+ POOL_ABI,
124
+ } from './abi.js';
122
125
 
123
126
  // Utilities
124
127
  export {
package/src/prover.ts CHANGED
@@ -133,6 +133,9 @@ export async function prove(input: ProofInput, circuitName: string): Promise<str
133
133
  utils.stringifyBigInts(input) as any,
134
134
  wasmPath,
135
135
  zkeyPath,
136
+ undefined,
137
+ undefined,
138
+ { singleThread: true },
136
139
  );
137
140
  const proof = result.proof as unknown as SnarkProof;
138
141
 
package/src/relay.ts CHANGED
@@ -98,8 +98,8 @@ export async function submitRelay(options: SubmitRelayOptions): Promise<RelayRes
98
98
  throw new RelayError('Invalid type. Must be "withdraw" or "transfer"', 400);
99
99
  }
100
100
 
101
- if (pool !== 'eth' && pool !== 'usdc' && pool !== 'cbbtc') {
102
- throw new RelayError('Invalid pool. Must be "eth", "usdc", or "cbbtc"', 400);
101
+ if (pool !== 'eth' && pool !== 'usdc') {
102
+ throw new RelayError('Invalid pool. Must be "eth" or "usdc"', 400);
103
103
  }
104
104
 
105
105
  if (!proofArgs || !extData) {
package/src/types.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Supported tokens
7
7
  */
8
- export type Token = 'ETH' | 'USDC' | 'CBBTC';
8
+ export type Token = 'ETH' | 'USDC';
9
9
 
10
10
  /**
11
11
  * Encrypted message format (x25519-xsalsa20-poly1305)
@@ -27,9 +27,6 @@ export interface NetworkAddresses {
27
27
  usdcPool: `0x${string}`;
28
28
  usdcQueue: `0x${string}`;
29
29
  usdcToken: `0x${string}`;
30
- cbbtcPool: `0x${string}`;
31
- cbbtcQueue: `0x${string}`;
32
- cbbtcToken: `0x${string}`;
33
30
  chainId: number;
34
31
  relayUrl: string;
35
32
  }
@@ -121,7 +118,7 @@ export interface PrivateBalanceResult {
121
118
  /**
122
119
  * Pool type for relay operations
123
120
  */
124
- export type RelayPool = 'eth' | 'usdc' | 'cbbtc';
121
+ export type RelayPool = 'eth' | 'usdc';
125
122
 
126
123
  /**
127
124
  * Type of relay transaction