@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.
- package/README.md +446 -0
- package/dist/cli/index.cjs +6431 -0
- package/dist/index.cjs +1912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2099 -0
- package/dist/index.d.ts +2099 -0
- package/dist/index.js +1840 -0
- package/dist/index.js.map +1 -0
- package/keys/transaction16.wasm +0 -0
- package/keys/transaction16.zkey +0 -0
- package/keys/transaction2.wasm +0 -0
- package/keys/transaction2.zkey +0 -0
- package/package.json +70 -0
- package/src/abi.ts +631 -0
- package/src/addresses.ts +53 -0
- package/src/balance.ts +266 -0
- package/src/cli/commands/balance.ts +118 -0
- package/src/cli/commands/deposit.ts +115 -0
- package/src/cli/commands/init.ts +147 -0
- package/src/cli/commands/keypair.ts +31 -0
- package/src/cli/commands/private-balance.ts +68 -0
- package/src/cli/commands/queue-balance.ts +58 -0
- package/src/cli/commands/register.ts +119 -0
- package/src/cli/commands/transfer.ts +137 -0
- package/src/cli/commands/withdraw.ts +79 -0
- package/src/cli/config.ts +58 -0
- package/src/cli/errors.ts +114 -0
- package/src/cli/index.ts +52 -0
- package/src/cli/wallet.ts +228 -0
- package/src/deposit.ts +183 -0
- package/src/index.ts +160 -0
- package/src/keypair.ts +170 -0
- package/src/merkle.ts +71 -0
- package/src/prover.ts +176 -0
- package/src/relay.ts +216 -0
- package/src/transaction.ts +260 -0
- package/src/transfer.ts +462 -0
- package/src/types.ts +306 -0
- package/src/utils.ts +151 -0
- package/src/utxo.ts +119 -0
- package/src/withdraw.ts +299 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction preparation for Veil SDK
|
|
3
|
+
* Core function to build ZK proofs for withdrawals and transfers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { buildMerkleTree, MERKLE_TREE_HEIGHT } from './merkle.js';
|
|
7
|
+
import { prove, selectCircuit, type ProofInput } from './prover.js';
|
|
8
|
+
import { toFixedHex, getExtDataHash, shuffle, FIELD_SIZE } from './utils.js';
|
|
9
|
+
import { Utxo } from './utxo.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* External data for a transaction (sent alongside proof)
|
|
13
|
+
*/
|
|
14
|
+
export interface ExtData {
|
|
15
|
+
recipient: string;
|
|
16
|
+
extAmount: string;
|
|
17
|
+
relayer: string;
|
|
18
|
+
fee: string;
|
|
19
|
+
encryptedOutput1: string;
|
|
20
|
+
encryptedOutput2: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Proof arguments for on-chain verification
|
|
25
|
+
*/
|
|
26
|
+
export interface ProofArgs {
|
|
27
|
+
proof: string;
|
|
28
|
+
root: string;
|
|
29
|
+
inputNullifiers: string[];
|
|
30
|
+
outputCommitments: string[];
|
|
31
|
+
publicAmount: string;
|
|
32
|
+
extDataHash: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of preparing a transaction
|
|
37
|
+
*/
|
|
38
|
+
export interface TransactionResult {
|
|
39
|
+
args: ProofArgs;
|
|
40
|
+
extData: ExtData;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parameters for preparing a transaction
|
|
45
|
+
*/
|
|
46
|
+
export interface PrepareTransactionParams {
|
|
47
|
+
/** All commitments from the merkle tree (from pool contract) */
|
|
48
|
+
commitments: (string | bigint)[];
|
|
49
|
+
/** Input UTXOs to spend */
|
|
50
|
+
inputs?: Utxo[];
|
|
51
|
+
/** Output UTXOs to create */
|
|
52
|
+
outputs?: Utxo[];
|
|
53
|
+
/** Transaction fee (usually 0 for relay) */
|
|
54
|
+
fee?: bigint | number;
|
|
55
|
+
/** Recipient address for withdrawals (0x0 for transfers) */
|
|
56
|
+
recipient?: string | bigint | number;
|
|
57
|
+
/** Relayer address (0x0 for now) */
|
|
58
|
+
relayer?: string | bigint | number;
|
|
59
|
+
/** Optional progress callback */
|
|
60
|
+
onProgress?: (stage: string, detail?: string) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Internal function to generate proof
|
|
65
|
+
*/
|
|
66
|
+
interface GetProofParams {
|
|
67
|
+
inputs: Utxo[];
|
|
68
|
+
outputs: Utxo[];
|
|
69
|
+
tree: ReturnType<typeof buildMerkleTree> extends Promise<infer T> ? T : never;
|
|
70
|
+
extAmount: bigint;
|
|
71
|
+
fee: bigint;
|
|
72
|
+
recipient: string | bigint;
|
|
73
|
+
relayer: string | bigint;
|
|
74
|
+
onProgress?: (stage: string, detail?: string) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getProof({
|
|
78
|
+
inputs,
|
|
79
|
+
outputs,
|
|
80
|
+
tree,
|
|
81
|
+
extAmount,
|
|
82
|
+
fee,
|
|
83
|
+
recipient,
|
|
84
|
+
relayer,
|
|
85
|
+
onProgress,
|
|
86
|
+
}: GetProofParams): Promise<TransactionResult> {
|
|
87
|
+
// Shuffle inputs and outputs for privacy
|
|
88
|
+
inputs = shuffle([...inputs]);
|
|
89
|
+
outputs = shuffle([...outputs]);
|
|
90
|
+
|
|
91
|
+
onProgress?.('Building merkle paths...');
|
|
92
|
+
|
|
93
|
+
const inputMerklePathIndices: number[] = [];
|
|
94
|
+
const inputMerklePathElements: (bigint | number)[][] = [];
|
|
95
|
+
|
|
96
|
+
// Get merkle path for each input
|
|
97
|
+
for (const input of inputs) {
|
|
98
|
+
if (input.amount > 0n) {
|
|
99
|
+
const inputIndex = tree.indexOf(toFixedHex(input.getCommitment()));
|
|
100
|
+
if (inputIndex < 0) {
|
|
101
|
+
throw new Error(`Input commitment ${toFixedHex(input.getCommitment())} was not found in merkle tree`);
|
|
102
|
+
}
|
|
103
|
+
input.index = inputIndex;
|
|
104
|
+
inputMerklePathIndices.push(inputIndex);
|
|
105
|
+
inputMerklePathElements.push(
|
|
106
|
+
tree.path(inputIndex).pathElements.map((el: string | number | bigint) => BigInt(el))
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
// Empty input (padding) - use zero path
|
|
110
|
+
inputMerklePathIndices.push(0);
|
|
111
|
+
inputMerklePathElements.push(new Array(tree.levels).fill(0));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onProgress?.('Encrypting outputs...');
|
|
116
|
+
|
|
117
|
+
// Build external data
|
|
118
|
+
const extData: ExtData = {
|
|
119
|
+
recipient: toFixedHex(recipient, 20),
|
|
120
|
+
extAmount: extAmount.toString(),
|
|
121
|
+
relayer: toFixedHex(relayer, 20),
|
|
122
|
+
fee: fee.toString(),
|
|
123
|
+
encryptedOutput1: outputs[0].encrypt(),
|
|
124
|
+
encryptedOutput2: outputs[1].encrypt(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Calculate extDataHash using raw values
|
|
128
|
+
const extDataHashInput = {
|
|
129
|
+
recipient: recipient,
|
|
130
|
+
extAmount: extAmount,
|
|
131
|
+
relayer: relayer,
|
|
132
|
+
fee: fee,
|
|
133
|
+
encryptedOutput1: extData.encryptedOutput1,
|
|
134
|
+
encryptedOutput2: extData.encryptedOutput2,
|
|
135
|
+
};
|
|
136
|
+
const extDataHash = getExtDataHash(extDataHashInput);
|
|
137
|
+
|
|
138
|
+
// Build proof input
|
|
139
|
+
const proofInput: ProofInput = {
|
|
140
|
+
root: BigInt(tree.root()),
|
|
141
|
+
inputNullifier: inputs.map((x) => x.getNullifier()),
|
|
142
|
+
outputCommitment: outputs.map((x) => x.getCommitment()),
|
|
143
|
+
publicAmount: ((BigInt(extAmount) - BigInt(fee) + FIELD_SIZE) % FIELD_SIZE).toString(),
|
|
144
|
+
extDataHash,
|
|
145
|
+
|
|
146
|
+
// Input UTXO data
|
|
147
|
+
inAmount: inputs.map((x) => x.amount),
|
|
148
|
+
inPrivateKey: inputs.map((x) => x.keypair.privkey),
|
|
149
|
+
inBlinding: inputs.map((x) => x.blinding),
|
|
150
|
+
inPathIndices: inputMerklePathIndices,
|
|
151
|
+
inPathElements: inputMerklePathElements,
|
|
152
|
+
|
|
153
|
+
// Output UTXO data
|
|
154
|
+
outAmount: outputs.map((x) => x.amount),
|
|
155
|
+
outBlinding: outputs.map((x) => x.blinding),
|
|
156
|
+
outPubkey: outputs.map((x) => x.keypair.pubkey),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
onProgress?.('Generating ZK proof...', `${inputs.length} inputs`);
|
|
160
|
+
|
|
161
|
+
// Select circuit based on input count and generate proof
|
|
162
|
+
const circuitName = selectCircuit(inputs.length);
|
|
163
|
+
const proof = await prove(proofInput, circuitName);
|
|
164
|
+
|
|
165
|
+
// Build proof arguments for on-chain verification
|
|
166
|
+
const args: ProofArgs = {
|
|
167
|
+
proof,
|
|
168
|
+
root: toFixedHex(proofInput.root),
|
|
169
|
+
inputNullifiers: inputs.map((x) => toFixedHex(x.getNullifier())),
|
|
170
|
+
outputCommitments: outputs.map((x) => toFixedHex(x.getCommitment())),
|
|
171
|
+
publicAmount: toFixedHex(proofInput.publicAmount),
|
|
172
|
+
extDataHash: toFixedHex(extDataHash),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
onProgress?.('Proof generated successfully');
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
args,
|
|
179
|
+
extData,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Prepare a transaction (withdrawal or transfer)
|
|
185
|
+
* Builds the ZK proof and external data needed for on-chain execution
|
|
186
|
+
*
|
|
187
|
+
* @param params - Transaction parameters
|
|
188
|
+
* @returns Proof arguments and external data
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* // Withdrawal: send funds to external address
|
|
193
|
+
* const result = await prepareTransaction({
|
|
194
|
+
* commitments: poolCommitments,
|
|
195
|
+
* inputs: [utxo1, utxo2],
|
|
196
|
+
* outputs: [changeUtxo], // Just change
|
|
197
|
+
* recipient: '0x1234...', // Withdrawal address
|
|
198
|
+
* fee: 0,
|
|
199
|
+
* relayer: '0x0000...',
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* // Transfer: move funds to another Veil user
|
|
203
|
+
* const result = await prepareTransaction({
|
|
204
|
+
* commitments: poolCommitments,
|
|
205
|
+
* inputs: [utxo1],
|
|
206
|
+
* outputs: [recipientUtxo, changeUtxo],
|
|
207
|
+
* recipient: 0, // No external recipient
|
|
208
|
+
* fee: 0,
|
|
209
|
+
* relayer: '0x0000...',
|
|
210
|
+
* });
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export async function prepareTransaction({
|
|
214
|
+
commitments,
|
|
215
|
+
inputs = [],
|
|
216
|
+
outputs = [],
|
|
217
|
+
fee = 0,
|
|
218
|
+
recipient = 0,
|
|
219
|
+
relayer = 0,
|
|
220
|
+
onProgress,
|
|
221
|
+
}: PrepareTransactionParams): Promise<TransactionResult> {
|
|
222
|
+
// Validate input/output counts
|
|
223
|
+
if (inputs.length > 16 || outputs.length > 2) {
|
|
224
|
+
throw new Error('Incorrect inputs/outputs count. Maximum: 16 inputs, 2 outputs.');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Pad inputs to 2 or 16 with empty UTXOs
|
|
228
|
+
while (inputs.length !== 2 && inputs.length < 16) {
|
|
229
|
+
inputs.push(new Utxo());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Pad outputs to 2 with empty UTXOs
|
|
233
|
+
while (outputs.length < 2) {
|
|
234
|
+
outputs.push(new Utxo());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Calculate external amount (fee + outputs - inputs)
|
|
238
|
+
// Positive = withdrawal, Negative = deposit
|
|
239
|
+
const extAmount = BigInt(fee) +
|
|
240
|
+
outputs.reduce((sum, x) => sum + x.amount, 0n) -
|
|
241
|
+
inputs.reduce((sum, x) => sum + x.amount, 0n);
|
|
242
|
+
|
|
243
|
+
onProgress?.('Building merkle tree...');
|
|
244
|
+
|
|
245
|
+
// Build merkle tree and generate proof
|
|
246
|
+
const tree = await buildMerkleTree(commitments);
|
|
247
|
+
|
|
248
|
+
const result = await getProof({
|
|
249
|
+
inputs,
|
|
250
|
+
outputs,
|
|
251
|
+
tree,
|
|
252
|
+
extAmount,
|
|
253
|
+
fee: BigInt(fee),
|
|
254
|
+
recipient: String(recipient),
|
|
255
|
+
relayer: String(relayer),
|
|
256
|
+
onProgress,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
}
|
package/src/transfer.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transfer functions for Veil SDK
|
|
3
|
+
* Build ZK proofs to transfer funds privately within the pool
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createPublicClient, http, parseUnits } from 'viem';
|
|
7
|
+
import { base } from 'viem/chains';
|
|
8
|
+
import { getAddresses, POOL_CONFIG } from './addresses.js';
|
|
9
|
+
import { POOL_ABI, ENTRY_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 { selectUtxosForWithdraw } from './withdraw.js';
|
|
16
|
+
import type {
|
|
17
|
+
BuildTransferProofOptions,
|
|
18
|
+
ProofBuildResult,
|
|
19
|
+
TransferResult,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a recipient is registered and get their deposit key
|
|
24
|
+
*
|
|
25
|
+
* @param address - Address to check
|
|
26
|
+
* @param rpcUrl - Optional RPC URL
|
|
27
|
+
* @returns Whether registered and their deposit key if so
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const { isRegistered, depositKey } = await checkRecipientRegistration('0x1234...');
|
|
32
|
+
* if (!isRegistered) {
|
|
33
|
+
* console.log('Recipient needs to register first');
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function checkRecipientRegistration(
|
|
38
|
+
address: `0x${string}`,
|
|
39
|
+
rpcUrl?: string
|
|
40
|
+
): Promise<{ isRegistered: boolean; depositKey?: string }> {
|
|
41
|
+
const addresses = getAddresses();
|
|
42
|
+
|
|
43
|
+
const publicClient = createPublicClient({
|
|
44
|
+
chain: base,
|
|
45
|
+
transport: http(rpcUrl),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const depositKey = await publicClient.readContract({
|
|
49
|
+
address: addresses.entry,
|
|
50
|
+
abi: ENTRY_ABI,
|
|
51
|
+
functionName: 'depositKeys',
|
|
52
|
+
args: [address],
|
|
53
|
+
}) as string;
|
|
54
|
+
|
|
55
|
+
// If depositKey exists and is not empty, recipient is registered
|
|
56
|
+
const isRegistered = !!(depositKey && depositKey !== '0x' && depositKey.length > 2);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
isRegistered,
|
|
60
|
+
depositKey: isRegistered ? depositKey : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fetch all commitments from the pool contract
|
|
66
|
+
*/
|
|
67
|
+
async function fetchCommitments(
|
|
68
|
+
rpcUrl: string | undefined,
|
|
69
|
+
poolAddress: `0x${string}`,
|
|
70
|
+
onProgress?: (stage: string, detail?: string) => void
|
|
71
|
+
): Promise<string[]> {
|
|
72
|
+
const publicClient = createPublicClient({
|
|
73
|
+
chain: base,
|
|
74
|
+
transport: http(rpcUrl),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
onProgress?.('Fetching commitment count...');
|
|
78
|
+
const nextIndex = await publicClient.readContract({
|
|
79
|
+
address: poolAddress,
|
|
80
|
+
abi: POOL_ABI,
|
|
81
|
+
functionName: 'nextIndex',
|
|
82
|
+
}) as number;
|
|
83
|
+
|
|
84
|
+
if (nextIndex === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const BATCH_SIZE = 5000;
|
|
89
|
+
const commitments: string[] = [];
|
|
90
|
+
const totalBatches = Math.ceil(nextIndex / BATCH_SIZE);
|
|
91
|
+
|
|
92
|
+
for (let start = 0; start < nextIndex; start += BATCH_SIZE) {
|
|
93
|
+
const end = Math.min(start + BATCH_SIZE, nextIndex);
|
|
94
|
+
const batchNum = Math.floor(start / BATCH_SIZE) + 1;
|
|
95
|
+
onProgress?.('Fetching commitments', `batch ${batchNum}/${totalBatches}`);
|
|
96
|
+
|
|
97
|
+
const batch = await publicClient.readContract({
|
|
98
|
+
address: poolAddress,
|
|
99
|
+
abi: POOL_ABI,
|
|
100
|
+
functionName: 'getCommitments',
|
|
101
|
+
args: [BigInt(start), BigInt(end)],
|
|
102
|
+
}) as `0x${string}`[];
|
|
103
|
+
|
|
104
|
+
commitments.push(...batch.map(c => c.toString()));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return commitments;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a transfer proof
|
|
112
|
+
*
|
|
113
|
+
* This function:
|
|
114
|
+
* 1. Verifies the recipient is registered
|
|
115
|
+
* 2. Fetches the sender's unspent UTXOs
|
|
116
|
+
* 3. Selects UTXOs to cover the transfer amount
|
|
117
|
+
* 4. Creates output UTXOs for recipient and change
|
|
118
|
+
* 5. Builds the ZK proof
|
|
119
|
+
*
|
|
120
|
+
* @param options - Transfer options
|
|
121
|
+
* @returns Proof data ready for relay submission
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const senderKeypair = new Keypair(process.env.VEIL_KEY);
|
|
126
|
+
* const proof = await buildTransferProof({
|
|
127
|
+
* amount: '0.1',
|
|
128
|
+
* recipientAddress: '0x1234...',
|
|
129
|
+
* senderKeypair,
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export async function buildTransferProof(
|
|
134
|
+
options: BuildTransferProofOptions
|
|
135
|
+
): Promise<ProofBuildResult> {
|
|
136
|
+
const {
|
|
137
|
+
amount,
|
|
138
|
+
recipientAddress,
|
|
139
|
+
senderKeypair,
|
|
140
|
+
rpcUrl,
|
|
141
|
+
onProgress,
|
|
142
|
+
} = options;
|
|
143
|
+
|
|
144
|
+
const addresses = getAddresses();
|
|
145
|
+
const poolConfig = POOL_CONFIG.eth;
|
|
146
|
+
const poolAddress = addresses.ethPool;
|
|
147
|
+
|
|
148
|
+
// 1. Check recipient is registered and get their deposit key
|
|
149
|
+
onProgress?.('Checking recipient registration...');
|
|
150
|
+
const { isRegistered, depositKey } = await checkRecipientRegistration(
|
|
151
|
+
recipientAddress,
|
|
152
|
+
rpcUrl
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!isRegistered || !depositKey) {
|
|
156
|
+
throw new Error(`Recipient ${recipientAddress} is not registered. They need to register first.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Get sender's unspent UTXOs
|
|
160
|
+
onProgress?.('Fetching your UTXOs...');
|
|
161
|
+
const balanceResult = await getPrivateBalance({
|
|
162
|
+
keypair: senderKeypair,
|
|
163
|
+
rpcUrl,
|
|
164
|
+
onProgress,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const unspentUtxoInfos = balanceResult.utxos.filter(u => !u.isSpent);
|
|
168
|
+
if (unspentUtxoInfos.length === 0) {
|
|
169
|
+
throw new Error('No unspent UTXOs available for transfer');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Re-decrypt UTXOs to get full Utxo objects
|
|
173
|
+
onProgress?.('Preparing UTXOs...');
|
|
174
|
+
|
|
175
|
+
const publicClient = createPublicClient({
|
|
176
|
+
chain: base,
|
|
177
|
+
transport: http(rpcUrl),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const utxos: Utxo[] = [];
|
|
181
|
+
for (const utxoInfo of unspentUtxoInfos) {
|
|
182
|
+
const encryptedOutputs = await publicClient.readContract({
|
|
183
|
+
address: poolAddress,
|
|
184
|
+
abi: POOL_ABI,
|
|
185
|
+
functionName: 'getEncryptedOutputs',
|
|
186
|
+
args: [BigInt(utxoInfo.index), BigInt(utxoInfo.index + 1)],
|
|
187
|
+
}) as string[];
|
|
188
|
+
|
|
189
|
+
if (encryptedOutputs.length > 0) {
|
|
190
|
+
try {
|
|
191
|
+
const utxo = Utxo.decrypt(encryptedOutputs[0], senderKeypair);
|
|
192
|
+
utxo.index = utxoInfo.index;
|
|
193
|
+
utxos.push(utxo);
|
|
194
|
+
} catch {
|
|
195
|
+
// Skip if decryption fails
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (utxos.length === 0) {
|
|
201
|
+
throw new Error('Failed to decrypt UTXOs');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 3. Select UTXOs for transfer
|
|
205
|
+
onProgress?.('Selecting UTXOs...');
|
|
206
|
+
const { selectedUtxos, changeAmount } = selectUtxosForWithdraw(
|
|
207
|
+
utxos,
|
|
208
|
+
amount,
|
|
209
|
+
poolConfig.decimals
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 4. Create output UTXOs
|
|
213
|
+
const outputs: Utxo[] = [];
|
|
214
|
+
const transferWei = parseUnits(amount, poolConfig.decimals);
|
|
215
|
+
|
|
216
|
+
// Create recipient UTXO using their deposit key
|
|
217
|
+
const recipientKeypair = Keypair.fromString(depositKey);
|
|
218
|
+
const recipientUtxo = new Utxo({
|
|
219
|
+
amount: transferWei,
|
|
220
|
+
keypair: recipientKeypair,
|
|
221
|
+
});
|
|
222
|
+
outputs.push(recipientUtxo);
|
|
223
|
+
|
|
224
|
+
// Create change UTXO for sender if needed
|
|
225
|
+
if (changeAmount > 0n) {
|
|
226
|
+
const changeUtxo = new Utxo({
|
|
227
|
+
amount: changeAmount,
|
|
228
|
+
keypair: senderKeypair,
|
|
229
|
+
});
|
|
230
|
+
outputs.push(changeUtxo);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 5. Fetch all commitments from pool
|
|
234
|
+
const commitments = await fetchCommitments(rpcUrl, poolAddress, onProgress);
|
|
235
|
+
|
|
236
|
+
// 6. Build the ZK proof
|
|
237
|
+
// For transfers, recipient field is 0x0 (no external withdrawal)
|
|
238
|
+
onProgress?.('Building ZK proof...');
|
|
239
|
+
const result = await prepareTransaction({
|
|
240
|
+
commitments,
|
|
241
|
+
inputs: selectedUtxos,
|
|
242
|
+
outputs,
|
|
243
|
+
fee: 0,
|
|
244
|
+
recipient: '0x0000000000000000000000000000000000000000',
|
|
245
|
+
relayer: '0x0000000000000000000000000000000000000000',
|
|
246
|
+
onProgress,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
proofArgs: {
|
|
251
|
+
proof: result.args.proof,
|
|
252
|
+
root: result.args.root,
|
|
253
|
+
inputNullifiers: result.args.inputNullifiers,
|
|
254
|
+
outputCommitments: result.args.outputCommitments as [string, string],
|
|
255
|
+
publicAmount: result.args.publicAmount,
|
|
256
|
+
extDataHash: result.args.extDataHash,
|
|
257
|
+
},
|
|
258
|
+
extData: result.extData,
|
|
259
|
+
inputCount: selectedUtxos.length,
|
|
260
|
+
outputCount: outputs.length,
|
|
261
|
+
amount,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute a transfer by building proof and submitting to relay
|
|
267
|
+
*
|
|
268
|
+
* @param options - Transfer options
|
|
269
|
+
* @returns Transfer result with transaction hash
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* const senderKeypair = new Keypair(process.env.VEIL_KEY);
|
|
274
|
+
* const result = await transfer({
|
|
275
|
+
* amount: '0.1',
|
|
276
|
+
* recipientAddress: '0x1234...',
|
|
277
|
+
* senderKeypair,
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* console.log(`Transfer tx: ${result.transactionHash}`);
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
export async function transfer(
|
|
284
|
+
options: BuildTransferProofOptions
|
|
285
|
+
): Promise<TransferResult> {
|
|
286
|
+
const { amount, recipientAddress, onProgress } = options;
|
|
287
|
+
|
|
288
|
+
// Build the proof
|
|
289
|
+
const proof = await buildTransferProof(options);
|
|
290
|
+
|
|
291
|
+
// Submit to relay
|
|
292
|
+
onProgress?.('Submitting to relay...');
|
|
293
|
+
const relayResult = await submitRelay({
|
|
294
|
+
type: 'transfer',
|
|
295
|
+
pool: 'eth',
|
|
296
|
+
proofArgs: proof.proofArgs,
|
|
297
|
+
extData: proof.extData,
|
|
298
|
+
metadata: {
|
|
299
|
+
amount,
|
|
300
|
+
recipient: recipientAddress,
|
|
301
|
+
inputUtxoCount: proof.inputCount,
|
|
302
|
+
outputUtxoCount: proof.outputCount,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
success: relayResult.success,
|
|
308
|
+
transactionHash: relayResult.transactionHash,
|
|
309
|
+
blockNumber: relayResult.blockNumber,
|
|
310
|
+
amount,
|
|
311
|
+
recipient: recipientAddress,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Merge UTXOs by doing a self-transfer
|
|
317
|
+
* This consolidates multiple small UTXOs into fewer larger ones
|
|
318
|
+
*
|
|
319
|
+
* Unlike regular transfers, merge doesn't need a recipient address -
|
|
320
|
+
* it uses the sender's keypair directly to create the output UTXO.
|
|
321
|
+
*
|
|
322
|
+
* @param options - Merge options
|
|
323
|
+
* @returns Transfer result
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* const keypair = new Keypair(process.env.VEIL_KEY);
|
|
328
|
+
* const result = await mergeUtxos({
|
|
329
|
+
* amount: '0.5', // Total amount to consolidate
|
|
330
|
+
* keypair,
|
|
331
|
+
* });
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
export async function mergeUtxos(options: {
|
|
335
|
+
amount: string;
|
|
336
|
+
keypair: Keypair;
|
|
337
|
+
rpcUrl?: string;
|
|
338
|
+
onProgress?: (stage: string, detail?: string) => void;
|
|
339
|
+
}): Promise<TransferResult> {
|
|
340
|
+
const { amount, keypair, rpcUrl, onProgress } = options;
|
|
341
|
+
|
|
342
|
+
const addresses = getAddresses();
|
|
343
|
+
const poolConfig = POOL_CONFIG.eth;
|
|
344
|
+
const poolAddress = addresses.ethPool;
|
|
345
|
+
|
|
346
|
+
// 1. Get sender's unspent UTXOs
|
|
347
|
+
onProgress?.('Fetching your UTXOs...');
|
|
348
|
+
const balanceResult = await getPrivateBalance({
|
|
349
|
+
keypair,
|
|
350
|
+
rpcUrl,
|
|
351
|
+
onProgress,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const unspentUtxoInfos = balanceResult.utxos.filter(u => !u.isSpent);
|
|
355
|
+
if (unspentUtxoInfos.length === 0) {
|
|
356
|
+
throw new Error('No unspent UTXOs available for merge');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Re-decrypt UTXOs to get full Utxo objects
|
|
360
|
+
onProgress?.('Preparing UTXOs...');
|
|
361
|
+
|
|
362
|
+
const publicClient = createPublicClient({
|
|
363
|
+
chain: base,
|
|
364
|
+
transport: http(rpcUrl),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const utxos: Utxo[] = [];
|
|
368
|
+
for (const utxoInfo of unspentUtxoInfos) {
|
|
369
|
+
const encryptedOutputs = await publicClient.readContract({
|
|
370
|
+
address: poolAddress,
|
|
371
|
+
abi: POOL_ABI,
|
|
372
|
+
functionName: 'getEncryptedOutputs',
|
|
373
|
+
args: [BigInt(utxoInfo.index), BigInt(utxoInfo.index + 1)],
|
|
374
|
+
}) as string[];
|
|
375
|
+
|
|
376
|
+
if (encryptedOutputs.length > 0) {
|
|
377
|
+
try {
|
|
378
|
+
const utxo = Utxo.decrypt(encryptedOutputs[0], keypair);
|
|
379
|
+
utxo.index = utxoInfo.index;
|
|
380
|
+
utxos.push(utxo);
|
|
381
|
+
} catch {
|
|
382
|
+
// Skip if decryption fails
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (utxos.length === 0) {
|
|
388
|
+
throw new Error('Failed to decrypt UTXOs');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 2. Select UTXOs for merge
|
|
392
|
+
onProgress?.('Selecting UTXOs...');
|
|
393
|
+
const { selectedUtxos, changeAmount } = selectUtxosForWithdraw(
|
|
394
|
+
utxos,
|
|
395
|
+
amount,
|
|
396
|
+
poolConfig.decimals
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// 3. Create output UTXO - use sender's keypair directly (self-transfer)
|
|
400
|
+
const outputs: Utxo[] = [];
|
|
401
|
+
const mergeWei = parseUnits(amount, poolConfig.decimals);
|
|
402
|
+
|
|
403
|
+
// Create merged UTXO using own keypair
|
|
404
|
+
const mergedUtxo = new Utxo({
|
|
405
|
+
amount: mergeWei,
|
|
406
|
+
keypair: keypair,
|
|
407
|
+
});
|
|
408
|
+
outputs.push(mergedUtxo);
|
|
409
|
+
|
|
410
|
+
// Create change UTXO if needed
|
|
411
|
+
if (changeAmount > 0n) {
|
|
412
|
+
const changeUtxo = new Utxo({
|
|
413
|
+
amount: changeAmount,
|
|
414
|
+
keypair: keypair,
|
|
415
|
+
});
|
|
416
|
+
outputs.push(changeUtxo);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 4. Fetch all commitments from pool
|
|
420
|
+
const commitments = await fetchCommitments(rpcUrl, poolAddress, onProgress);
|
|
421
|
+
|
|
422
|
+
// 5. Build the ZK proof (recipient = 0x0 for self-transfer)
|
|
423
|
+
onProgress?.('Building ZK proof...');
|
|
424
|
+
const result = await prepareTransaction({
|
|
425
|
+
commitments,
|
|
426
|
+
inputs: selectedUtxos,
|
|
427
|
+
outputs,
|
|
428
|
+
fee: 0,
|
|
429
|
+
recipient: '0x0000000000000000000000000000000000000000',
|
|
430
|
+
relayer: '0x0000000000000000000000000000000000000000',
|
|
431
|
+
onProgress,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// 6. Submit to relay
|
|
435
|
+
onProgress?.('Submitting to relay...');
|
|
436
|
+
const relayResult = await submitRelay({
|
|
437
|
+
type: 'transfer',
|
|
438
|
+
pool: 'eth',
|
|
439
|
+
proofArgs: {
|
|
440
|
+
proof: result.args.proof,
|
|
441
|
+
root: result.args.root,
|
|
442
|
+
inputNullifiers: result.args.inputNullifiers,
|
|
443
|
+
outputCommitments: result.args.outputCommitments as [string, string],
|
|
444
|
+
publicAmount: result.args.publicAmount,
|
|
445
|
+
extDataHash: result.args.extDataHash,
|
|
446
|
+
},
|
|
447
|
+
extData: result.extData,
|
|
448
|
+
metadata: {
|
|
449
|
+
amount,
|
|
450
|
+
inputUtxoCount: selectedUtxos.length,
|
|
451
|
+
outputUtxoCount: outputs.length,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
success: relayResult.success,
|
|
457
|
+
transactionHash: relayResult.transactionHash,
|
|
458
|
+
blockNumber: relayResult.blockNumber,
|
|
459
|
+
amount,
|
|
460
|
+
recipient: 'self',
|
|
461
|
+
};
|
|
462
|
+
}
|