@veil-cash/sdk 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veil-cash/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.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
@@ -647,3 +647,175 @@ export const ERC20_ABI = [
647
647
  type: 'function',
648
648
  },
649
649
  ] as const;
650
+
651
+ /**
652
+ * Veil Forwarder Factory ABI
653
+ */
654
+ export const FORWARDER_FACTORY_ABI = [
655
+ {
656
+ inputs: [],
657
+ name: 'CONTRACT_VERSION',
658
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
659
+ stateMutability: 'view',
660
+ type: 'function',
661
+ },
662
+ {
663
+ inputs: [
664
+ { internalType: 'bytes32', name: '_salt', type: 'bytes32' },
665
+ { internalType: 'bytes', name: '_childDepositKey', type: 'bytes' },
666
+ { internalType: 'address', name: '_owner', type: 'address' },
667
+ ],
668
+ name: 'computeAddress',
669
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
670
+ stateMutability: 'view',
671
+ type: 'function',
672
+ },
673
+ {
674
+ inputs: [
675
+ { internalType: 'bytes32', name: '_salt', type: 'bytes32' },
676
+ { internalType: 'bytes', name: '_childDepositKey', type: 'bytes' },
677
+ { internalType: 'address', name: '_owner', type: 'address' },
678
+ ],
679
+ name: 'deploy',
680
+ outputs: [{ internalType: 'address', name: 'forwarder', type: 'address' }],
681
+ stateMutability: 'nonpayable',
682
+ type: 'function',
683
+ },
684
+ {
685
+ inputs: [],
686
+ name: 'relayer',
687
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
688
+ stateMutability: 'view',
689
+ type: 'function',
690
+ },
691
+ {
692
+ inputs: [],
693
+ name: 'veilEntry',
694
+ outputs: [{ internalType: 'address payable', name: '', type: 'address' }],
695
+ stateMutability: 'view',
696
+ type: 'function',
697
+ },
698
+ {
699
+ inputs: [],
700
+ name: 'usdc',
701
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
702
+ stateMutability: 'view',
703
+ type: 'function',
704
+ },
705
+ {
706
+ inputs: [],
707
+ name: 'owner',
708
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
709
+ stateMutability: 'view',
710
+ type: 'function',
711
+ },
712
+ ] as const;
713
+
714
+ /**
715
+ * Veil Forwarder ABI
716
+ */
717
+ export const FORWARDER_ABI = [
718
+ {
719
+ inputs: [],
720
+ name: 'CONTRACT_VERSION',
721
+ outputs: [{ internalType: 'string', name: '', type: 'string' }],
722
+ stateMutability: 'view',
723
+ type: 'function',
724
+ },
725
+ {
726
+ inputs: [
727
+ { internalType: 'address', name: '_token', type: 'address' },
728
+ { internalType: 'address', name: '_to', type: 'address' },
729
+ { internalType: 'uint256', name: '_amount', type: 'uint256' },
730
+ { internalType: 'uint256', name: '_nonce', type: 'uint256' },
731
+ { internalType: 'uint256', name: '_deadline', type: 'uint256' },
732
+ { internalType: 'bytes', name: '_signature', type: 'bytes' },
733
+ ],
734
+ name: 'withdraw',
735
+ outputs: [],
736
+ stateMutability: 'nonpayable',
737
+ type: 'function',
738
+ },
739
+ {
740
+ inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
741
+ name: 'usedNonces',
742
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
743
+ stateMutability: 'view',
744
+ type: 'function',
745
+ },
746
+ {
747
+ inputs: [],
748
+ name: 'owner',
749
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
750
+ stateMutability: 'view',
751
+ type: 'function',
752
+ },
753
+ {
754
+ inputs: [],
755
+ name: 'factory',
756
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
757
+ stateMutability: 'view',
758
+ type: 'function',
759
+ },
760
+ {
761
+ inputs: [],
762
+ name: 'childDepositKey',
763
+ outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
764
+ stateMutability: 'view',
765
+ type: 'function',
766
+ },
767
+ {
768
+ inputs: [],
769
+ name: 'entry',
770
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
771
+ stateMutability: 'view',
772
+ type: 'function',
773
+ },
774
+ {
775
+ inputs: [],
776
+ name: 'usdc',
777
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
778
+ stateMutability: 'view',
779
+ type: 'function',
780
+ },
781
+ {
782
+ inputs: [],
783
+ name: 'sweepETH',
784
+ outputs: [],
785
+ stateMutability: 'nonpayable',
786
+ type: 'function',
787
+ },
788
+ {
789
+ inputs: [],
790
+ name: 'sweepUSDC',
791
+ outputs: [],
792
+ stateMutability: 'nonpayable',
793
+ type: 'function',
794
+ },
795
+ {
796
+ inputs: [],
797
+ name: 'eip712Domain',
798
+ outputs: [
799
+ { internalType: 'bytes1', name: 'fields', type: 'bytes1' },
800
+ { internalType: 'string', name: 'name', type: 'string' },
801
+ { internalType: 'string', name: 'version', type: 'string' },
802
+ { internalType: 'uint256', name: 'chainId', type: 'uint256' },
803
+ { internalType: 'address', name: 'verifyingContract', type: 'address' },
804
+ { internalType: 'bytes32', name: 'salt', type: 'bytes32' },
805
+ { internalType: 'uint256[]', name: 'extensions', type: 'uint256[]' },
806
+ ],
807
+ stateMutability: 'view',
808
+ type: 'function',
809
+ },
810
+ { type: 'error', name: 'ZeroAddress', inputs: [] },
811
+ { type: 'error', name: 'ZeroAmount', inputs: [] },
812
+ { type: 'error', name: 'InvalidDepositKey', inputs: [] },
813
+ { type: 'error', name: 'NotRelayer', inputs: [] },
814
+ { type: 'error', name: 'NoETHBalance', inputs: [] },
815
+ { type: 'error', name: 'NoTokenBalance', inputs: [] },
816
+ { type: 'error', name: 'TokenApproveFailed', inputs: [] },
817
+ { type: 'error', name: 'ETHTransferFailed', inputs: [] },
818
+ { type: 'error', name: 'NonceUsed', inputs: [] },
819
+ { type: 'error', name: 'Unauthorized', inputs: [] },
820
+ { type: 'error', name: 'DeadlineExpired', inputs: [] },
821
+ ] as const;
package/src/addresses.ts CHANGED
@@ -14,10 +14,16 @@ export const ADDRESSES: NetworkAddresses = {
14
14
  usdcPool: '0x5c50d58E49C59d112680c187De2Bf989d2a91242',
15
15
  usdcQueue: '0x5530241b24504bF05C9a22e95A1F5458888e6a9B',
16
16
  usdcToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
17
+ forwarderFactory: '0x2848Fd62293A1ff3b4a897E9FcD0e5962dcc8101',
17
18
  chainId: 8453,
18
19
  relayUrl: 'https://veil-relay.up.railway.app',
19
20
  } as const;
20
21
 
22
+ /**
23
+ * Veil forwarder EIP-712 contract version
24
+ */
25
+ export const FORWARDER_CONTRACT_VERSION = '1' as const;
26
+
21
27
  /**
22
28
  * Pool configuration (decimals, symbols, etc.)
23
29
  */
@@ -76,6 +82,14 @@ export function getQueueAddress(
76
82
  }
77
83
  }
78
84
 
85
+ /**
86
+ * Get the forwarder factory contract address
87
+ * @returns Forwarder factory address for Base mainnet
88
+ */
89
+ export function getForwarderFactoryAddress(): `0x${string}` {
90
+ return getAddresses().forwarderFactory;
91
+ }
92
+
79
93
  /**
80
94
  * Get Relay URL
81
95
  * @returns Relay URL for Base mainnet
@@ -0,0 +1,354 @@
1
+ import { Command } from 'commander';
2
+ import { isAddress } from 'viem';
3
+ import {
4
+ buildSubaccountRecoveryTx,
5
+ deploySubaccountForwarder,
6
+ deriveSubaccountSlot,
7
+ getSubaccountStatus,
8
+ isSubaccountForwarderDeployed,
9
+ MAX_SUBACCOUNT_SLOTS,
10
+ sweepSubaccountForwarder,
11
+ } from '../../subaccount.js';
12
+ import { getConfig } from '../config.js';
13
+ import { CLIError, ErrorCode, handleCLIError } from '../errors.js';
14
+ import { printFields, printHeader, printJson, printLine, printList, printSection, txUrl } from '../output.js';
15
+ import { sendTransaction } from '../wallet.js';
16
+ import type { SubaccountAsset } from '../../types.js';
17
+
18
+ function parseSlotValue(raw: string): number {
19
+ const normalized = raw.trim();
20
+ if (!/^\d+$/.test(normalized)) {
21
+ throw new CLIError(ErrorCode.INVALID_SLOT, '--slot must be a non-negative integer');
22
+ }
23
+
24
+ const slot = Number(normalized);
25
+ if (slot >= MAX_SUBACCOUNT_SLOTS) {
26
+ throw new CLIError(
27
+ ErrorCode.INVALID_SLOT,
28
+ `--slot must be 0-${MAX_SUBACCOUNT_SLOTS - 1} (max ${MAX_SUBACCOUNT_SLOTS} subaccounts supported)`,
29
+ );
30
+ }
31
+
32
+ return slot;
33
+ }
34
+
35
+ function getRequiredVeilKey(): `0x${string}` {
36
+ const veilKey = process.env.VEIL_KEY;
37
+ if (!veilKey) {
38
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY required. Set VEIL_KEY env');
39
+ }
40
+ if (!/^0x[a-fA-F0-9]{64}$/.test(veilKey)) {
41
+ throw new CLIError(ErrorCode.VEIL_KEY_MISSING, 'VEIL_KEY must be a 0x-prefixed 32-byte hex string');
42
+ }
43
+ return veilKey as `0x${string}`;
44
+ }
45
+
46
+ function parseAsset(raw: string): SubaccountAsset {
47
+ const asset = raw.toLowerCase();
48
+ if (asset !== 'eth' && asset !== 'usdc') {
49
+ throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported asset: ${raw}. Supported: eth, usdc`);
50
+ }
51
+ return asset;
52
+ }
53
+
54
+ function printQueueHuman(
55
+ title: string,
56
+ queue: {
57
+ queueBalance: string;
58
+ pendingCount: number;
59
+ pendingDeposits: Array<{ nonce: string; amount: string; status: string }>;
60
+ },
61
+ ): void {
62
+ printSection(title);
63
+ printFields([
64
+ { label: 'Queue balance', value: queue.queueBalance },
65
+ { label: 'Pending', value: queue.pendingCount },
66
+ ]);
67
+
68
+ if (queue.pendingDeposits.length > 0) {
69
+ printList(
70
+ queue.pendingDeposits.map((deposit) => `nonce ${deposit.nonce}: ${deposit.amount} (${deposit.status})`),
71
+ );
72
+ }
73
+ }
74
+
75
+ export function createSubaccountCommand(): Command {
76
+ const subaccount = new Command('subaccount')
77
+ .description('Manage Veil subaccounts')
78
+ .addHelpText('after', `
79
+ Examples:
80
+ veil subaccount derive --slot 0
81
+ veil subaccount status --slot 0
82
+ veil subaccount deploy --slot 0
83
+ veil subaccount sweep --slot 0 --asset eth
84
+ veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
85
+ veil subaccount address --slot 0
86
+ `);
87
+
88
+ subaccount
89
+ .command('derive')
90
+ .description('Derive subaccount metadata for a slot')
91
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
92
+ .option('--json', 'Output as JSON')
93
+ .action(async (options) => {
94
+ try {
95
+ const rootPrivateKey = getRequiredVeilKey();
96
+ const rpcUrl = process.env.RPC_URL;
97
+ const slot = await deriveSubaccountSlot({
98
+ rootPrivateKey,
99
+ slot: options.slot,
100
+ rpcUrl,
101
+ });
102
+ const deployed = await isSubaccountForwarderDeployed({
103
+ forwarderAddress: slot.forwarderAddress,
104
+ rpcUrl,
105
+ });
106
+
107
+ const output = {
108
+ ...slot,
109
+ deployed,
110
+ };
111
+
112
+ if (options.json) {
113
+ printJson(output);
114
+ return;
115
+ }
116
+
117
+ printHeader(`Subaccount Slot ${slot.slot}`);
118
+ printFields([
119
+ { label: 'Child owner', value: slot.childOwner },
120
+ { label: 'Deposit key', value: slot.childDepositKey },
121
+ { label: 'Salt', value: slot.salt },
122
+ { label: 'Forwarder', value: slot.forwarderAddress },
123
+ { label: 'Deployed', value: deployed },
124
+ ]);
125
+ printLine();
126
+ } catch (error) {
127
+ handleCLIError(error);
128
+ }
129
+ });
130
+
131
+ subaccount
132
+ .command('status')
133
+ .description('Show subaccount deployment, balances, and queue state')
134
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
135
+ .option('--json', 'Output as JSON')
136
+ .action(async (options) => {
137
+ try {
138
+ const rootPrivateKey = getRequiredVeilKey();
139
+ const status = await getSubaccountStatus({
140
+ rootPrivateKey,
141
+ slot: options.slot,
142
+ rpcUrl: process.env.RPC_URL,
143
+ });
144
+
145
+ if (options.json) {
146
+ printJson(status);
147
+ return;
148
+ }
149
+
150
+ printHeader(`Subaccount Slot ${status.slot.slot}`);
151
+ printFields([
152
+ { label: 'Forwarder', value: status.slot.forwarderAddress },
153
+ { label: 'Child owner', value: status.slot.childOwner },
154
+ { label: 'Deposit key', value: status.slot.childDepositKey },
155
+ { label: 'Salt', value: status.slot.salt },
156
+ { label: 'Deployed', value: status.deployed },
157
+ ]);
158
+
159
+ printSection('Forwarder Balances');
160
+ printFields([
161
+ { label: 'ETH', value: `${status.balances.eth.balance} ETH` },
162
+ { label: 'USDC', value: `${status.balances.usdc.balance} USDC` },
163
+ ]);
164
+
165
+ printQueueHuman('ETH Queue', status.queues.eth);
166
+ printQueueHuman('USDC Queue', status.queues.usdc);
167
+ printLine();
168
+ } catch (error) {
169
+ handleCLIError(error);
170
+ }
171
+ });
172
+
173
+ subaccount
174
+ .command('deploy')
175
+ .description('Deploy a subaccount forwarder through the relay')
176
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
177
+ .option('--json', 'Output as JSON')
178
+ .action(async (options) => {
179
+ try {
180
+ const rootPrivateKey = getRequiredVeilKey();
181
+ const slot = await deriveSubaccountSlot({
182
+ rootPrivateKey,
183
+ slot: options.slot,
184
+ rpcUrl: process.env.RPC_URL,
185
+ });
186
+ const result = await deploySubaccountForwarder({
187
+ rootPrivateKey,
188
+ slot: options.slot,
189
+ rpcUrl: process.env.RPC_URL,
190
+ relayUrl: process.env.RELAY_URL,
191
+ });
192
+
193
+ const output = {
194
+ ...result,
195
+ slot: options.slot,
196
+ forwarderAddress: slot.forwarderAddress,
197
+ };
198
+
199
+ if (options.json) {
200
+ printJson(output);
201
+ return;
202
+ }
203
+
204
+ printHeader('Subaccount Deploy Submitted');
205
+ printFields([
206
+ { label: 'Slot', value: options.slot },
207
+ { label: 'Forwarder', value: slot.forwarderAddress },
208
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
209
+ { label: 'Block', value: result.blockNumber },
210
+ ]);
211
+ printLine();
212
+ } catch (error) {
213
+ handleCLIError(error);
214
+ }
215
+ });
216
+
217
+ subaccount
218
+ .command('sweep')
219
+ .description('Sweep ETH or USDC from a subaccount forwarder through the relay')
220
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
221
+ .requiredOption('--asset <asset>', 'Asset to sweep (eth or usdc)', parseAsset)
222
+ .option('--json', 'Output as JSON')
223
+ .action(async (options) => {
224
+ try {
225
+ const rootPrivateKey = getRequiredVeilKey();
226
+ const slot = await deriveSubaccountSlot({
227
+ rootPrivateKey,
228
+ slot: options.slot,
229
+ rpcUrl: process.env.RPC_URL,
230
+ });
231
+ const result = await sweepSubaccountForwarder({
232
+ forwarderAddress: slot.forwarderAddress,
233
+ asset: options.asset,
234
+ relayUrl: process.env.RELAY_URL,
235
+ });
236
+
237
+ const output = {
238
+ ...result,
239
+ slot: options.slot,
240
+ asset: options.asset,
241
+ forwarderAddress: slot.forwarderAddress,
242
+ };
243
+
244
+ if (options.json) {
245
+ printJson(output);
246
+ return;
247
+ }
248
+
249
+ printHeader('Subaccount Sweep Submitted');
250
+ printFields([
251
+ { label: 'Slot', value: options.slot },
252
+ { label: 'Asset', value: options.asset.toUpperCase() },
253
+ { label: 'Forwarder', value: slot.forwarderAddress },
254
+ { label: 'Transaction', value: txUrl(result.transactionHash) },
255
+ { label: 'Block', value: result.blockNumber },
256
+ ]);
257
+ printLine();
258
+ } catch (error) {
259
+ handleCLIError(error);
260
+ }
261
+ });
262
+
263
+ subaccount
264
+ .command('recover')
265
+ .description('Recover assets sitting on the subaccount forwarder with a direct withdraw transaction')
266
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
267
+ .requiredOption('--asset <asset>', 'Asset to recover (eth or usdc)', parseAsset)
268
+ .requiredOption('--to <address>', 'Recipient address')
269
+ .requiredOption('--amount <value>', 'Amount to recover')
270
+ .option('--json', 'Output as JSON')
271
+ .action(async (options) => {
272
+ try {
273
+ const rootPrivateKey = getRequiredVeilKey();
274
+ if (!isAddress(options.to)) {
275
+ throw new CLIError(ErrorCode.INVALID_ADDRESS, `Invalid recipient address: ${options.to}`);
276
+ }
277
+ const config = getConfig({});
278
+ const recovery = await buildSubaccountRecoveryTx({
279
+ rootPrivateKey,
280
+ slot: options.slot,
281
+ asset: options.asset,
282
+ to: options.to as `0x${string}`,
283
+ amount: options.amount,
284
+ rpcUrl: process.env.RPC_URL,
285
+ });
286
+ const result = await sendTransaction(config, recovery.transaction);
287
+
288
+ const output = {
289
+ success: result.receipt.status === 'success',
290
+ slot: options.slot,
291
+ asset: recovery.asset,
292
+ amount: recovery.amount,
293
+ amountWei: recovery.amountWei,
294
+ forwarderAddress: recovery.forwarderAddress,
295
+ recipient: recovery.recipient,
296
+ nonce: recovery.nonce,
297
+ deadline: recovery.deadline,
298
+ signature: recovery.signature,
299
+ transactionHash: result.hash,
300
+ blockNumber: result.receipt.blockNumber.toString(),
301
+ };
302
+
303
+ if (options.json) {
304
+ printJson(output);
305
+ return;
306
+ }
307
+
308
+ printHeader('Subaccount Recovery Submitted');
309
+ printFields([
310
+ { label: 'Slot', value: options.slot },
311
+ { label: 'Asset', value: recovery.asset.toUpperCase() },
312
+ { label: 'Amount', value: recovery.amount },
313
+ { label: 'Recipient', value: recovery.recipient },
314
+ { label: 'Forwarder', value: recovery.forwarderAddress },
315
+ { label: 'Nonce', value: recovery.nonce },
316
+ { label: 'Transaction', value: txUrl(result.hash) },
317
+ { label: 'Block', value: result.receipt.blockNumber },
318
+ ]);
319
+ printLine();
320
+ } catch (error) {
321
+ handleCLIError(error);
322
+ }
323
+ });
324
+
325
+ subaccount
326
+ .command('address')
327
+ .description('Print the predicted forwarder address for a subaccount slot')
328
+ .requiredOption('--slot <n>', 'Subaccount slot', parseSlotValue)
329
+ .option('--json', 'Output as JSON')
330
+ .action(async (options) => {
331
+ try {
332
+ const rootPrivateKey = getRequiredVeilKey();
333
+ const slot = await deriveSubaccountSlot({
334
+ rootPrivateKey,
335
+ slot: options.slot,
336
+ rpcUrl: process.env.RPC_URL,
337
+ });
338
+
339
+ if (options.json) {
340
+ printJson({
341
+ slot: options.slot,
342
+ forwarderAddress: slot.forwarderAddress,
343
+ });
344
+ return;
345
+ }
346
+
347
+ printLine(slot.forwarderAddress);
348
+ } catch (error) {
349
+ handleCLIError(error);
350
+ }
351
+ });
352
+
353
+ return subaccount;
354
+ }
package/src/cli/errors.ts CHANGED
@@ -11,6 +11,7 @@ export const ErrorCode = {
11
11
  DEPOSIT_KEY_MISSING: 'DEPOSIT_KEY_MISSING',
12
12
  CONFIG_CONFLICT: 'CONFIG_CONFLICT',
13
13
  INVALID_ADDRESS: 'INVALID_ADDRESS',
14
+ INVALID_SLOT: 'INVALID_SLOT',
14
15
  INVALID_AMOUNT: 'INVALID_AMOUNT',
15
16
  INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
16
17
  USER_NOT_REGISTERED: 'USER_NOT_REGISTERED',
@@ -58,6 +59,9 @@ function inferErrorCode(message: string): ErrorCodeType {
58
59
  if (msg.includes('invalid') && msg.includes('address')) {
59
60
  return ErrorCode.INVALID_ADDRESS;
60
61
  }
62
+ if (msg.includes('invalid') && msg.includes('slot')) {
63
+ return ErrorCode.INVALID_SLOT;
64
+ }
61
65
  if (msg.includes('insufficient balance') || msg.includes('not enough')) {
62
66
  return ErrorCode.INSUFFICIENT_BALANCE;
63
67
  }
package/src/cli/index.ts CHANGED
@@ -13,6 +13,7 @@
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)
16
+ * veil subaccount status --slot 0 # Check subaccount status
16
17
  */
17
18
 
18
19
  import { Command } from 'commander';
@@ -27,6 +28,7 @@ import { createPrivateBalanceCommand } from './commands/private-balance.js';
27
28
  import { createWithdrawCommand } from './commands/withdraw.js';
28
29
  import { createTransferCommand, createMergeCommand } from './commands/transfer.js';
29
30
  import { createStatusCommand } from './commands/status.js';
31
+ import { createSubaccountCommand } from './commands/subaccount.js';
30
32
 
31
33
  // Load environment variables
32
34
  loadEnv();
@@ -36,13 +38,14 @@ const program = new Command();
36
38
  program
37
39
  .name('veil')
38
40
  .description('CLI for Veil Cash privacy pools on Base')
39
- .version('0.5.0')
41
+ .version('0.6.0')
40
42
  .addHelpText('after', `
41
43
  Getting started:
42
44
  veil init
43
45
  veil register
44
46
  veil deposit ETH 0.1
45
47
  veil balance
48
+ veil subaccount status --slot 0
46
49
  `);
47
50
 
48
51
  // Add commands
@@ -57,6 +60,7 @@ program.addCommand(createWithdrawCommand());
57
60
  program.addCommand(createTransferCommand());
58
61
  program.addCommand(createMergeCommand());
59
62
  program.addCommand(createStatusCommand());
63
+ program.addCommand(createSubaccountCommand());
60
64
 
61
65
  const knownTopLevelCommands = new Set([
62
66
  ...program.commands.map((command) => command.name()),
package/src/cli/wallet.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { privateKeyToAccount } from 'viem/accounts';
15
15
  import { base } from 'viem/chains';
16
16
  import type { TransactionData } from '../types.js';
17
- import { ENTRY_ABI, ERC20_ABI } from '../abi.js';
17
+ import { ENTRY_ABI, ERC20_ABI, FORWARDER_ABI } from '../abi.js';
18
18
  import { getAddresses, POOL_CONFIG, ADDRESSES } from '../addresses.js';
19
19
 
20
20
  export interface WalletConfig {
@@ -118,7 +118,7 @@ function decodeCustomError(error: unknown): string | null {
118
118
 
119
119
  if (possibleData && typeof possibleData === 'string' && possibleData.startsWith('0x')) {
120
120
  try {
121
- for (const abi of [ENTRY_ABI] as const) {
121
+ for (const abi of [ENTRY_ABI, FORWARDER_ABI] as const) {
122
122
  try {
123
123
  const decoded = decodeErrorResult({
124
124
  abi,