@veil-cash/sdk 0.1.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.
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Withdrawal functions for Veil SDK
3
+ * Build ZK proofs to withdraw funds from the pool to a public address
4
+ */
5
+
6
+ import { createPublicClient, http, parseUnits, formatUnits } from 'viem';
7
+ import { base } from 'viem/chains';
8
+ import { getAddresses, POOL_CONFIG } from './addresses.js';
9
+ import { POOL_ABI } from './abi.js';
10
+ import { Keypair } from './keypair.js';
11
+ import { Utxo } from './utxo.js';
12
+ import { getPrivateBalance } from './balance.js';
13
+ import { prepareTransaction } from './transaction.js';
14
+ import { submitRelay } from './relay.js';
15
+ import type {
16
+ BuildWithdrawProofOptions,
17
+ ProofBuildResult,
18
+ WithdrawResult,
19
+ UtxoSelectionResult,
20
+ } from './types.js';
21
+
22
+ /**
23
+ * Select UTXOs for withdrawal using largest-first algorithm
24
+ *
25
+ * @param utxos - Available unspent UTXOs
26
+ * @param amount - Amount to withdraw (human readable)
27
+ * @param decimals - Token decimals (default: 18 for ETH)
28
+ * @returns Selected UTXOs and change amount
29
+ */
30
+ export function selectUtxosForWithdraw(
31
+ utxos: Utxo[],
32
+ amount: string,
33
+ decimals: number = 18
34
+ ): UtxoSelectionResult {
35
+ const withdrawWei = parseUnits(amount, decimals);
36
+
37
+ // Sort UTXOs by amount (largest first)
38
+ const sortedUtxos = [...utxos].sort((a, b) => Number(b.amount - a.amount));
39
+
40
+ let totalSelected = 0n;
41
+ const selectedUtxos: Utxo[] = [];
42
+
43
+ for (const utxo of sortedUtxos) {
44
+ selectedUtxos.push(utxo);
45
+ totalSelected += utxo.amount;
46
+
47
+ if (totalSelected >= withdrawWei) {
48
+ break;
49
+ }
50
+ }
51
+
52
+ if (totalSelected < withdrawWei) {
53
+ throw new Error(
54
+ `Insufficient balance. Need ${amount}, have ${formatUnits(totalSelected, decimals)}`
55
+ );
56
+ }
57
+
58
+ const changeAmount = totalSelected - withdrawWei;
59
+
60
+ return { selectedUtxos, totalSelected, changeAmount };
61
+ }
62
+
63
+ /**
64
+ * Fetch all commitments from the pool contract
65
+ *
66
+ * @param rpcUrl - RPC URL
67
+ * @param poolAddress - Pool contract address
68
+ * @param onProgress - Progress callback
69
+ * @returns Array of commitment hashes
70
+ */
71
+ async function fetchCommitments(
72
+ rpcUrl: string | undefined,
73
+ poolAddress: `0x${string}`,
74
+ onProgress?: (stage: string, detail?: string) => void
75
+ ): Promise<string[]> {
76
+ const publicClient = createPublicClient({
77
+ chain: base,
78
+ transport: http(rpcUrl),
79
+ });
80
+
81
+ // Get total count
82
+ onProgress?.('Fetching commitment count...');
83
+ const nextIndex = await publicClient.readContract({
84
+ address: poolAddress,
85
+ abi: POOL_ABI,
86
+ functionName: 'nextIndex',
87
+ }) as number;
88
+
89
+ if (nextIndex === 0) {
90
+ return [];
91
+ }
92
+
93
+ // Fetch commitments in batches
94
+ const BATCH_SIZE = 5000;
95
+ const commitments: string[] = [];
96
+ const totalBatches = Math.ceil(nextIndex / BATCH_SIZE);
97
+
98
+ for (let start = 0; start < nextIndex; start += BATCH_SIZE) {
99
+ const end = Math.min(start + BATCH_SIZE, nextIndex);
100
+ const batchNum = Math.floor(start / BATCH_SIZE) + 1;
101
+ onProgress?.('Fetching commitments', `batch ${batchNum}/${totalBatches}`);
102
+
103
+ const batch = await publicClient.readContract({
104
+ address: poolAddress,
105
+ abi: POOL_ABI,
106
+ functionName: 'getCommitments',
107
+ args: [BigInt(start), BigInt(end)],
108
+ }) as `0x${string}`[];
109
+
110
+ commitments.push(...batch.map(c => c.toString()));
111
+ }
112
+
113
+ return commitments;
114
+ }
115
+
116
+ /**
117
+ * Build a withdrawal proof
118
+ *
119
+ * This function:
120
+ * 1. Fetches the user's unspent UTXOs
121
+ * 2. Selects UTXOs to cover the withdrawal amount
122
+ * 3. Fetches all commitments from the pool
123
+ * 4. Builds the ZK proof
124
+ *
125
+ * @param options - Withdrawal options
126
+ * @returns Proof data ready for relay submission
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const keypair = new Keypair(process.env.VEIL_KEY);
131
+ * const proof = await buildWithdrawProof({
132
+ * amount: '0.1',
133
+ * recipient: '0x1234...',
134
+ * keypair,
135
+ * onProgress: (stage, detail) => console.log(stage, detail),
136
+ * });
137
+ * ```
138
+ */
139
+ export async function buildWithdrawProof(
140
+ options: BuildWithdrawProofOptions
141
+ ): Promise<ProofBuildResult> {
142
+ const {
143
+ amount,
144
+ recipient,
145
+ keypair,
146
+ rpcUrl,
147
+ onProgress,
148
+ } = options;
149
+
150
+ const addresses = getAddresses();
151
+ const poolConfig = POOL_CONFIG.eth;
152
+ const poolAddress = addresses.ethPool;
153
+
154
+ // 1. Get user's unspent UTXOs
155
+ onProgress?.('Fetching your UTXOs...');
156
+ const balanceResult = await getPrivateBalance({
157
+ keypair,
158
+ rpcUrl,
159
+ onProgress,
160
+ });
161
+
162
+ // Filter to only unspent UTXOs and recreate Utxo objects with keypair
163
+ const unspentUtxoInfos = balanceResult.utxos.filter(u => !u.isSpent);
164
+ if (unspentUtxoInfos.length === 0) {
165
+ throw new Error('No unspent UTXOs available for withdrawal');
166
+ }
167
+
168
+ // We need to re-decrypt the UTXOs to get full Utxo objects
169
+ // For now, we'll fetch encrypted outputs and decrypt again
170
+ onProgress?.('Preparing UTXOs...');
171
+
172
+ const publicClient = createPublicClient({
173
+ chain: base,
174
+ transport: http(rpcUrl),
175
+ });
176
+
177
+ // Fetch encrypted outputs for our unspent UTXOs
178
+ const utxos: Utxo[] = [];
179
+ for (const utxoInfo of unspentUtxoInfos) {
180
+ const encryptedOutputs = await publicClient.readContract({
181
+ address: poolAddress,
182
+ abi: POOL_ABI,
183
+ functionName: 'getEncryptedOutputs',
184
+ args: [BigInt(utxoInfo.index), BigInt(utxoInfo.index + 1)],
185
+ }) as string[];
186
+
187
+ if (encryptedOutputs.length > 0) {
188
+ try {
189
+ const utxo = Utxo.decrypt(encryptedOutputs[0], keypair);
190
+ utxo.index = utxoInfo.index;
191
+ utxos.push(utxo);
192
+ } catch {
193
+ // Skip if decryption fails
194
+ }
195
+ }
196
+ }
197
+
198
+ if (utxos.length === 0) {
199
+ throw new Error('Failed to decrypt UTXOs');
200
+ }
201
+
202
+ // 2. Select UTXOs for withdrawal
203
+ onProgress?.('Selecting UTXOs...');
204
+ const { selectedUtxos, changeAmount } = selectUtxosForWithdraw(
205
+ utxos,
206
+ amount,
207
+ poolConfig.decimals
208
+ );
209
+
210
+ // 3. Create output UTXOs (just change, since withdrawal goes to external address)
211
+ const outputs: Utxo[] = [];
212
+ if (changeAmount > 0n) {
213
+ const changeUtxo = new Utxo({
214
+ amount: changeAmount,
215
+ keypair: keypair,
216
+ });
217
+ outputs.push(changeUtxo);
218
+ }
219
+
220
+ // 4. Fetch all commitments from pool
221
+ const commitments = await fetchCommitments(rpcUrl, poolAddress, onProgress);
222
+
223
+ // 5. Build the ZK proof
224
+ onProgress?.('Building ZK proof...');
225
+ const result = await prepareTransaction({
226
+ commitments,
227
+ inputs: selectedUtxos,
228
+ outputs,
229
+ fee: 0,
230
+ recipient,
231
+ relayer: '0x0000000000000000000000000000000000000000',
232
+ onProgress,
233
+ });
234
+
235
+ return {
236
+ proofArgs: {
237
+ proof: result.args.proof,
238
+ root: result.args.root,
239
+ inputNullifiers: result.args.inputNullifiers,
240
+ outputCommitments: result.args.outputCommitments as [string, string],
241
+ publicAmount: result.args.publicAmount,
242
+ extDataHash: result.args.extDataHash,
243
+ },
244
+ extData: result.extData,
245
+ inputCount: selectedUtxos.length,
246
+ outputCount: outputs.length,
247
+ amount,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Execute a withdrawal by building proof and submitting to relay
253
+ *
254
+ * @param options - Withdrawal options
255
+ * @returns Withdrawal result with transaction hash
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const keypair = new Keypair(process.env.VEIL_KEY);
260
+ * const result = await withdraw({
261
+ * amount: '0.1',
262
+ * recipient: '0x1234...',
263
+ * keypair,
264
+ * });
265
+ *
266
+ * console.log(`Withdrawal tx: ${result.transactionHash}`);
267
+ * ```
268
+ */
269
+ export async function withdraw(
270
+ options: BuildWithdrawProofOptions
271
+ ): Promise<WithdrawResult> {
272
+ const { amount, recipient, onProgress } = options;
273
+
274
+ // Build the proof
275
+ const proof = await buildWithdrawProof(options);
276
+
277
+ // Submit to relay
278
+ onProgress?.('Submitting to relay...');
279
+ const relayResult = await submitRelay({
280
+ type: 'withdraw',
281
+ pool: 'eth',
282
+ proofArgs: proof.proofArgs,
283
+ extData: proof.extData,
284
+ metadata: {
285
+ amount,
286
+ recipient,
287
+ inputUtxoCount: proof.inputCount,
288
+ outputUtxoCount: proof.outputCount,
289
+ },
290
+ });
291
+
292
+ return {
293
+ success: relayResult.success,
294
+ transactionHash: relayResult.transactionHash,
295
+ blockNumber: relayResult.blockNumber,
296
+ amount,
297
+ recipient,
298
+ };
299
+ }