@veilo/sdk-core 0.1.17

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/src/client.ts ADDED
@@ -0,0 +1,352 @@
1
+ import * as anchor from "@coral-xyz/anchor";
2
+ import { Program, BN, Idl } from "@coral-xyz/anchor";
3
+ import { PublicKey, SystemProgram, Keypair } from "@solana/web3.js";
4
+
5
+ import {
6
+ ProofBuilder,
7
+ WithdrawCircuitInputs,
8
+ WithdrawProofStruct,
9
+ deriveNullifier,
10
+ encodeSnarkjsProofToWithdrawProof,
11
+ } from "./proof";
12
+
13
+ import {
14
+ createNoteWithCommitment,
15
+ EncryptedNotePlaintext,
16
+ encryptNote,
17
+ SerializedNote,
18
+ } from "./note";
19
+
20
+ import { MerklePath, MerkleTree } from "./merkle";
21
+ import { randomBytes } from "crypto";
22
+
23
+ // -----------------------------------------------------------------------------
24
+ // Types
25
+ // -----------------------------------------------------------------------------
26
+
27
+ export type NoteAndMerkleData = {
28
+ note: SerializedNote;
29
+ leafIndex: number;
30
+ root: Uint8Array;
31
+ merklePath: MerklePath;
32
+ };
33
+
34
+ // -----------------------------------------------------------------------------
35
+ // High-level deposit helper (off-chain Merkle is just for zk / demos)
36
+ // -----------------------------------------------------------------------------
37
+
38
+ /**
39
+ * High-level helper:
40
+ * - Creates a random note + commitment
41
+ * - Inserts the commitment into an *off-chain* Merkle tree (your zk tree)
42
+ * - Calls on-chain `depositFixed` with the commitment
43
+ * - Returns note + index + Merkle path (+ off-chain root)
44
+ *
45
+ * The on-chain program *also* keeps its own rolling 32-byte root, but that is
46
+ * intentionally minimal and independent from this off-chain tree. For a real
47
+ * zk circuit you’d want both to share the same hash function.
48
+ */
49
+ export async function createNoteDepositWithMerkle<T extends Idl>(params: {
50
+ program: Program<T>;
51
+ depositor: anchor.Wallet;
52
+ denomIndex: number;
53
+ valueLamports: bigint;
54
+ tree: MerkleTree;
55
+ }): Promise<NoteAndMerkleData> {
56
+ const { program, depositor, denomIndex, valueLamports, tree } = params;
57
+
58
+ const note = createNoteWithCommitment({
59
+ value: valueLamports,
60
+ owner: depositor.publicKey,
61
+ });
62
+
63
+ // Maintain your off-chain mirror (for proofs)
64
+ const { index, root } = tree.insert(note.commitment);
65
+ const merklePath = tree.getPath(index);
66
+
67
+ // Call low-level deposit with just the commitment
68
+ await depositFixedSol({
69
+ program,
70
+ depositor,
71
+ denomIndex,
72
+ commitment: note.commitment,
73
+ note,
74
+ });
75
+
76
+ return {
77
+ note,
78
+ leafIndex: index,
79
+ root,
80
+ merklePath,
81
+ };
82
+ }
83
+
84
+ // -----------------------------------------------------------------------------
85
+ // PDA helpers (v3 seeds)
86
+ // -----------------------------------------------------------------------------
87
+
88
+ export function getPoolPdas(programId: PublicKey, commitmentSeed?: Uint8Array) {
89
+ const [config] = PublicKey.findProgramAddressSync(
90
+ [Buffer.from("privacy_config_v3")],
91
+ programId
92
+ );
93
+ const [vault] = PublicKey.findProgramAddressSync(
94
+ [Buffer.from("privacy_vault_v3")],
95
+ programId
96
+ );
97
+ const [noteTree] = PublicKey.findProgramAddressSync(
98
+ [Buffer.from("privacy_note_tree_v3")],
99
+ programId
100
+ );
101
+ const [nullifiers] = PublicKey.findProgramAddressSync(
102
+ [Buffer.from("privacy_nullifiers_v3")],
103
+ programId
104
+ );
105
+
106
+ const [noteHint] = PublicKey.findProgramAddressSync(
107
+ [Buffer.from("privacy_note_hint_v3"), commitmentSeed ?? Buffer.alloc(0)],
108
+ programId
109
+ );
110
+
111
+ return { config, vault, noteTree, nullifiers, noteHint };
112
+ }
113
+
114
+ // -----------------------------------------------------------------------------
115
+ // Pool initialization
116
+ // -----------------------------------------------------------------------------
117
+
118
+ export async function initializePool<T extends Idl>(params: {
119
+ program: Program<T>;
120
+ admin: anchor.Wallet;
121
+ denomsLamports: (bigint | number)[];
122
+ feeBps: number; // 0–10000
123
+ }) {
124
+ const { program, admin, denomsLamports, feeBps } = params;
125
+ const { config, vault, noteTree, nullifiers } = getPoolPdas(
126
+ program.programId
127
+ );
128
+
129
+ await (program.methods as any)
130
+ .initialize(
131
+ denomsLamports.map((d) => new BN(d.toString())),
132
+ feeBps
133
+ )
134
+ .accounts({
135
+ config,
136
+ vault,
137
+ noteTree,
138
+ nullifiers,
139
+ admin: admin.publicKey,
140
+ systemProgram: SystemProgram.programId,
141
+ } as any)
142
+ .rpc();
143
+ }
144
+
145
+ // -----------------------------------------------------------------------------
146
+ // Deposit helpers
147
+ // -----------------------------------------------------------------------------
148
+
149
+ export type DepositNoteResult = {
150
+ commitment: Uint8Array; // 32 bytes
151
+ };
152
+
153
+ /**
154
+ * Low-level deposit helper.
155
+ * Expects a 32-byte commitment; on-chain NoteTree keeps a rolling 32-byte root.
156
+ */
157
+ export async function depositFixedSol<T extends Idl>(params: {
158
+ program: Program<T>;
159
+ depositor: anchor.Wallet;
160
+ denomIndex: number;
161
+ commitment: Uint8Array; // 32
162
+ note: SerializedNote;
163
+ }) {
164
+ const { program, depositor, denomIndex, commitment, note } = params;
165
+ const { config, vault, noteTree, noteHint } = getPoolPdas(
166
+ program.programId,
167
+ commitment
168
+ );
169
+
170
+ if (commitment.length !== 32) {
171
+ throw new Error("commitment must be 32 bytes");
172
+ }
173
+
174
+ const notePlain: EncryptedNotePlaintext = {
175
+ secret: randomBytes(32),
176
+ nullifier: deriveNullifier(note),
177
+ denomIndex: 0,
178
+ };
179
+
180
+ const sharedKey = randomBytes(32); // derive from recipient pubkey in real wallet
181
+ const encryptedNote = encryptNote(notePlain, sharedKey);
182
+
183
+ await (program.methods as any)
184
+ .depositFixed(
185
+ denomIndex,
186
+ Array.from(commitment) // [u8;32]
187
+ )
188
+ .accounts({
189
+ config,
190
+ vault,
191
+ noteTree,
192
+ noteHint,
193
+ depositor: depositor.publicKey,
194
+ systemProgram: SystemProgram.programId,
195
+ } as any)
196
+ .rpc();
197
+ }
198
+
199
+ /**
200
+ * Convenience: build a random Note off-chain, commit it, and call deposit.
201
+ */
202
+ export async function createNoteAndDeposit<T extends Idl>(params: {
203
+ program: Program<T>;
204
+ depositor: anchor.Wallet;
205
+ denomIndex: number;
206
+ valueLamports: bigint;
207
+ }) {
208
+ const { program, depositor, denomIndex, valueLamports } = params;
209
+
210
+ const note = createNoteWithCommitment({
211
+ value: valueLamports,
212
+ owner: depositor.publicKey,
213
+ });
214
+
215
+ await depositFixedSol({
216
+ program,
217
+ depositor,
218
+ denomIndex,
219
+ commitment: note.commitment,
220
+ note,
221
+ });
222
+
223
+ return note;
224
+ }
225
+
226
+ // -----------------------------------------------------------------------------
227
+ // Relayers & Withdrawals
228
+ // -----------------------------------------------------------------------------
229
+
230
+ export async function addRelayer<T extends Idl>(params: {
231
+ program: Program<T>;
232
+ admin: anchor.Wallet;
233
+ newRelayer: PublicKey;
234
+ }) {
235
+ const { program, admin, newRelayer } = params;
236
+ const { config } = getPoolPdas(program.programId);
237
+
238
+ await (program.methods as any)
239
+ .addRelayer(newRelayer)
240
+ .accounts({
241
+ config,
242
+ admin: admin.publicKey,
243
+ } as any)
244
+ .rpc();
245
+ }
246
+
247
+ /**
248
+ * Low-level withdraw:
249
+ * - Expects the **already packed** WithdrawProofStruct
250
+ * (matching Rust `WithdrawProof` fields).
251
+ */
252
+ export async function withdrawViaRelayer<T extends Idl>(params: {
253
+ program: Program<T>;
254
+ relayer: Keypair;
255
+ recipient: PublicKey;
256
+ denomIndex: number;
257
+ root: Uint8Array; // 32 bytes
258
+ nullifier: Uint8Array; // 32 bytes
259
+ proof: WithdrawProofStruct;
260
+ }) {
261
+ const { program, relayer, recipient, denomIndex, root, nullifier, proof } =
262
+ params;
263
+ const { config, vault, noteTree, nullifiers } = getPoolPdas(
264
+ program.programId
265
+ );
266
+
267
+ if (root.length !== 32) throw new Error("root must be 32 bytes");
268
+ if (nullifier.length !== 32) throw new Error("nullifier must be 32 bytes");
269
+
270
+ await (program.methods as any)
271
+ .withdraw(
272
+ Array.from(root),
273
+ Array.from(nullifier),
274
+ denomIndex,
275
+ recipient,
276
+ proof // <- Anchor / IDL maps this to WithdrawProof { proof_a, proof_b, proof_c }
277
+ )
278
+ .accounts({
279
+ config,
280
+ vault,
281
+ noteTree,
282
+ nullifiers,
283
+ relayer: relayer.publicKey,
284
+ recipient,
285
+ systemProgram: SystemProgram.programId,
286
+ } as any)
287
+ .signers([relayer])
288
+ .rpc();
289
+ }
290
+
291
+ /**
292
+ * High-level withdraw:
293
+ * - Takes note + Merkle path + denom + fee info
294
+ * - Uses a pluggable ProofBuilder (snarkjs.groth16) to create a RawProof
295
+ * - Encodes RawProof into WithdrawProofStruct
296
+ * - Calls the low-level withdrawViaRelayer
297
+ *
298
+ * In production, you'd typically call a relayer HTTP API instead, and let it
299
+ * build/verify the proof server-side. But this is handy for tools / tests.
300
+ */
301
+ export async function withdrawViaRelayerWithProof<T extends Idl>(params: {
302
+ program: Program<T>;
303
+ relayer: Keypair;
304
+ recipient: PublicKey;
305
+ denomIndex: number;
306
+ feeBps: number;
307
+ root: Uint8Array;
308
+ nullifier: Uint8Array;
309
+ noteData: SerializedNote;
310
+ merklePath: MerklePath;
311
+ builder: ProofBuilder; // must produce a valid snarkjs Groth16 proof
312
+ }) {
313
+ const {
314
+ program,
315
+ relayer,
316
+ recipient,
317
+ denomIndex,
318
+ feeBps,
319
+ root,
320
+ nullifier,
321
+ noteData,
322
+ merklePath,
323
+ builder,
324
+ } = params;
325
+
326
+ const inputs: WithdrawCircuitInputs = {
327
+ root,
328
+ note: noteData,
329
+ merklePath,
330
+ nullifier,
331
+ denomIndex,
332
+ recipient,
333
+ relayer: relayer.publicKey,
334
+ feeBps,
335
+ };
336
+
337
+ // Build snarkjs proof off-chain
338
+ const rawProof = await builder(inputs);
339
+
340
+ // Pack into the struct expected by the on-chain Rust program
341
+ const withdrawProof = encodeSnarkjsProofToWithdrawProof(rawProof);
342
+
343
+ await withdrawViaRelayer({
344
+ program,
345
+ relayer,
346
+ recipient,
347
+ denomIndex,
348
+ root,
349
+ nullifier,
350
+ proof: withdrawProof,
351
+ });
352
+ }
package/src/config.ts ADDED
@@ -0,0 +1,13 @@
1
+ // config.ts
2
+ import { LAMPORTS_PER_SOL } from "@solana/web3.js";
3
+
4
+ export const DEFAULT_FEE_BPS = 50; // 0.5%
5
+
6
+ export function sol(n: number): bigint {
7
+ return BigInt(n) * BigInt(LAMPORTS_PER_SOL);
8
+ }
9
+
10
+ export type PoolInitConfig = {
11
+ denomsSol: number[]; // [1, 5] etc
12
+ feeBps?: number;
13
+ };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./client";
2
+ export * from "./note";
3
+ export * from "./config";
4
+ export * from "./merkle";
5
+ export * from "./proof";
6
+ export { initPoseidon } from "./poseidon";
package/src/merkle.ts ADDED
@@ -0,0 +1,178 @@
1
+ // sdk-core/src/merkle.ts
2
+ import {
3
+ BN254_FR_MODULUS,
4
+ bytesToBigIntBE,
5
+ bigIntToBytesBE,
6
+ } from "./note";
7
+ import {
8
+ getPoseidon,
9
+ frToBigInt,
10
+ } from "./poseidon";
11
+
12
+ export type MerklePath = {
13
+ /** Index of the leaf in the tree (0-based) */
14
+ leafIndex: number;
15
+ /** Sibling hashes from leaf level up to (but not including) the root */
16
+ path: Uint8Array[];
17
+ /**
18
+ * 0 = current node is left child, sibling is right
19
+ * 1 = current node is right child, sibling is left
20
+ * (matches your Circom `pathIndices` semantics)
21
+ */
22
+ indices: number[];
23
+ };
24
+
25
+ export type Bytes32 = Uint8Array;
26
+
27
+ // Poseidon-based hash of two 32-byte field elements.
28
+ function hashPair(left: Bytes32, right: Bytes32): Bytes32 {
29
+ if (left.length !== 32 || right.length !== 32) {
30
+ throw new Error("hashPair expects 32-byte inputs");
31
+ }
32
+
33
+ const poseidon = getPoseidon();
34
+
35
+ const lf = bytesToBigIntBE(left) % BN254_FR_MODULUS;
36
+ const rf = bytesToBigIntBE(right) % BN254_FR_MODULUS;
37
+
38
+ const fe = poseidon([lf, rf]);
39
+ const outBig = frToBigInt(fe) % BN254_FR_MODULUS;
40
+
41
+ return bigIntToBytesBE(outBig);
42
+ }
43
+
44
+ /**
45
+ * Fixed-depth binary Merkle tree over BN254 Fr:
46
+ * - Leaves and internal nodes are 32-byte big-endian Fr elements.
47
+ * - Hash is Poseidon(2)(left, right).
48
+ * - Unfilled leaves use the Poseidon zero chain:
49
+ * zero[0] = 0
50
+ * zero[i+1] = Poseidon(zero[i], zero[i])
51
+ *
52
+ * Depth must match:
53
+ * - Circom WithdrawCircuit(depth)
54
+ * - Rust MERKLE_TREE_HEIGHT
55
+ */
56
+ export class MerkleTree {
57
+ readonly depth: number;
58
+ readonly zeroes: Uint8Array[];
59
+ readonly layers: Uint8Array[][];
60
+ nextIndex: number;
61
+
62
+ constructor(depth = 16) {
63
+ this.depth = depth;
64
+ this.zeroes = [];
65
+ this.layers = [];
66
+
67
+ // zero[0] = 0 (Fr), then zero[i+1] = Poseidon(zero[i], zero[i])
68
+ let zero = bigIntToBytesBE(0n);
69
+ this.zeroes.push(zero);
70
+
71
+ for (let level = 1; level <= depth; level++) {
72
+ zero = hashPair(zero, zero);
73
+ this.zeroes.push(zero);
74
+ }
75
+
76
+ // Allocate layers:
77
+ // level 0: 2^depth leaves
78
+ // level 1: 2^(depth-1) parents
79
+ // ...
80
+ // level depth: 1 root
81
+ for (let level = 0; level <= depth; level++) {
82
+ const size = 1 << (depth - level);
83
+
84
+ if (!Number.isSafeInteger(size) || size <= 0) {
85
+ throw new Error(
86
+ `Invalid MerkleTree depth=${depth}, level=${level}, size=${size}`,
87
+ );
88
+ }
89
+
90
+ const arr: Uint8Array[] = new Array(size);
91
+ for (let i = 0; i < size; i++) {
92
+ arr[i] = this.zeroes[level];
93
+ }
94
+ this.layers.push(arr);
95
+ }
96
+
97
+ this.nextIndex = 0;
98
+ }
99
+
100
+ get capacity(): number {
101
+ return 1 << this.depth;
102
+ }
103
+
104
+ get root(): Uint8Array {
105
+ return this.layers[this.depth][0];
106
+ }
107
+
108
+ /**
109
+ * Insert a leaf and recompute the path up to the root.
110
+ * Returns the leaf index and the new root.
111
+ */
112
+ insert(leaf: Uint8Array): { index: number; root: Uint8Array } {
113
+ if (leaf.length !== 32) {
114
+ throw new Error("leaf must be 32 bytes");
115
+ }
116
+ const index = this.nextIndex;
117
+ if (index >= this.capacity) {
118
+ throw new Error("Merkle tree is full");
119
+ }
120
+
121
+ // Set leaf
122
+ this.layers[0][index] = leaf;
123
+
124
+ // Bubble up
125
+ let idx = index;
126
+ for (let level = 1; level <= this.depth; level++) {
127
+ const parentIndex = Math.floor(idx / 2);
128
+ const leftIndex = parentIndex * 2;
129
+ const rightIndex = leftIndex + 1;
130
+
131
+ const left = this.layers[level - 1][leftIndex];
132
+ const right =
133
+ rightIndex < this.layers[level - 1].length
134
+ ? this.layers[level - 1][rightIndex]
135
+ : this.zeroes[level - 1];
136
+
137
+ this.layers[level][parentIndex] = hashPair(left, right);
138
+ idx = parentIndex;
139
+ }
140
+
141
+ this.nextIndex++;
142
+ return { index, root: this.root };
143
+ }
144
+
145
+ /**
146
+ * Return Merkle proof (path + indices) for a given leaf index.
147
+ * This feeds directly into the Circom `MerklePathVerifier(depth)`:
148
+ * - `pathElements[i]` = sibling
149
+ * - `pathIndices[i]` = 0/1 as defined above
150
+ */
151
+ getPath(index: number): MerklePath {
152
+ if (index < 0 || index >= this.capacity) {
153
+ throw new Error("index out of range");
154
+ }
155
+
156
+ const path: Uint8Array[] = [];
157
+ const indices: number[] = [];
158
+
159
+ let idx = index;
160
+ for (let level = 0; level < this.depth; level++) {
161
+ const isRight = idx % 2 === 1;
162
+ const siblingIndex = isRight ? idx - 1 : idx + 1;
163
+
164
+ const sibling =
165
+ siblingIndex < this.layers[level].length
166
+ ? this.layers[level][siblingIndex]
167
+ : this.zeroes[level];
168
+
169
+ path.push(sibling);
170
+ // 0 = current is left, 1 = current is right
171
+ indices.push(isRight ? 1 : 0);
172
+
173
+ idx = Math.floor(idx / 2);
174
+ }
175
+
176
+ return { leafIndex: index, path, indices };
177
+ }
178
+ }