@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/src/subaccount.ts
CHANGED
|
@@ -12,14 +12,21 @@ import {
|
|
|
12
12
|
} from 'viem';
|
|
13
13
|
import { privateKeyToAccount, privateKeyToAddress } from 'viem/accounts';
|
|
14
14
|
import { base } from 'viem/chains';
|
|
15
|
-
import { FORWARDER_ABI, FORWARDER_FACTORY_ABI, ERC20_ABI } from './abi.js';
|
|
16
|
-
import { FORWARDER_CONTRACT_VERSION, getAddresses, getForwarderFactoryAddress } from './addresses.js';
|
|
17
|
-
import { getQueueBalance } from './balance.js';
|
|
15
|
+
import { FORWARDER_ABI, FORWARDER_FACTORY_ABI, ERC20_ABI, POOL_ABI } from './abi.js';
|
|
16
|
+
import { FORWARDER_CONTRACT_VERSION, getAddresses, getForwarderFactoryAddress, getPoolAddress, POOL_CONFIG } from './addresses.js';
|
|
17
|
+
import { getPrivateBalance, getQueueBalance } from './balance.js';
|
|
18
18
|
import { Keypair } from './keypair.js';
|
|
19
|
-
import { postRelayJson } from './relay.js';
|
|
19
|
+
import { postRelayJson, submitRelay } from './relay.js';
|
|
20
|
+
import { prepareTransaction } from './transaction.js';
|
|
21
|
+
import { Utxo } from './utxo.js';
|
|
22
|
+
import { selectUtxosForWithdraw } from './withdraw.js';
|
|
20
23
|
import type {
|
|
21
24
|
SubaccountAsset,
|
|
22
25
|
SubaccountDeployRequest,
|
|
26
|
+
SubaccountMergeOptions,
|
|
27
|
+
SubaccountMergeResult,
|
|
28
|
+
PrivateBalanceResult,
|
|
29
|
+
SubaccountPrivateBalanceStatus,
|
|
23
30
|
SubaccountQueueStatus,
|
|
24
31
|
SubaccountRecoveryResult,
|
|
25
32
|
SubaccountRelayResult,
|
|
@@ -134,11 +141,14 @@ export async function predictSubaccountForwarder(options: {
|
|
|
134
141
|
rpcUrl?: string;
|
|
135
142
|
}): Promise<`0x${string}`> {
|
|
136
143
|
const publicClient = createBaseClient(options.rpcUrl);
|
|
144
|
+
const depositKeyBytes = options.childDepositKey.startsWith('0x')
|
|
145
|
+
? options.childDepositKey
|
|
146
|
+
: `0x${options.childDepositKey}`;
|
|
137
147
|
return publicClient.readContract({
|
|
138
148
|
abi: FORWARDER_FACTORY_ABI,
|
|
139
149
|
address: getForwarderFactoryAddress(),
|
|
140
150
|
functionName: 'computeAddress',
|
|
141
|
-
args: [options.salt,
|
|
151
|
+
args: [options.salt, depositKeyBytes as `0x${string}`, options.childOwner],
|
|
142
152
|
}) as Promise<`0x${string}`>;
|
|
143
153
|
}
|
|
144
154
|
|
|
@@ -183,14 +193,14 @@ export async function isSubaccountForwarderDeployed(options: {
|
|
|
183
193
|
|
|
184
194
|
export async function deploySubaccountForwarder(
|
|
185
195
|
options: SubaccountDeployRequest,
|
|
186
|
-
): Promise<SubaccountRelayResult> {
|
|
196
|
+
): Promise<SubaccountRelayResult & { slot: SubaccountSlot }> {
|
|
187
197
|
const slot = await deriveSubaccountSlot({
|
|
188
198
|
rootPrivateKey: options.rootPrivateKey,
|
|
189
199
|
slot: options.slot,
|
|
190
200
|
rpcUrl: options.rpcUrl,
|
|
191
201
|
});
|
|
192
202
|
|
|
193
|
-
|
|
203
|
+
const result = await postRelayJson<SubaccountRelayResult>(
|
|
194
204
|
'/stealth/deploy',
|
|
195
205
|
{
|
|
196
206
|
salt: slot.salt,
|
|
@@ -200,6 +210,8 @@ export async function deploySubaccountForwarder(
|
|
|
200
210
|
},
|
|
201
211
|
options.relayUrl,
|
|
202
212
|
);
|
|
213
|
+
|
|
214
|
+
return { ...result, slot };
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
export async function sweepSubaccountForwarder(
|
|
@@ -233,6 +245,37 @@ function toQueueStatus(
|
|
|
233
245
|
};
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
function toPrivateBalanceStatus(result: PrivateBalanceResult): SubaccountPrivateBalanceStatus {
|
|
249
|
+
return {
|
|
250
|
+
privateBalance: result.privateBalance,
|
|
251
|
+
privateBalanceWei: result.privateBalanceWei,
|
|
252
|
+
utxoCount: result.utxoCount,
|
|
253
|
+
spentCount: result.spentCount,
|
|
254
|
+
unspentCount: result.unspentCount,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function getSubaccountPrivateBalance(options: {
|
|
259
|
+
rootPrivateKey: `0x${string}`;
|
|
260
|
+
slot: number;
|
|
261
|
+
pool?: 'eth' | 'usdc';
|
|
262
|
+
rpcUrl?: string;
|
|
263
|
+
onProgress?: (stage: string, detail?: string) => void;
|
|
264
|
+
}): Promise<PrivateBalanceResult> {
|
|
265
|
+
const normalizedSlot = normalizeSlot(options.slot);
|
|
266
|
+
assertPrivateKey(options.rootPrivateKey, 'rootPrivateKey');
|
|
267
|
+
|
|
268
|
+
const childPrivateKey = deriveSubaccountChildPrivateKey(options.rootPrivateKey, normalizedSlot);
|
|
269
|
+
const childKeypair = new Keypair(childPrivateKey);
|
|
270
|
+
|
|
271
|
+
return getPrivateBalance({
|
|
272
|
+
keypair: childKeypair,
|
|
273
|
+
pool: options.pool,
|
|
274
|
+
rpcUrl: options.rpcUrl,
|
|
275
|
+
onProgress: options.onProgress,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
236
279
|
export async function getSubaccountStatus(options: {
|
|
237
280
|
rootPrivateKey: `0x${string}`;
|
|
238
281
|
slot: number;
|
|
@@ -242,7 +285,7 @@ export async function getSubaccountStatus(options: {
|
|
|
242
285
|
const publicClient = createBaseClient(options.rpcUrl);
|
|
243
286
|
const addresses = getAddresses();
|
|
244
287
|
|
|
245
|
-
const [deployed, ethWei, usdcWei, ethQueue, usdcQueue] = await Promise.all([
|
|
288
|
+
const [deployed, ethWei, usdcWei, ethQueue, usdcQueue, ethPrivate, usdcPrivate] = await Promise.all([
|
|
246
289
|
isSubaccountForwarderDeployed({
|
|
247
290
|
forwarderAddress: slot.forwarderAddress,
|
|
248
291
|
rpcUrl: options.rpcUrl,
|
|
@@ -264,6 +307,18 @@ export async function getSubaccountStatus(options: {
|
|
|
264
307
|
pool: 'usdc',
|
|
265
308
|
rpcUrl: options.rpcUrl,
|
|
266
309
|
}),
|
|
310
|
+
getSubaccountPrivateBalance({
|
|
311
|
+
rootPrivateKey: options.rootPrivateKey,
|
|
312
|
+
slot: options.slot,
|
|
313
|
+
pool: 'eth',
|
|
314
|
+
rpcUrl: options.rpcUrl,
|
|
315
|
+
}),
|
|
316
|
+
getSubaccountPrivateBalance({
|
|
317
|
+
rootPrivateKey: options.rootPrivateKey,
|
|
318
|
+
slot: options.slot,
|
|
319
|
+
pool: 'usdc',
|
|
320
|
+
rpcUrl: options.rpcUrl,
|
|
321
|
+
}),
|
|
267
322
|
]);
|
|
268
323
|
|
|
269
324
|
return {
|
|
@@ -279,6 +334,10 @@ export async function getSubaccountStatus(options: {
|
|
|
279
334
|
balanceWei: usdcWei.toString(),
|
|
280
335
|
},
|
|
281
336
|
},
|
|
337
|
+
privateBalances: {
|
|
338
|
+
eth: toPrivateBalanceStatus(ethPrivate),
|
|
339
|
+
usdc: toPrivateBalanceStatus(usdcPrivate),
|
|
340
|
+
},
|
|
282
341
|
queues: {
|
|
283
342
|
eth: toQueueStatus('eth', ethQueue),
|
|
284
343
|
usdc: toQueueStatus('usdc', usdcQueue),
|
|
@@ -474,3 +533,188 @@ export async function buildSubaccountRecoveryTx(options: {
|
|
|
474
533
|
signature,
|
|
475
534
|
};
|
|
476
535
|
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Merge a subaccount's entire private balance back to the main wallet.
|
|
539
|
+
*
|
|
540
|
+
* Builds a ZK transfer proof that moves every unspent UTXO belonging to the
|
|
541
|
+
* child keypair into a new UTXO encrypted to the parent (root) keypair,
|
|
542
|
+
* then submits it via the relay.
|
|
543
|
+
*
|
|
544
|
+
* @param options - Merge options
|
|
545
|
+
* @returns Merge result with transaction hash and amount
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```typescript
|
|
549
|
+
* const result = await mergeSubaccount({
|
|
550
|
+
* rootPrivateKey: process.env.VEIL_KEY as `0x${string}`,
|
|
551
|
+
* slot: 0,
|
|
552
|
+
* pool: 'eth',
|
|
553
|
+
* });
|
|
554
|
+
* console.log(`Merged ${result.amount} — tx: ${result.transactionHash}`);
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
export async function mergeSubaccount(
|
|
558
|
+
options: SubaccountMergeOptions,
|
|
559
|
+
): Promise<SubaccountMergeResult> {
|
|
560
|
+
const {
|
|
561
|
+
rootPrivateKey,
|
|
562
|
+
slot,
|
|
563
|
+
pool = 'eth',
|
|
564
|
+
rpcUrl,
|
|
565
|
+
relayUrl,
|
|
566
|
+
onProgress,
|
|
567
|
+
} = options;
|
|
568
|
+
|
|
569
|
+
const normalizedSlot = normalizeSlot(slot);
|
|
570
|
+
assertPrivateKey(rootPrivateKey, 'rootPrivateKey');
|
|
571
|
+
|
|
572
|
+
const poolConfig = POOL_CONFIG[pool];
|
|
573
|
+
const poolAddress = getPoolAddress(pool);
|
|
574
|
+
|
|
575
|
+
// Derive child and parent keypairs
|
|
576
|
+
const childPrivateKey = deriveSubaccountChildPrivateKey(rootPrivateKey, normalizedSlot);
|
|
577
|
+
const childKeypair = new Keypair(childPrivateKey);
|
|
578
|
+
const parentKeypair = new Keypair(rootPrivateKey);
|
|
579
|
+
|
|
580
|
+
// Fetch child's private balance
|
|
581
|
+
onProgress?.('Fetching subaccount balance...');
|
|
582
|
+
const balanceResult = await getPrivateBalance({
|
|
583
|
+
keypair: childKeypair,
|
|
584
|
+
pool,
|
|
585
|
+
rpcUrl,
|
|
586
|
+
onProgress,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const unspentUtxoInfos = balanceResult.utxos.filter(u => !u.isSpent);
|
|
590
|
+
if (unspentUtxoInfos.length === 0) {
|
|
591
|
+
throw new Error('Subaccount has no unspent UTXOs to merge');
|
|
592
|
+
}
|
|
593
|
+
if (unspentUtxoInfos.length > 16) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Subaccount has ${unspentUtxoInfos.length} unspent UTXOs which exceeds the 16-input circuit limit. ` +
|
|
596
|
+
'Consolidate UTXOs on the subaccount first before merging.',
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Re-decrypt UTXOs to get full Utxo objects
|
|
601
|
+
onProgress?.('Preparing UTXOs...');
|
|
602
|
+
const publicClient = createPublicClient({
|
|
603
|
+
chain: base,
|
|
604
|
+
transport: http(rpcUrl),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const utxos: Utxo[] = [];
|
|
608
|
+
for (const utxoInfo of unspentUtxoInfos) {
|
|
609
|
+
const encryptedOutputs = await publicClient.readContract({
|
|
610
|
+
address: poolAddress,
|
|
611
|
+
abi: POOL_ABI,
|
|
612
|
+
functionName: 'getEncryptedOutputs',
|
|
613
|
+
args: [BigInt(utxoInfo.index), BigInt(utxoInfo.index + 1)],
|
|
614
|
+
}) as string[];
|
|
615
|
+
|
|
616
|
+
if (encryptedOutputs.length > 0) {
|
|
617
|
+
try {
|
|
618
|
+
const utxo = Utxo.decrypt(encryptedOutputs[0], childKeypair);
|
|
619
|
+
utxo.index = utxoInfo.index;
|
|
620
|
+
utxos.push(utxo);
|
|
621
|
+
} catch {
|
|
622
|
+
// Skip if decryption fails
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (utxos.length === 0) {
|
|
628
|
+
throw new Error('Failed to decrypt subaccount UTXOs');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Select all UTXOs — transfer the full balance
|
|
632
|
+
onProgress?.('Selecting UTXOs...');
|
|
633
|
+
const amount = balanceResult.privateBalance;
|
|
634
|
+
const { selectedUtxos, changeAmount } = selectUtxosForWithdraw(
|
|
635
|
+
utxos,
|
|
636
|
+
amount,
|
|
637
|
+
poolConfig.decimals,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
// Create output UTXO encrypted to the parent keypair
|
|
641
|
+
const outputs: Utxo[] = [];
|
|
642
|
+
const mergeWei = parseUnits(amount, poolConfig.decimals);
|
|
643
|
+
|
|
644
|
+
outputs.push(new Utxo({ amount: mergeWei, keypair: parentKeypair }));
|
|
645
|
+
|
|
646
|
+
if (changeAmount > 0n) {
|
|
647
|
+
outputs.push(new Utxo({ amount: changeAmount, keypair: parentKeypair }));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Fetch all commitments from pool
|
|
651
|
+
onProgress?.('Fetching commitments...');
|
|
652
|
+
const nextIndex = await publicClient.readContract({
|
|
653
|
+
address: poolAddress,
|
|
654
|
+
abi: POOL_ABI,
|
|
655
|
+
functionName: 'nextIndex',
|
|
656
|
+
}) as number;
|
|
657
|
+
|
|
658
|
+
const BATCH_SIZE = 5000;
|
|
659
|
+
const commitments: string[] = [];
|
|
660
|
+
const totalBatches = Math.ceil(nextIndex / BATCH_SIZE);
|
|
661
|
+
|
|
662
|
+
for (let start = 0; start < nextIndex; start += BATCH_SIZE) {
|
|
663
|
+
const end = Math.min(start + BATCH_SIZE, nextIndex);
|
|
664
|
+
const batchNum = Math.floor(start / BATCH_SIZE) + 1;
|
|
665
|
+
onProgress?.('Fetching commitments', `batch ${batchNum}/${totalBatches}`);
|
|
666
|
+
|
|
667
|
+
const batch = await publicClient.readContract({
|
|
668
|
+
address: poolAddress,
|
|
669
|
+
abi: POOL_ABI,
|
|
670
|
+
functionName: 'getCommitments',
|
|
671
|
+
args: [BigInt(start), BigInt(end)],
|
|
672
|
+
}) as `0x${string}`[];
|
|
673
|
+
|
|
674
|
+
commitments.push(...batch.map(c => c.toString()));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Build ZK proof (recipient = 0x0 for in-pool transfer)
|
|
678
|
+
onProgress?.('Building ZK proof...');
|
|
679
|
+
const result = await prepareTransaction({
|
|
680
|
+
commitments,
|
|
681
|
+
inputs: selectedUtxos,
|
|
682
|
+
outputs,
|
|
683
|
+
fee: 0,
|
|
684
|
+
recipient: '0x0000000000000000000000000000000000000000',
|
|
685
|
+
relayer: '0x0000000000000000000000000000000000000000',
|
|
686
|
+
onProgress,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Submit to relay
|
|
690
|
+
onProgress?.('Submitting to relay...');
|
|
691
|
+
const relayResult = await submitRelay({
|
|
692
|
+
type: 'transfer',
|
|
693
|
+
pool,
|
|
694
|
+
relayUrl,
|
|
695
|
+
proofArgs: {
|
|
696
|
+
proof: result.args.proof,
|
|
697
|
+
root: result.args.root,
|
|
698
|
+
inputNullifiers: result.args.inputNullifiers,
|
|
699
|
+
outputCommitments: result.args.outputCommitments as [string, string],
|
|
700
|
+
publicAmount: result.args.publicAmount,
|
|
701
|
+
extDataHash: result.args.extDataHash,
|
|
702
|
+
},
|
|
703
|
+
extData: result.extData,
|
|
704
|
+
metadata: {
|
|
705
|
+
amount,
|
|
706
|
+
recipient: 'self',
|
|
707
|
+
inputUtxoCount: selectedUtxos.length,
|
|
708
|
+
outputUtxoCount: outputs.length,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
success: relayResult.success,
|
|
714
|
+
transactionHash: relayResult.transactionHash,
|
|
715
|
+
blockNumber: relayResult.blockNumber,
|
|
716
|
+
amount,
|
|
717
|
+
slot: normalizedSlot,
|
|
718
|
+
pool,
|
|
719
|
+
};
|
|
720
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -377,6 +377,25 @@ export interface SubaccountBalances {
|
|
|
377
377
|
usdc: SubaccountAssetBalance;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Private pool balance summary for a specific asset
|
|
382
|
+
*/
|
|
383
|
+
export interface SubaccountPrivateBalanceStatus {
|
|
384
|
+
privateBalance: string;
|
|
385
|
+
privateBalanceWei: string;
|
|
386
|
+
utxoCount: number;
|
|
387
|
+
spentCount: number;
|
|
388
|
+
unspentCount: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Private pool balances for both supported assets
|
|
393
|
+
*/
|
|
394
|
+
export interface SubaccountPrivateBalances {
|
|
395
|
+
eth: SubaccountPrivateBalanceStatus;
|
|
396
|
+
usdc: SubaccountPrivateBalanceStatus;
|
|
397
|
+
}
|
|
398
|
+
|
|
380
399
|
/**
|
|
381
400
|
* Queue status for a specific asset
|
|
382
401
|
*/
|
|
@@ -395,6 +414,7 @@ export interface SubaccountStatusResult {
|
|
|
395
414
|
slot: SubaccountSlot;
|
|
396
415
|
deployed: boolean;
|
|
397
416
|
balances: SubaccountBalances;
|
|
417
|
+
privateBalances: SubaccountPrivateBalances;
|
|
398
418
|
queues: {
|
|
399
419
|
eth: SubaccountQueueStatus;
|
|
400
420
|
usdc: SubaccountQueueStatus;
|
|
@@ -427,6 +447,42 @@ export interface SubaccountWithdrawTypedData {
|
|
|
427
447
|
};
|
|
428
448
|
}
|
|
429
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Options for merging a subaccount's private balance back to the main wallet
|
|
452
|
+
*/
|
|
453
|
+
export interface SubaccountMergeOptions {
|
|
454
|
+
/** Root private key (VEIL_KEY) */
|
|
455
|
+
rootPrivateKey: `0x${string}`;
|
|
456
|
+
/** Subaccount slot (0-2) */
|
|
457
|
+
slot: number;
|
|
458
|
+
/** Pool to merge in (default: 'eth') */
|
|
459
|
+
pool?: RelayPool;
|
|
460
|
+
/** Optional RPC URL */
|
|
461
|
+
rpcUrl?: string;
|
|
462
|
+
/** Optional relay URL */
|
|
463
|
+
relayUrl?: string;
|
|
464
|
+
/** Progress callback */
|
|
465
|
+
onProgress?: (stage: string, detail?: string) => void;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Result from merging a subaccount's balance to the main wallet
|
|
470
|
+
*/
|
|
471
|
+
export interface SubaccountMergeResult {
|
|
472
|
+
/** Whether the merge was successful */
|
|
473
|
+
success: boolean;
|
|
474
|
+
/** Transaction hash */
|
|
475
|
+
transactionHash: string;
|
|
476
|
+
/** Block number of the transaction */
|
|
477
|
+
blockNumber: string;
|
|
478
|
+
/** Amount merged (human-readable) */
|
|
479
|
+
amount: string;
|
|
480
|
+
/** Subaccount slot that was merged */
|
|
481
|
+
slot: number;
|
|
482
|
+
/** Pool the merge was executed in */
|
|
483
|
+
pool: RelayPool;
|
|
484
|
+
}
|
|
485
|
+
|
|
430
486
|
/**
|
|
431
487
|
* Built recovery transaction and signing metadata
|
|
432
488
|
*/
|