@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/README.md +31 -2
- package/SDK.md +55 -1
- package/dist/cli/index.cjs +806 -22
- package/dist/index.cjs +554 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +486 -1
- package/dist/index.d.ts +486 -1
- package/dist/index.js +537 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/abi.ts +172 -0
- package/src/addresses.ts +14 -0
- package/src/cli/commands/subaccount.ts +354 -0
- package/src/cli/errors.ts +4 -0
- package/src/cli/index.ts +5 -1
- package/src/cli/wallet.ts +2 -2
- package/src/index.ts +35 -0
- package/src/relay.ts +36 -24
- package/src/subaccount.ts +476 -0
- package/src/types.ts +134 -0
package/package.json
CHANGED
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.
|
|
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,
|