@veil-cash/sdk 0.6.0 → 0.6.2
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 +51 -38
- package/SDK.md +20 -2
- package/dist/cli/index.cjs +257 -13
- package/dist/index.cjs +185 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -2
- package/dist/index.d.ts +84 -2
- package/dist/index.js +184 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/veil/SKILL.md +93 -3
- package/skills/veil/reference.md +91 -2
- package/src/cli/commands/subaccount.ts +90 -9
- package/src/cli/index.ts +1 -1
- package/src/index.ts +6 -0
- package/src/relay.ts +10 -1
- package/src/subaccount.ts +252 -8
- package/src/types.ts +56 -0
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @veil-cash/sdk
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
[
|
|
5
|
-
[
|
|
3
|
+
[npm version](https://www.npmjs.com/package/@veil-cash/sdk)
|
|
4
|
+
[npm downloads](https://www.npmjs.com/package/@veil-cash/sdk)
|
|
5
|
+
[license](https://github.com/veildotcash/veildotcash-sdk/blob/main/LICENSE)
|
|
6
6
|
|
|
7
7
|
SDK and CLI for interacting with [Veil Cash](https://veil.cash) privacy pools on Base.
|
|
8
8
|
|
|
9
9
|
Generate keypairs, register, deposit, withdraw, transfer, and merge ETH and USDC privately.
|
|
10
10
|
|
|
11
|
+
`0.6.2` adds `mergeSubaccount` — transfer a subaccount's private pool balance back to the main wallet via a ZK proof. Also adds `veil subaccount merge` CLI command.
|
|
12
|
+
|
|
11
13
|
`0.6.0` adds SDK-first subaccount support for deterministic slot derivation, forwarder status, relay-backed deploy/sweep, and direct recovery.
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
@@ -21,13 +23,14 @@ pnpm add @veil-cash/sdk
|
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
For global CLI access:
|
|
26
|
+
|
|
24
27
|
```bash
|
|
25
28
|
npm install -g @veil-cash/sdk
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
## Agent Skill
|
|
29
32
|
|
|
30
|
-
This repo includes a Veil agent skill at [
|
|
33
|
+
This repo includes a Veil agent skill at [skills/veil/SKILL.md](./skills/veil/SKILL.md).
|
|
31
34
|
|
|
32
35
|
Quick mapping:
|
|
33
36
|
|
|
@@ -35,16 +38,18 @@ Quick mapping:
|
|
|
35
38
|
- CLI binary: `veil`
|
|
36
39
|
- agent skill file: `skills/veil/SKILL.md`
|
|
37
40
|
|
|
38
|
-
If you are pointing an agent at this repo, send it to [
|
|
41
|
+
If you are pointing an agent at this repo, send it to [skills/veil/SKILL.md](./skills/veil/SKILL.md) for the canonical CLI workflow.
|
|
39
42
|
|
|
40
43
|
If you install the npm package, the published package also includes the `skills/` directory so the same skill can be discovered from the installed package contents.
|
|
41
44
|
|
|
42
45
|
## Supported Assets
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
|
47
|
+
|
|
48
|
+
| Asset | Decimals | Token Contract |
|
|
49
|
+
| ----- | -------- | -------------------------------------------- |
|
|
50
|
+
| ETH | 18 | Native ETH (via WETH) |
|
|
51
|
+
| USDC | 6 | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
|
|
52
|
+
|
|
48
53
|
|
|
49
54
|
## CLI Quick Start
|
|
50
55
|
|
|
@@ -83,6 +88,7 @@ veil subaccount derive --slot 0
|
|
|
83
88
|
veil subaccount status --slot 0
|
|
84
89
|
veil subaccount deploy --slot 0
|
|
85
90
|
veil subaccount sweep --slot 0 --asset eth
|
|
91
|
+
veil subaccount merge --slot 0 --pool eth
|
|
86
92
|
veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
|
|
87
93
|
|
|
88
94
|
# 9. Use JSON or unsigned modes when you need automation
|
|
@@ -204,7 +210,7 @@ Subaccounts are deterministic child slots derived from your main `VEIL_KEY`:
|
|
|
204
210
|
|
|
205
211
|
`root key -> slot -> child key -> child deposit key -> forwarder`
|
|
206
212
|
|
|
207
|
-
Base mainnet only. Deploy and sweep are relay-backed.
|
|
213
|
+
Base mainnet only. Deploy and sweep are relay-backed. Merge transfers the subaccount's private pool balance back to the main wallet via a ZK proof. Status reports forwarder wallet balances, private pool balances, and queue state for the child slot. Recovery is for assets still sitting on the forwarder after refund or rejection, and is submitted directly on-chain by your CLI gas payer.
|
|
208
214
|
|
|
209
215
|
```bash
|
|
210
216
|
veil subaccount derive --slot 0
|
|
@@ -212,6 +218,7 @@ veil subaccount status --slot 0
|
|
|
212
218
|
veil subaccount address --slot 0
|
|
213
219
|
veil subaccount deploy --slot 0
|
|
214
220
|
veil subaccount sweep --slot 0 --asset eth
|
|
221
|
+
veil subaccount merge --slot 0 --pool eth
|
|
215
222
|
veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
|
|
216
223
|
veil subaccount status --slot 0 --json
|
|
217
224
|
```
|
|
@@ -220,21 +227,25 @@ veil subaccount status --slot 0 --json
|
|
|
220
227
|
|
|
221
228
|
The CLI uses two config files:
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
|
226
|
-
| `.env` |
|
|
230
|
+
|
|
231
|
+
| File | Purpose |
|
|
232
|
+
| ----------- | ---------------------------------------------------------------------------- |
|
|
233
|
+
| `.env.veil` | Veil keypair (VEIL_KEY, DEPOSIT_KEY) - created by `veil init` |
|
|
234
|
+
| `.env` | Wallet config (WALLET_KEY or SIGNER_ADDRESS, RPC_URL) - your existing config |
|
|
235
|
+
|
|
227
236
|
|
|
228
237
|
### Variables
|
|
229
238
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
239
|
+
|
|
240
|
+
| Variable | Description |
|
|
241
|
+
| ---------------- | ---------------------------------------------------------------------------------------------- |
|
|
242
|
+
| `VEIL_KEY` | Your Veil private key (for ZK proofs, withdrawals, transfers) |
|
|
243
|
+
| `DEPOSIT_KEY` | Your Veil deposit key (public, for register/deposit) |
|
|
244
|
+
| `WALLET_KEY` | Ethereum wallet private key (for signing transactions) |
|
|
245
|
+
| `SIGNER_ADDRESS` | Ethereum address for unsigned/query flows when signing is handled externally |
|
|
246
|
+
| `RPC_URL` | Base RPC URL (optional, defaults to public RPC) |
|
|
247
|
+
| `RELAY_URL` | Override relay base URL for relayed CLI operations, subaccount deploy/sweep, and status checks |
|
|
248
|
+
|
|
238
249
|
|
|
239
250
|
`WALLET_KEY` and `SIGNER_ADDRESS` are mutually exclusive. Use `WALLET_KEY` for commands that sign transactions, and `SIGNER_ADDRESS` for address-only agent flows like `status`, `balance`, and `register --unsigned`.
|
|
240
251
|
|
|
@@ -252,22 +263,24 @@ Commands print human-readable success output by default. Errors are standardized
|
|
|
252
263
|
|
|
253
264
|
### Error Codes
|
|
254
265
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
266
|
+
|
|
267
|
+
| Code | Description |
|
|
268
|
+
| ---------------------- | --------------------------------- |
|
|
269
|
+
| `VEIL_KEY_MISSING` | VEIL_KEY not provided |
|
|
270
|
+
| `WALLET_KEY_MISSING` | WALLET_KEY not provided |
|
|
271
|
+
| `DEPOSIT_KEY_MISSING` | DEPOSIT_KEY not provided |
|
|
272
|
+
| `CONFIG_CONFLICT` | Conflicting CLI env vars provided |
|
|
273
|
+
| `INVALID_ADDRESS` | Invalid Ethereum address format |
|
|
274
|
+
| `INVALID_SLOT` | Invalid subaccount slot format |
|
|
275
|
+
| `INVALID_AMOUNT` | Invalid or below minimum amount |
|
|
276
|
+
| `INSUFFICIENT_BALANCE` | Not enough ETH balance |
|
|
277
|
+
| `USER_NOT_REGISTERED` | Recipient not registered in Veil |
|
|
278
|
+
| `NO_UTXOS` | No unspent UTXOs available |
|
|
279
|
+
| `RELAY_ERROR` | Error from relayer service |
|
|
280
|
+
| `RPC_ERROR` | RPC/network error |
|
|
281
|
+
| `CONTRACT_ERROR` | Smart contract reverted |
|
|
282
|
+
| `UNKNOWN_ERROR` | Unexpected error |
|
|
283
|
+
|
|
271
284
|
|
|
272
285
|
## SDK Docs
|
|
273
286
|
|
package/SDK.md
CHANGED
|
@@ -10,7 +10,7 @@ If you are looking for the CLI first-run flow, go back to the main [README](./RE
|
|
|
10
10
|
import {
|
|
11
11
|
Keypair, buildRegisterTx, buildDepositETHTx,
|
|
12
12
|
buildDepositUSDCTx, buildApproveUSDCTx,
|
|
13
|
-
withdraw, transfer, getSubaccountStatus,
|
|
13
|
+
withdraw, transfer, getSubaccountStatus, mergeSubaccount,
|
|
14
14
|
} from '@veil-cash/sdk';
|
|
15
15
|
import { createWalletClient, http } from 'viem';
|
|
16
16
|
import { base } from 'viem/chains';
|
|
@@ -203,14 +203,16 @@ Subaccounts are deterministic child slots derived from your main Veil key:
|
|
|
203
203
|
|
|
204
204
|
`root key -> slot -> child key -> child deposit key -> forwarder`
|
|
205
205
|
|
|
206
|
-
Base mainnet only. Status shows the forwarder wallet and queue state
|
|
206
|
+
Base mainnet only. Status shows the child slot's forwarder wallet balances, private pool balances, and queue state. Deploy and sweep are relay-backed. Merge transfers the subaccount's private pool balance back to the main wallet via a ZK proof. Recovery signs a forwarder withdraw request with the child key and returns a direct transaction for your gas payer to submit.
|
|
207
207
|
|
|
208
208
|
```typescript
|
|
209
209
|
import {
|
|
210
210
|
deriveSubaccountSlot,
|
|
211
|
+
getSubaccountPrivateBalance,
|
|
211
212
|
getSubaccountStatus,
|
|
212
213
|
deploySubaccountForwarder,
|
|
213
214
|
sweepSubaccountForwarder,
|
|
215
|
+
mergeSubaccount,
|
|
214
216
|
buildSubaccountRecoveryTx,
|
|
215
217
|
} from '@veil-cash/sdk';
|
|
216
218
|
|
|
@@ -223,6 +225,14 @@ const status = await getSubaccountStatus({
|
|
|
223
225
|
rootPrivateKey: veilKey,
|
|
224
226
|
slot: 0,
|
|
225
227
|
});
|
|
228
|
+
// status.privateBalances.eth.privateBalance, status.privateBalances.usdc.privateBalance
|
|
229
|
+
|
|
230
|
+
const privateBalance = await getSubaccountPrivateBalance({
|
|
231
|
+
rootPrivateKey: veilKey,
|
|
232
|
+
slot: 0,
|
|
233
|
+
pool: 'eth',
|
|
234
|
+
});
|
|
235
|
+
// privateBalance.privateBalance, privateBalance.unspentCount
|
|
226
236
|
|
|
227
237
|
await deploySubaccountForwarder({
|
|
228
238
|
rootPrivateKey: veilKey,
|
|
@@ -234,6 +244,14 @@ await sweepSubaccountForwarder({
|
|
|
234
244
|
asset: 'eth',
|
|
235
245
|
});
|
|
236
246
|
|
|
247
|
+
// Merge subaccount's private pool balance back to the main wallet
|
|
248
|
+
const mergeResult = await mergeSubaccount({
|
|
249
|
+
rootPrivateKey: veilKey,
|
|
250
|
+
slot: 0,
|
|
251
|
+
pool: 'eth', // 'eth' | 'usdc' (default: 'eth')
|
|
252
|
+
});
|
|
253
|
+
// mergeResult.amount, mergeResult.transactionHash
|
|
254
|
+
|
|
237
255
|
const recovery = await buildSubaccountRecoveryTx({
|
|
238
256
|
rootPrivateKey: veilKey,
|
|
239
257
|
slot: 0,
|
package/dist/cli/index.cjs
CHANGED
|
@@ -6682,7 +6682,16 @@ async function postRelayJson(endpoint, body, relayUrl) {
|
|
|
6682
6682
|
},
|
|
6683
6683
|
body: JSON.stringify(body)
|
|
6684
6684
|
});
|
|
6685
|
-
const
|
|
6685
|
+
const text = await response.text();
|
|
6686
|
+
let data;
|
|
6687
|
+
try {
|
|
6688
|
+
data = JSON.parse(text);
|
|
6689
|
+
} catch {
|
|
6690
|
+
throw new RelayError(
|
|
6691
|
+
`Relay returned non-JSON response (HTTP ${response.status})`,
|
|
6692
|
+
response.status
|
|
6693
|
+
);
|
|
6694
|
+
}
|
|
6686
6695
|
if (!response.ok) {
|
|
6687
6696
|
const errorData = data;
|
|
6688
6697
|
throw new RelayError(
|
|
@@ -7589,11 +7598,12 @@ function deriveSubaccountChildDepositKey(childPrivateKey) {
|
|
|
7589
7598
|
}
|
|
7590
7599
|
async function predictSubaccountForwarder(options) {
|
|
7591
7600
|
const publicClient = createBaseClient(options.rpcUrl);
|
|
7601
|
+
const depositKeyBytes = options.childDepositKey.startsWith("0x") ? options.childDepositKey : `0x${options.childDepositKey}`;
|
|
7592
7602
|
return publicClient.readContract({
|
|
7593
7603
|
abi: FORWARDER_FACTORY_ABI,
|
|
7594
7604
|
address: getForwarderFactoryAddress(),
|
|
7595
7605
|
functionName: "computeAddress",
|
|
7596
|
-
args: [options.salt,
|
|
7606
|
+
args: [options.salt, depositKeyBytes, options.childOwner]
|
|
7597
7607
|
});
|
|
7598
7608
|
}
|
|
7599
7609
|
async function deriveSubaccountSlot(options) {
|
|
@@ -7630,7 +7640,7 @@ async function deploySubaccountForwarder(options) {
|
|
|
7630
7640
|
slot: options.slot,
|
|
7631
7641
|
rpcUrl: options.rpcUrl
|
|
7632
7642
|
});
|
|
7633
|
-
|
|
7643
|
+
const result = await postRelayJson(
|
|
7634
7644
|
"/stealth/deploy",
|
|
7635
7645
|
{
|
|
7636
7646
|
salt: slot.salt,
|
|
@@ -7640,6 +7650,7 @@ async function deploySubaccountForwarder(options) {
|
|
|
7640
7650
|
},
|
|
7641
7651
|
options.relayUrl
|
|
7642
7652
|
);
|
|
7653
|
+
return { ...result, slot };
|
|
7643
7654
|
}
|
|
7644
7655
|
async function sweepSubaccountForwarder(options) {
|
|
7645
7656
|
const asset = normalizeAsset(options.asset);
|
|
@@ -7664,11 +7675,32 @@ function toQueueStatus(asset, result) {
|
|
|
7664
7675
|
pendingDeposits: result.pendingDeposits
|
|
7665
7676
|
};
|
|
7666
7677
|
}
|
|
7678
|
+
function toPrivateBalanceStatus(result) {
|
|
7679
|
+
return {
|
|
7680
|
+
privateBalance: result.privateBalance,
|
|
7681
|
+
privateBalanceWei: result.privateBalanceWei,
|
|
7682
|
+
utxoCount: result.utxoCount,
|
|
7683
|
+
spentCount: result.spentCount,
|
|
7684
|
+
unspentCount: result.unspentCount
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
async function getSubaccountPrivateBalance(options) {
|
|
7688
|
+
const normalizedSlot = normalizeSlot(options.slot);
|
|
7689
|
+
assertPrivateKey(options.rootPrivateKey, "rootPrivateKey");
|
|
7690
|
+
const childPrivateKey = deriveSubaccountChildPrivateKey(options.rootPrivateKey, normalizedSlot);
|
|
7691
|
+
const childKeypair = new Keypair(childPrivateKey);
|
|
7692
|
+
return getPrivateBalance({
|
|
7693
|
+
keypair: childKeypair,
|
|
7694
|
+
pool: options.pool,
|
|
7695
|
+
rpcUrl: options.rpcUrl,
|
|
7696
|
+
onProgress: options.onProgress
|
|
7697
|
+
});
|
|
7698
|
+
}
|
|
7667
7699
|
async function getSubaccountStatus(options) {
|
|
7668
7700
|
const slot = await deriveSubaccountSlot(options);
|
|
7669
7701
|
const publicClient = createBaseClient(options.rpcUrl);
|
|
7670
7702
|
const addresses = getAddresses();
|
|
7671
|
-
const [deployed, ethWei, usdcWei, ethQueue, usdcQueue] = await Promise.all([
|
|
7703
|
+
const [deployed, ethWei, usdcWei, ethQueue, usdcQueue, ethPrivate, usdcPrivate] = await Promise.all([
|
|
7672
7704
|
isSubaccountForwarderDeployed({
|
|
7673
7705
|
forwarderAddress: slot.forwarderAddress,
|
|
7674
7706
|
rpcUrl: options.rpcUrl
|
|
@@ -7689,6 +7721,18 @@ async function getSubaccountStatus(options) {
|
|
|
7689
7721
|
address: slot.forwarderAddress,
|
|
7690
7722
|
pool: "usdc",
|
|
7691
7723
|
rpcUrl: options.rpcUrl
|
|
7724
|
+
}),
|
|
7725
|
+
getSubaccountPrivateBalance({
|
|
7726
|
+
rootPrivateKey: options.rootPrivateKey,
|
|
7727
|
+
slot: options.slot,
|
|
7728
|
+
pool: "eth",
|
|
7729
|
+
rpcUrl: options.rpcUrl
|
|
7730
|
+
}),
|
|
7731
|
+
getSubaccountPrivateBalance({
|
|
7732
|
+
rootPrivateKey: options.rootPrivateKey,
|
|
7733
|
+
slot: options.slot,
|
|
7734
|
+
pool: "usdc",
|
|
7735
|
+
rpcUrl: options.rpcUrl
|
|
7692
7736
|
})
|
|
7693
7737
|
]);
|
|
7694
7738
|
return {
|
|
@@ -7704,6 +7748,10 @@ async function getSubaccountStatus(options) {
|
|
|
7704
7748
|
balanceWei: usdcWei.toString()
|
|
7705
7749
|
}
|
|
7706
7750
|
},
|
|
7751
|
+
privateBalances: {
|
|
7752
|
+
eth: toPrivateBalanceStatus(ethPrivate),
|
|
7753
|
+
usdc: toPrivateBalanceStatus(usdcPrivate)
|
|
7754
|
+
},
|
|
7707
7755
|
queues: {
|
|
7708
7756
|
eth: toQueueStatus("eth", ethQueue),
|
|
7709
7757
|
usdc: toQueueStatus("usdc", usdcQueue)
|
|
@@ -7850,6 +7898,137 @@ async function buildSubaccountRecoveryTx(options) {
|
|
|
7850
7898
|
signature
|
|
7851
7899
|
};
|
|
7852
7900
|
}
|
|
7901
|
+
async function mergeSubaccount(options) {
|
|
7902
|
+
const {
|
|
7903
|
+
rootPrivateKey,
|
|
7904
|
+
slot,
|
|
7905
|
+
pool = "eth",
|
|
7906
|
+
rpcUrl,
|
|
7907
|
+
relayUrl,
|
|
7908
|
+
onProgress
|
|
7909
|
+
} = options;
|
|
7910
|
+
const normalizedSlot = normalizeSlot(slot);
|
|
7911
|
+
assertPrivateKey(rootPrivateKey, "rootPrivateKey");
|
|
7912
|
+
const poolConfig = POOL_CONFIG[pool];
|
|
7913
|
+
const poolAddress = getPoolAddress(pool);
|
|
7914
|
+
const childPrivateKey = deriveSubaccountChildPrivateKey(rootPrivateKey, normalizedSlot);
|
|
7915
|
+
const childKeypair = new Keypair(childPrivateKey);
|
|
7916
|
+
const parentKeypair = new Keypair(rootPrivateKey);
|
|
7917
|
+
onProgress?.("Fetching subaccount balance...");
|
|
7918
|
+
const balanceResult = await getPrivateBalance({
|
|
7919
|
+
keypair: childKeypair,
|
|
7920
|
+
pool,
|
|
7921
|
+
rpcUrl,
|
|
7922
|
+
onProgress
|
|
7923
|
+
});
|
|
7924
|
+
const unspentUtxoInfos = balanceResult.utxos.filter((u) => !u.isSpent);
|
|
7925
|
+
if (unspentUtxoInfos.length === 0) {
|
|
7926
|
+
throw new Error("Subaccount has no unspent UTXOs to merge");
|
|
7927
|
+
}
|
|
7928
|
+
if (unspentUtxoInfos.length > 16) {
|
|
7929
|
+
throw new Error(
|
|
7930
|
+
`Subaccount has ${unspentUtxoInfos.length} unspent UTXOs which exceeds the 16-input circuit limit. Consolidate UTXOs on the subaccount first before merging.`
|
|
7931
|
+
);
|
|
7932
|
+
}
|
|
7933
|
+
onProgress?.("Preparing UTXOs...");
|
|
7934
|
+
const publicClient = viem.createPublicClient({
|
|
7935
|
+
chain: chains.base,
|
|
7936
|
+
transport: viem.http(rpcUrl)
|
|
7937
|
+
});
|
|
7938
|
+
const utxos = [];
|
|
7939
|
+
for (const utxoInfo of unspentUtxoInfos) {
|
|
7940
|
+
const encryptedOutputs = await publicClient.readContract({
|
|
7941
|
+
address: poolAddress,
|
|
7942
|
+
abi: POOL_ABI,
|
|
7943
|
+
functionName: "getEncryptedOutputs",
|
|
7944
|
+
args: [BigInt(utxoInfo.index), BigInt(utxoInfo.index + 1)]
|
|
7945
|
+
});
|
|
7946
|
+
if (encryptedOutputs.length > 0) {
|
|
7947
|
+
try {
|
|
7948
|
+
const utxo = Utxo.decrypt(encryptedOutputs[0], childKeypair);
|
|
7949
|
+
utxo.index = utxoInfo.index;
|
|
7950
|
+
utxos.push(utxo);
|
|
7951
|
+
} catch {
|
|
7952
|
+
}
|
|
7953
|
+
}
|
|
7954
|
+
}
|
|
7955
|
+
if (utxos.length === 0) {
|
|
7956
|
+
throw new Error("Failed to decrypt subaccount UTXOs");
|
|
7957
|
+
}
|
|
7958
|
+
onProgress?.("Selecting UTXOs...");
|
|
7959
|
+
const amount = balanceResult.privateBalance;
|
|
7960
|
+
const { selectedUtxos, changeAmount } = selectUtxosForWithdraw(
|
|
7961
|
+
utxos,
|
|
7962
|
+
amount,
|
|
7963
|
+
poolConfig.decimals
|
|
7964
|
+
);
|
|
7965
|
+
const outputs = [];
|
|
7966
|
+
const mergeWei = viem.parseUnits(amount, poolConfig.decimals);
|
|
7967
|
+
outputs.push(new Utxo({ amount: mergeWei, keypair: parentKeypair }));
|
|
7968
|
+
if (changeAmount > 0n) {
|
|
7969
|
+
outputs.push(new Utxo({ amount: changeAmount, keypair: parentKeypair }));
|
|
7970
|
+
}
|
|
7971
|
+
onProgress?.("Fetching commitments...");
|
|
7972
|
+
const nextIndex = await publicClient.readContract({
|
|
7973
|
+
address: poolAddress,
|
|
7974
|
+
abi: POOL_ABI,
|
|
7975
|
+
functionName: "nextIndex"
|
|
7976
|
+
});
|
|
7977
|
+
const BATCH_SIZE = 5e3;
|
|
7978
|
+
const commitments = [];
|
|
7979
|
+
const totalBatches = Math.ceil(nextIndex / BATCH_SIZE);
|
|
7980
|
+
for (let start = 0; start < nextIndex; start += BATCH_SIZE) {
|
|
7981
|
+
const end = Math.min(start + BATCH_SIZE, nextIndex);
|
|
7982
|
+
const batchNum = Math.floor(start / BATCH_SIZE) + 1;
|
|
7983
|
+
onProgress?.("Fetching commitments", `batch ${batchNum}/${totalBatches}`);
|
|
7984
|
+
const batch = await publicClient.readContract({
|
|
7985
|
+
address: poolAddress,
|
|
7986
|
+
abi: POOL_ABI,
|
|
7987
|
+
functionName: "getCommitments",
|
|
7988
|
+
args: [BigInt(start), BigInt(end)]
|
|
7989
|
+
});
|
|
7990
|
+
commitments.push(...batch.map((c) => c.toString()));
|
|
7991
|
+
}
|
|
7992
|
+
onProgress?.("Building ZK proof...");
|
|
7993
|
+
const result = await prepareTransaction({
|
|
7994
|
+
commitments,
|
|
7995
|
+
inputs: selectedUtxos,
|
|
7996
|
+
outputs,
|
|
7997
|
+
fee: 0,
|
|
7998
|
+
recipient: "0x0000000000000000000000000000000000000000",
|
|
7999
|
+
relayer: "0x0000000000000000000000000000000000000000",
|
|
8000
|
+
onProgress
|
|
8001
|
+
});
|
|
8002
|
+
onProgress?.("Submitting to relay...");
|
|
8003
|
+
const relayResult = await submitRelay({
|
|
8004
|
+
type: "transfer",
|
|
8005
|
+
pool,
|
|
8006
|
+
relayUrl,
|
|
8007
|
+
proofArgs: {
|
|
8008
|
+
proof: result.args.proof,
|
|
8009
|
+
root: result.args.root,
|
|
8010
|
+
inputNullifiers: result.args.inputNullifiers,
|
|
8011
|
+
outputCommitments: result.args.outputCommitments,
|
|
8012
|
+
publicAmount: result.args.publicAmount,
|
|
8013
|
+
extDataHash: result.args.extDataHash
|
|
8014
|
+
},
|
|
8015
|
+
extData: result.extData,
|
|
8016
|
+
metadata: {
|
|
8017
|
+
amount,
|
|
8018
|
+
recipient: "self",
|
|
8019
|
+
inputUtxoCount: selectedUtxos.length,
|
|
8020
|
+
outputUtxoCount: outputs.length
|
|
8021
|
+
}
|
|
8022
|
+
});
|
|
8023
|
+
return {
|
|
8024
|
+
success: relayResult.success,
|
|
8025
|
+
transactionHash: relayResult.transactionHash,
|
|
8026
|
+
blockNumber: relayResult.blockNumber,
|
|
8027
|
+
amount,
|
|
8028
|
+
slot: normalizedSlot,
|
|
8029
|
+
pool
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
7853
8032
|
|
|
7854
8033
|
// src/cli/commands/subaccount.ts
|
|
7855
8034
|
function parseSlotValue(raw) {
|
|
@@ -7883,6 +8062,13 @@ function parseAsset(raw) {
|
|
|
7883
8062
|
}
|
|
7884
8063
|
return asset;
|
|
7885
8064
|
}
|
|
8065
|
+
function parsePool(raw) {
|
|
8066
|
+
const pool = raw.toLowerCase();
|
|
8067
|
+
if (pool !== "eth" && pool !== "usdc") {
|
|
8068
|
+
throw new CLIError(ErrorCode.INVALID_AMOUNT, `Unsupported pool: ${raw}. Supported: eth, usdc`);
|
|
8069
|
+
}
|
|
8070
|
+
return pool;
|
|
8071
|
+
}
|
|
7886
8072
|
function printQueueHuman(title, queue) {
|
|
7887
8073
|
printSection(title);
|
|
7888
8074
|
printFields([
|
|
@@ -7902,6 +8088,7 @@ Examples:
|
|
|
7902
8088
|
veil subaccount status --slot 0
|
|
7903
8089
|
veil subaccount deploy --slot 0
|
|
7904
8090
|
veil subaccount sweep --slot 0 --asset eth
|
|
8091
|
+
veil subaccount merge --slot 0 --pool eth
|
|
7905
8092
|
veil subaccount recover --slot 0 --asset usdc --to 0xRecipientAddress --amount 25
|
|
7906
8093
|
veil subaccount address --slot 0
|
|
7907
8094
|
`);
|
|
@@ -7939,7 +8126,7 @@ Examples:
|
|
|
7939
8126
|
handleCLIError(error);
|
|
7940
8127
|
}
|
|
7941
8128
|
});
|
|
7942
|
-
subaccount.command("status").description("Show subaccount deployment, balances, and queue state").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
|
|
8129
|
+
subaccount.command("status").description("Show subaccount deployment, forwarder balances, private balances, and queue state").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
|
|
7943
8130
|
try {
|
|
7944
8131
|
const rootPrivateKey = getRequiredVeilKey();
|
|
7945
8132
|
const status = await getSubaccountStatus({
|
|
@@ -7964,6 +8151,17 @@ Examples:
|
|
|
7964
8151
|
{ label: "ETH", value: `${status.balances.eth.balance} ETH` },
|
|
7965
8152
|
{ label: "USDC", value: `${status.balances.usdc.balance} USDC` }
|
|
7966
8153
|
]);
|
|
8154
|
+
printSection("Private Pool Balances");
|
|
8155
|
+
printFields([
|
|
8156
|
+
{
|
|
8157
|
+
label: "ETH",
|
|
8158
|
+
value: `${status.privateBalances.eth.privateBalance} ETH (${status.privateBalances.eth.unspentCount} unspent / ${status.privateBalances.eth.spentCount} spent / ${status.privateBalances.eth.utxoCount} total UTXOs)`
|
|
8159
|
+
},
|
|
8160
|
+
{
|
|
8161
|
+
label: "USDC",
|
|
8162
|
+
value: `${status.privateBalances.usdc.privateBalance} USDC (${status.privateBalances.usdc.unspentCount} unspent / ${status.privateBalances.usdc.spentCount} spent / ${status.privateBalances.usdc.utxoCount} total UTXOs)`
|
|
8163
|
+
}
|
|
8164
|
+
]);
|
|
7967
8165
|
printQueueHuman("ETH Queue", status.queues.eth);
|
|
7968
8166
|
printQueueHuman("USDC Queue", status.queues.usdc);
|
|
7969
8167
|
printLine();
|
|
@@ -7974,11 +8172,6 @@ Examples:
|
|
|
7974
8172
|
subaccount.command("deploy").description("Deploy a subaccount forwarder through the relay").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--json", "Output as JSON").action(async (options) => {
|
|
7975
8173
|
try {
|
|
7976
8174
|
const rootPrivateKey = getRequiredVeilKey();
|
|
7977
|
-
const slot = await deriveSubaccountSlot({
|
|
7978
|
-
rootPrivateKey,
|
|
7979
|
-
slot: options.slot,
|
|
7980
|
-
rpcUrl: process.env.RPC_URL
|
|
7981
|
-
});
|
|
7982
8175
|
const result = await deploySubaccountForwarder({
|
|
7983
8176
|
rootPrivateKey,
|
|
7984
8177
|
slot: options.slot,
|
|
@@ -7988,7 +8181,7 @@ Examples:
|
|
|
7988
8181
|
const output = {
|
|
7989
8182
|
...result,
|
|
7990
8183
|
slot: options.slot,
|
|
7991
|
-
forwarderAddress: slot.forwarderAddress
|
|
8184
|
+
forwarderAddress: result.slot.forwarderAddress
|
|
7992
8185
|
};
|
|
7993
8186
|
if (options.json) {
|
|
7994
8187
|
printJson(output);
|
|
@@ -7997,7 +8190,7 @@ Examples:
|
|
|
7997
8190
|
printHeader("Subaccount Deploy Submitted");
|
|
7998
8191
|
printFields([
|
|
7999
8192
|
{ label: "Slot", value: options.slot },
|
|
8000
|
-
{ label: "Forwarder", value: slot.forwarderAddress },
|
|
8193
|
+
{ label: "Forwarder", value: result.slot.forwarderAddress },
|
|
8001
8194
|
{ label: "Transaction", value: txUrl(result.transactionHash) },
|
|
8002
8195
|
{ label: "Block", value: result.blockNumber }
|
|
8003
8196
|
]);
|
|
@@ -8042,12 +8235,63 @@ Examples:
|
|
|
8042
8235
|
handleCLIError(error);
|
|
8043
8236
|
}
|
|
8044
8237
|
});
|
|
8238
|
+
subaccount.command("merge").description("Merge a subaccount's private pool balance back to the main wallet").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).option("--pool <pool>", "Pool to merge (eth or usdc)", parsePool, "eth").option("--json", "Output as JSON").action(async (options) => {
|
|
8239
|
+
try {
|
|
8240
|
+
const rootPrivateKey = getRequiredVeilKey();
|
|
8241
|
+
const result = await mergeSubaccount({
|
|
8242
|
+
rootPrivateKey,
|
|
8243
|
+
slot: options.slot,
|
|
8244
|
+
pool: options.pool,
|
|
8245
|
+
rpcUrl: process.env.RPC_URL,
|
|
8246
|
+
relayUrl: process.env.RELAY_URL,
|
|
8247
|
+
onProgress: options.json ? void 0 : (stage, detail) => {
|
|
8248
|
+
const msg = detail ? `${stage} ${detail}` : stage;
|
|
8249
|
+
process.stderr.write(`\r\x1B[K${msg}`);
|
|
8250
|
+
}
|
|
8251
|
+
});
|
|
8252
|
+
if (!options.json) {
|
|
8253
|
+
process.stderr.write("\r\x1B[K");
|
|
8254
|
+
}
|
|
8255
|
+
const output = {
|
|
8256
|
+
success: result.success,
|
|
8257
|
+
slot: result.slot,
|
|
8258
|
+
pool: result.pool,
|
|
8259
|
+
amount: result.amount,
|
|
8260
|
+
transactionHash: result.transactionHash,
|
|
8261
|
+
blockNumber: result.blockNumber
|
|
8262
|
+
};
|
|
8263
|
+
if (options.json) {
|
|
8264
|
+
printJson(output);
|
|
8265
|
+
return;
|
|
8266
|
+
}
|
|
8267
|
+
printHeader("Subaccount Merge Submitted");
|
|
8268
|
+
printFields([
|
|
8269
|
+
{ label: "Slot", value: result.slot },
|
|
8270
|
+
{ label: "Pool", value: result.pool.toUpperCase() },
|
|
8271
|
+
{ label: "Amount", value: result.amount },
|
|
8272
|
+
{ label: "Transaction", value: txUrl(result.transactionHash) },
|
|
8273
|
+
{ label: "Block", value: result.blockNumber }
|
|
8274
|
+
]);
|
|
8275
|
+
printLine();
|
|
8276
|
+
} catch (error) {
|
|
8277
|
+
if (!options.json) {
|
|
8278
|
+
process.stderr.write("\r\x1B[K");
|
|
8279
|
+
}
|
|
8280
|
+
handleCLIError(error);
|
|
8281
|
+
}
|
|
8282
|
+
});
|
|
8045
8283
|
subaccount.command("recover").description("Recover assets sitting on the subaccount forwarder with a direct withdraw transaction").requiredOption("--slot <n>", "Subaccount slot", parseSlotValue).requiredOption("--asset <asset>", "Asset to recover (eth or usdc)", parseAsset).requiredOption("--to <address>", "Recipient address").requiredOption("--amount <value>", "Amount to recover").option("--json", "Output as JSON").action(async (options) => {
|
|
8046
8284
|
try {
|
|
8047
8285
|
const rootPrivateKey = getRequiredVeilKey();
|
|
8048
8286
|
if (!viem.isAddress(options.to)) {
|
|
8049
8287
|
throw new CLIError(ErrorCode.INVALID_ADDRESS, `Invalid recipient address: ${options.to}`);
|
|
8050
8288
|
}
|
|
8289
|
+
if (!process.env.WALLET_KEY) {
|
|
8290
|
+
throw new CLIError(
|
|
8291
|
+
ErrorCode.WALLET_KEY_MISSING,
|
|
8292
|
+
"WALLET_KEY required for recovery. Recovery submits a transaction on-chain and needs a gas payer."
|
|
8293
|
+
);
|
|
8294
|
+
}
|
|
8051
8295
|
const config = getConfig({});
|
|
8052
8296
|
const recovery = await buildSubaccountRecoveryTx({
|
|
8053
8297
|
rootPrivateKey,
|
|
@@ -8118,7 +8362,7 @@ Examples:
|
|
|
8118
8362
|
// src/cli/index.ts
|
|
8119
8363
|
loadEnv();
|
|
8120
8364
|
var program2 = new Command();
|
|
8121
|
-
program2.name("veil").description("CLI for Veil Cash privacy pools on Base").version("0.6.
|
|
8365
|
+
program2.name("veil").description("CLI for Veil Cash privacy pools on Base").version("0.6.2").addHelpText("after", `
|
|
8122
8366
|
Getting started:
|
|
8123
8367
|
veil init
|
|
8124
8368
|
veil register
|