@veilo/sdk-core 0.1.17 → 0.3.3

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.
Files changed (52) hide show
  1. package/README.md +1257 -272
  2. package/dist/cjs/client.d.ts +407 -0
  3. package/dist/cjs/client.js +914 -0
  4. package/dist/cjs/config.d.ts +82 -0
  5. package/dist/cjs/config.js +57 -0
  6. package/dist/cjs/events.d.ts +77 -0
  7. package/dist/cjs/events.js +167 -0
  8. package/dist/cjs/idl/privacy_pool.json +10313 -0
  9. package/dist/cjs/index.d.ts +13 -0
  10. package/dist/cjs/index.js +57 -0
  11. package/dist/cjs/merkle.d.ts +64 -0
  12. package/dist/cjs/merkle.js +133 -0
  13. package/dist/cjs/poseidon.d.ts +29 -0
  14. package/dist/cjs/poseidon.js +100 -0
  15. package/dist/cjs/program.d.ts +26 -0
  16. package/dist/cjs/program.js +38 -0
  17. package/dist/cjs/proof.d.ts +183 -0
  18. package/dist/cjs/proof.js +292 -0
  19. package/dist/cjs/prover.d.ts +54 -0
  20. package/dist/cjs/prover.js +112 -0
  21. package/dist/cjs/relayer.d.ts +295 -0
  22. package/dist/cjs/relayer.js +246 -0
  23. package/dist/cjs/retry.d.ts +32 -0
  24. package/dist/cjs/retry.js +75 -0
  25. package/dist/cjs/utxo.d.ts +215 -0
  26. package/dist/cjs/utxo.js +394 -0
  27. package/dist/esm/client.js +887 -0
  28. package/dist/esm/config.js +51 -0
  29. package/dist/esm/events.js +129 -0
  30. package/dist/esm/idl/privacy_pool.json +10313 -0
  31. package/dist/esm/index.js +22 -0
  32. package/dist/esm/merkle.js +129 -0
  33. package/dist/esm/poseidon.js +87 -0
  34. package/dist/esm/program.js +31 -0
  35. package/dist/esm/proof.js +281 -0
  36. package/dist/esm/prover.js +75 -0
  37. package/dist/esm/relayer.js +238 -0
  38. package/dist/esm/retry.js +71 -0
  39. package/dist/esm/utxo.js +372 -0
  40. package/package.json +47 -11
  41. package/src/client.ts +0 -352
  42. package/src/config.ts +0 -13
  43. package/src/index.ts +0 -6
  44. package/src/merkle.ts +0 -178
  45. package/src/note.ts +0 -193
  46. package/src/poseidon.ts +0 -62
  47. package/src/proof.ts +0 -170
  48. package/test/script.js +0 -0
  49. package/test-tsconfig.json +0 -19
  50. package/tests/note.test.ts +0 -50
  51. package/tests/sdk.integration.test.ts +0 -210
  52. package/tsconfig.json +0 -18
package/src/note.ts DELETED
@@ -1,193 +0,0 @@
1
- // sdk-core/src/note.ts
2
- import { PublicKey } from "@solana/web3.js";
3
- import {
4
- createCipheriv,
5
- createDecipheriv,
6
- createHash,
7
- randomBytes,
8
- } from "crypto";
9
- import { frToBigInt, getPoseidon } from "./poseidon";
10
-
11
- export type Note = {
12
- value: bigint; // u64 on-chain, bigint off-chain
13
- owner: PublicKey; // owner public key (later: spend key)
14
- rho: Uint8Array; // 32 bytes random
15
- r: Uint8Array; // 32 bytes random blinding
16
- };
17
-
18
- export type SerializedNote = {
19
- value: bigint;
20
- owner: PublicKey;
21
- rho: Uint8Array;
22
- r: Uint8Array;
23
- commitment: Uint8Array; // 32 bytes, BN254 Fr element
24
- };
25
-
26
- export type EncryptedNotePlaintext = {
27
- secret: Uint8Array; // 32 bytes
28
- nullifier: Uint8Array; // 32 bytes
29
- denomIndex: number; // 1 byte
30
- };
31
-
32
- export function encodeEncryptedNote(note: EncryptedNotePlaintext): Uint8Array {
33
- const buf = new Uint8Array(32 + 32 + 1);
34
- buf.set(note.secret, 0);
35
- buf.set(note.nullifier, 32);
36
- buf[64] = note.denomIndex;
37
- return buf;
38
- }
39
-
40
- // Decode back
41
- export function decodeEncryptedNote(buf: Uint8Array): EncryptedNotePlaintext {
42
- return {
43
- secret: buf.slice(0, 32),
44
- nullifier: buf.slice(32, 64),
45
- denomIndex: buf[64],
46
- };
47
- }
48
-
49
- export function encryptNote(
50
- plaintext: EncryptedNotePlaintext,
51
- sharedKey: Uint8Array // 32 bytes symmetric key (derived from recipient pubkey)
52
- ): Uint8Array {
53
- const iv = randomBytes(12); // ChaCha20/Poly1305 nonce
54
- const cipher = createCipheriv("chacha20-poly1305", sharedKey, iv, {
55
- authTagLength: 16,
56
- });
57
- const encoded = encodeEncryptedNote(plaintext);
58
- const encrypted = Buffer.concat([cipher.update(encoded), cipher.final()]);
59
- const authTag = cipher.getAuthTag();
60
-
61
- // Return iv + ciphertext + tag
62
- return Buffer.concat([iv, encrypted, authTag]);
63
- }
64
-
65
- export function decryptNote(
66
- ciphertext: Uint8Array,
67
- sharedKey: Uint8Array
68
- ): EncryptedNotePlaintext {
69
- const iv = ciphertext.slice(0, 12);
70
- const authTag = ciphertext.slice(ciphertext.length - 16);
71
- const enc = ciphertext.slice(12, ciphertext.length - 16);
72
-
73
- const decipher = createDecipheriv("chacha20-poly1305", sharedKey, iv, {
74
- authTagLength: 16,
75
- });
76
- decipher.setAuthTag(authTag);
77
- const decrypted = Buffer.concat([decipher.update(enc), decipher.final()]);
78
-
79
- return decodeEncryptedNote(decrypted);
80
- }
81
-
82
- function createOwnerHint(pubkey: PublicKey): Uint8Array {
83
- // Simple: hash of recipient encryption pubkey
84
- return createHash("sha256").update(pubkey.toBytes()).digest();
85
- }
86
-
87
- // -----------------------------------------------------------------------------
88
- // BN254 scalar field helpers (shared with merkle/proof)
89
- // -----------------------------------------------------------------------------
90
-
91
- // Standard BN254 scalar field modulus (same Fr as circom / Groth16)
92
- export const BN254_FR_MODULUS = BigInt(
93
- "21888242871839275222246405745257275088548364400416034343698204186575808495617"
94
- );
95
-
96
- export function bytesToBigIntBE(bytes: Uint8Array): bigint {
97
- let x = 0n;
98
- for (const b of bytes) {
99
- x = (x << 8n) | BigInt(b);
100
- }
101
- return x;
102
- }
103
-
104
- export function bigIntToBytesBE(x: bigint): Uint8Array {
105
- const out = new Uint8Array(32);
106
- let v = x;
107
- for (let i = 31; i >= 0; i--) {
108
- out[i] = Number(v & 0xffn);
109
- v >>= 8n;
110
- }
111
- return out;
112
- }
113
-
114
- // -----------------------------------------------------------------------------
115
- // Note construction
116
- // -----------------------------------------------------------------------------
117
-
118
- export function createRandomNote(params: {
119
- value: bigint;
120
- owner: PublicKey;
121
- }): Note {
122
- return {
123
- value: params.value,
124
- owner: params.owner,
125
- rho: new Uint8Array(randomBytes(32)),
126
- r: new Uint8Array(randomBytes(32)),
127
- };
128
- }
129
-
130
- /**
131
- * Optional helper: deterministic encoding of a note into bytes:
132
- * value (8 bytes, LE) || owner (32 bytes) || rho (32) || r (32)
133
- * Kept for compatibility / debugging.
134
- */
135
- export function encodeNoteToBytes(note: Note): Uint8Array {
136
- const valueBuf = Buffer.alloc(8);
137
- valueBuf.writeBigUInt64LE(note.value);
138
-
139
- const ownerBuf = note.owner.toBytes();
140
- if (ownerBuf.length !== 32) {
141
- throw new Error("Owner pubkey must be 32 bytes");
142
- }
143
- if (note.rho.length !== 32 || note.r.length !== 32) {
144
- throw new Error("rho and r must be 32 bytes each");
145
- }
146
-
147
- return Buffer.concat([
148
- valueBuf,
149
- Buffer.from(ownerBuf),
150
- Buffer.from(note.rho),
151
- Buffer.from(note.r),
152
- ]);
153
- }
154
-
155
- /**
156
- * Circom-compatible commitment:
157
- * commitment = Poseidon(value, owner, rho, r)
158
- *
159
- * All arguments are reduced into BN254 Fr before hashing.
160
- * This must match your Circom `NoteCommitment` template.
161
- */
162
- export function commitNote(note: Note): Uint8Array {
163
- const poseidon = getPoseidon();
164
-
165
- const valueField = note.value % BN254_FR_MODULUS;
166
-
167
- const ownerField = bytesToBigIntBE(note.owner.toBytes()) % BN254_FR_MODULUS;
168
-
169
- const rhoField = bytesToBigIntBE(note.rho) % BN254_FR_MODULUS;
170
-
171
- const rField = bytesToBigIntBE(note.r) % BN254_FR_MODULUS;
172
-
173
- const fe = poseidon([valueField, ownerField, rhoField, rField]);
174
- const outBig = frToBigInt(fe) % BN254_FR_MODULUS;
175
-
176
- return bigIntToBytesBE(outBig);
177
- }
178
-
179
- /**
180
- * Convenience: build a random note and its Poseidon commitment.
181
- */
182
- export function createNoteWithCommitment(params: {
183
- value: bigint;
184
- owner: PublicKey;
185
- }): SerializedNote {
186
- const note = createRandomNote(params);
187
- const commitment = commitNote(note);
188
-
189
- return {
190
- ...note,
191
- commitment,
192
- };
193
- }
package/src/poseidon.ts DELETED
@@ -1,62 +0,0 @@
1
- // sdk-core/src/poseidon.ts
2
- import {buildPoseidonReference} from "circomlibjs";
3
-
4
- export type PoseidonFn = ((inputs: bigint[]) => any) & {
5
- F: { toObject: (e: any) => bigint };
6
- };
7
-
8
- let poseidonInstance: PoseidonFn | null = null;
9
-
10
- export async function initPoseidon() {
11
- poseidonInstance = (await buildPoseidonReference()) as PoseidonFn;
12
- }
13
-
14
- export function getPoseidon(): PoseidonFn {
15
- if (!poseidonInstance) {
16
- throw new Error(
17
- "Poseidon not initialized. Call initPoseidon() once before using note/merkle/proof helpers."
18
- );
19
- }
20
- return poseidonInstance;
21
- }
22
-
23
- // Helpers for BN254 field
24
- export const BN254_FR_MODULUS = BigInt(
25
- "21888242871839275222246405745257275088548364400416034343698204186575808495617"
26
- );
27
-
28
- export function bytesToBigIntBE(bytes: Uint8Array): bigint {
29
- let x = 0n;
30
- for (const b of bytes) {
31
- x = (x << 8n) | BigInt(b);
32
- }
33
- return x;
34
- }
35
-
36
- export function bigIntToBytesBE(x: bigint): Uint8Array {
37
- const out = new Uint8Array(32);
38
- let v = x;
39
- for (let i = 31; i >= 0; i--) {
40
- out[i] = Number(v & 0xffn);
41
- v >>= 8n;
42
- }
43
- return out;
44
- }
45
-
46
- /** Convert ffjavascript field element → bigint safely */
47
- export function frToBigInt(fe: any): bigint {
48
- const poseidon = getPoseidon();
49
- const F = (poseidon as any).F;
50
-
51
- if (typeof fe === "bigint") {
52
- return fe;
53
- }
54
-
55
- if (F && typeof F.toObject === "function") {
56
- // This is how ffjavascript exposes the underlying BigInt
57
- return F.toObject(fe) as bigint;
58
- }
59
-
60
- // Fallback: treat as decimal string
61
- return BigInt(fe.toString());
62
- }
package/src/proof.ts DELETED
@@ -1,170 +0,0 @@
1
- // sdk-core/src/proof.ts
2
- import { PublicKey } from "@solana/web3.js";
3
- import {
4
- SerializedNote,
5
- BN254_FR_MODULUS,
6
- bytesToBigIntBE,
7
- bigIntToBytesBE,
8
- } from "./note";
9
- import { MerklePath } from "./merkle";
10
- import {frToBigInt, getPoseidon} from "./poseidon";
11
-
12
- // -----------------------------------------------------------------------------
13
- // Circuit inputs
14
- // -----------------------------------------------------------------------------
15
-
16
- export type WithdrawCircuitInputs = {
17
- root: Uint8Array;
18
- note: SerializedNote;
19
- merklePath: MerklePath;
20
- nullifier: Uint8Array;
21
- denomIndex: number;
22
- recipient: PublicKey;
23
- relayer: PublicKey;
24
- feeBps: number;
25
- };
26
-
27
- // -----------------------------------------------------------------------------
28
- // Nullifier helper – MUST match Circom:
29
- // nullifier = Poseidon(owner, rho)
30
- // -----------------------------------------------------------------------------
31
-
32
- export function deriveNullifier(note: SerializedNote): Uint8Array {
33
- const poseidon = getPoseidon();
34
-
35
- const ownerField =
36
- bytesToBigIntBE(note.owner.toBytes()) % BN254_FR_MODULUS;
37
- const rhoField =
38
- bytesToBigIntBE(note.rho) % BN254_FR_MODULUS;
39
-
40
- const fe = poseidon([ownerField, rhoField]);
41
- const outBig = frToBigInt(fe) % BN254_FR_MODULUS;
42
-
43
- return bigIntToBytesBE(outBig);
44
- }
45
-
46
- // -----------------------------------------------------------------------------
47
- // Groth16 proof types & packing (shared with relayer & dapps)
48
- // -----------------------------------------------------------------------------
49
-
50
- // Shape of a Groth16 proof produced by snarkjs.groth16
51
- export type RawProof = {
52
- pi_a: [string, string, string];
53
- pi_b: [[string, string], [string, string], [string, string]];
54
- pi_c: [string, string, string];
55
- protocol: string;
56
- curve: string;
57
- };
58
-
59
- /**
60
- * This matches the Rust struct:
61
- *
62
- * pub struct WithdrawProof {
63
- * pub proof_a: [u8; 64],
64
- * pub proof_b: [u8; 128],
65
- * pub proof_c: [u8; 64],
66
- * }
67
- *
68
- * In Anchor TS, this is just an object with three number[] fields.
69
- */
70
- export type WithdrawProofStruct = {
71
- proofA: number[]; // 64 bytes
72
- proofB: number[]; // 128 bytes
73
- proofC: number[]; // 64 bytes
74
- };
75
-
76
- // Builder now returns a RawProof (snarkjs output).
77
- export type ProofBuilder = (inputs: WithdrawCircuitInputs) => Promise<RawProof>;
78
-
79
- /**
80
- * Convert bigint -> 32-byte little-endian representation.
81
- * Used to pack proof coords into the 256-byte layout expected on-chain.
82
- */
83
- function bigintTo32BytesLE(x: bigint): Uint8Array {
84
- const out = new Uint8Array(32);
85
- let v = x;
86
- for (let i = 0; i < 32; i++) {
87
- out[i] = Number(v & 0xffn);
88
- v >>= 8n;
89
- }
90
- return out;
91
- }
92
-
93
- /**
94
- * Turn a snarkjs Groth16 proof into 256 bytes:
95
- * A(G1) | B(G2) | C(G1)
96
- * where:
97
- * A = (ax, ay)
98
- * B = (bx0, bx1; by0, by1)
99
- * C = (cx, cy)
100
- *
101
- * Each field element is encoded as 32-byte LE.
102
- * We ignore the extra projective / z components snarkjs keeps.
103
- */
104
- export function packProofToBytes(proof: RawProof): Uint8Array {
105
- const chunks: Uint8Array[] = [];
106
-
107
- // A: [ax, ay, az]
108
- const ax = BigInt(proof.pi_a[0]);
109
- const ay = BigInt(proof.pi_a[1]);
110
- chunks.push(bigintTo32BytesLE(ax), bigintTo32BytesLE(ay));
111
-
112
- // B: [[bx0, bx1], [by0, by1], [bz0, bz1]]
113
- // we only use x, y
114
- const bx = proof.pi_b[0];
115
- const by = proof.pi_b[1];
116
-
117
- const bx0 = BigInt(bx[0]);
118
- const bx1 = BigInt(bx[1]);
119
- const by0 = BigInt(by[0]);
120
- const by1 = BigInt(by[1]);
121
-
122
- chunks.push(
123
- bigintTo32BytesLE(bx0),
124
- bigintTo32BytesLE(bx1),
125
- bigintTo32BytesLE(by0),
126
- bigintTo32BytesLE(by1),
127
- );
128
-
129
- // C: [cx, cy, cz]
130
- const cx = BigInt(proof.pi_c[0]);
131
- const cy = BigInt(proof.pi_c[1]);
132
- chunks.push(bigintTo32BytesLE(cx), bigintTo32BytesLE(cy));
133
-
134
- // Flatten into single Uint8Array (256 bytes)
135
- const out = new Uint8Array(32 * (2 + 4 + 2)); // 256
136
- let offset = 0;
137
- for (const c of chunks) {
138
- out.set(c, offset);
139
- offset += c.length;
140
- }
141
- return out;
142
- }
143
-
144
- /**
145
- * Take a snarkjs proof and produce the JSON shape the Anchor program expects:
146
- *
147
- * pub struct WithdrawProof {
148
- * pub proof_a: [u8; 64],
149
- * pub proof_b: [u8; 128],
150
- * pub proof_c: [u8; 64],
151
- * }
152
- */
153
- export function encodeSnarkjsProofToWithdrawProof(
154
- proof: RawProof,
155
- ): WithdrawProofStruct {
156
- const full = packProofToBytes(proof);
157
- if (full.length !== 256) {
158
- throw new Error(`Expected 256 proof bytes, got ${full.length}`);
159
- }
160
-
161
- const proofA = full.slice(0, 64);
162
- const proofB = full.slice(64, 192);
163
- const proofC = full.slice(192, 256);
164
-
165
- return {
166
- proofA: Array.from(proofA),
167
- proofB: Array.from(proofB),
168
- proofC: Array.from(proofC),
169
- };
170
- }
package/test/script.js DELETED
File without changes
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "commonjs",
5
- "moduleResolution": "node",
6
- "declaration": true,
7
- "outDir": "dist",
8
- "rootDir": ".",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "resolveJsonModule": true // 👈 add this
14
- },
15
- "include": [
16
- "src/**/*.ts",
17
- "tests/**/*.test.ts"
18
- ]
19
- }
@@ -1,50 +0,0 @@
1
- // tests/note.test.ts
2
- import "mocha";
3
- import { strict as assert } from "assert";
4
- import { Keypair } from "@solana/web3.js";
5
- import {
6
- createRandomNote,
7
- encodeNoteToBytes,
8
- commitNote,
9
- createNoteWithCommitment,
10
- } from "../src";
11
-
12
- describe("note helpers", () => {
13
- it("creates a random note with correct sizes", () => {
14
- const owner = Keypair.generate().publicKey;
15
- const value = 123n;
16
-
17
- const note = createRandomNote({ value, owner });
18
-
19
- assert.equal(note.value, value);
20
- assert.equal(note.owner.toBase58(), owner.toBase58());
21
- assert.equal(note.rho.length, 32);
22
- assert.equal(note.r.length, 32);
23
- });
24
-
25
- it("encodes and commits deterministically", () => {
26
- const owner = Keypair.generate().publicKey;
27
- const value = 42n;
28
-
29
- const note1 = createRandomNote({ value, owner });
30
- const note2 = { ...note1 };
31
-
32
- const enc1 = encodeNoteToBytes(note1);
33
- const enc2 = encodeNoteToBytes(note2);
34
-
35
- assert.equal(enc1.length, enc2.length);
36
- assert.equal(enc1.length, 8 + 32 + 32 + 32);
37
-
38
- const c1 = commitNote(note1);
39
- const c2 = commitNote(note2);
40
-
41
- assert.equal(c1.length, 32);
42
- assert.equal(Buffer.compare(Buffer.from(c1), Buffer.from(c2)), 0);
43
- });
44
-
45
- it("createNoteWithCommitment returns 32-byte commitment", () => {
46
- const owner = Keypair.generate().publicKey;
47
- const note = createNoteWithCommitment({ value: 10n, owner });
48
- assert.equal(note.commitment.length, 32);
49
- });
50
- });
@@ -1,210 +0,0 @@
1
- import "mocha";
2
- import { strict as assert } from "assert";
3
- import * as anchor from "@coral-xyz/anchor";
4
- import type { Idl, Program } from "@coral-xyz/anchor";
5
- import {
6
- Keypair,
7
- LAMPORTS_PER_SOL,
8
- PublicKey,
9
- } from "@solana/web3.js";
10
- import fs from "fs";
11
- import os from "os";
12
- import path from "path";
13
-
14
- import {
15
- initializePool,
16
- addRelayer,
17
- getPoolPdas,
18
- createNoteAndDeposit,
19
- withdrawViaRelayer,
20
- sol,
21
- } from "../src";
22
-
23
- // ---- IDL + Provider helpers ----
24
-
25
- function loadIdl(): Idl {
26
- // NOTE: this path is relative to dist/tests at runtime
27
- const idlPath = path.join(
28
- __dirname,
29
- "../../../privacy-pool/target/idl/privacy_pool.json"
30
- );
31
- const raw = fs.readFileSync(idlPath, "utf8");
32
- return JSON.parse(raw);
33
- }
34
-
35
- function makeProvider(): anchor.AnchorProvider {
36
- const url = process.env.ANCHOR_PROVIDER_URL ?? "http://127.0.0.1:8899";
37
- const connection = new anchor.web3.Connection(url, "confirmed");
38
-
39
- const keypairPath =
40
- process.env.ANCHOR_WALLET ??
41
- path.join(os.homedir(), ".config", "solana", "id.json");
42
-
43
- const secret = JSON.parse(fs.readFileSync(keypairPath, "utf8"));
44
- const kp = Keypair.fromSecretKey(Uint8Array.from(secret));
45
- const wallet = new anchor.Wallet(kp);
46
-
47
- return new anchor.AnchorProvider(connection, wallet, {
48
- commitment: "confirmed",
49
- preflightCommitment: "confirmed",
50
- });
51
- }
52
-
53
- describe("privacy-pool SDK integration (Merkle v3, rolling root)", () => {
54
- const provider = makeProvider();
55
- anchor.setProvider(provider);
56
-
57
- const idl = loadIdl();
58
- const program = new anchor.Program(idl as Idl, provider) as Program<any>;
59
- const wallet = provider.wallet as anchor.Wallet;
60
-
61
- const connection = provider.connection;
62
-
63
- // This MUST match what initializePool uses below.
64
- const DENOM_INDEX = 0;
65
- const DENOMS_LAMPORTS = [sol(1), sol(5)]; // 1 SOL, 5 SOL
66
- const FEE_BPS = 50; // 0.5%
67
-
68
- async function airdropAndConfirm(pubkey: PublicKey, lamports: number) {
69
- const sig = await connection.requestAirdrop(pubkey, lamports);
70
- await connection.confirmTransaction(sig, "confirmed");
71
- }
72
-
73
- it("initialize + deposit + withdraw via SDK", async () => {
74
- const { config, vault, noteTree, nullifiers } = getPoolPdas(
75
- program.programId
76
- );
77
-
78
- // 1) Make sure admin has SOL
79
- await airdropAndConfirm(wallet.publicKey, 5 * LAMPORTS_PER_SOL);
80
-
81
- // 2) Initialize pool if needed (idempotent-ish for localnet dev)
82
- const existingConfig = await connection.getAccountInfo(config);
83
- if (!existingConfig) {
84
- await initializePool({
85
- program,
86
- admin: wallet,
87
- denomsLamports: DENOMS_LAMPORTS,
88
- feeBps: FEE_BPS,
89
- });
90
- } else {
91
- console.log(
92
- "Initialize skipped: config already exists on this cluster, continuing."
93
- );
94
- }
95
-
96
- // Ground truth amounts come from what we passed into initializePool
97
- const amountLamports = BigInt(DENOMS_LAMPORTS[DENOM_INDEX].toString());
98
- const fee = (amountLamports * BigInt(FEE_BPS)) / 10_000n;
99
- const toUser = amountLamports - fee;
100
-
101
- // 3) Deposit one note via SDK helper
102
- await createNoteAndDeposit({
103
- program,
104
- depositor: wallet,
105
- denomIndex: DENOM_INDEX,
106
- valueLamports: amountLamports,
107
- });
108
-
109
- // 4) Prepare relayer + recipient
110
- const relayer = Keypair.generate();
111
- const recipient = Keypair.generate();
112
-
113
- await airdropAndConfirm(relayer.publicKey, 2 * LAMPORTS_PER_SOL);
114
- await airdropAndConfirm(recipient.publicKey, 0.2 * LAMPORTS_PER_SOL);
115
-
116
- // Admin adds relayer
117
- await addRelayer({
118
- program,
119
- admin: wallet,
120
- newRelayer: relayer.publicKey,
121
- });
122
-
123
- // 5) Fetch current Merkle root from NoteTree account.
124
- const noteTreeAcc: any = await (program.account as any).noteTree.fetch(
125
- noteTree
126
- );
127
-
128
- // Support all known shapes:
129
- // - rolling history: { currentRootIndex, roots: number[][] }
130
- // - single root (previous layout): { lastRoot: number[] }
131
- // - current layout: { currentRoot: number[] }
132
- let rootBytes: Uint8Array;
133
-
134
- if (
135
- noteTreeAcc &&
136
- typeof noteTreeAcc.currentRootIndex !== "undefined" &&
137
- typeof noteTreeAcc.roots !== "undefined"
138
- ) {
139
- const idx: number = Number(noteTreeAcc.currentRootIndex);
140
- const rootsArr: number[][] = noteTreeAcc.roots;
141
- const rootArray: number[] = rootsArr[idx];
142
- if (!rootArray) {
143
- console.error("NoteTree account shape (roots):", noteTreeAcc);
144
- throw new Error("NoteTree.roots[currentRootIndex] is undefined");
145
- }
146
- rootBytes = new Uint8Array(rootArray);
147
- } else if (
148
- noteTreeAcc &&
149
- typeof noteTreeAcc.lastRoot !== "undefined"
150
- ) {
151
- const rootArray: number[] = noteTreeAcc.lastRoot;
152
- rootBytes = new Uint8Array(rootArray);
153
- } else if (
154
- noteTreeAcc &&
155
- typeof noteTreeAcc.currentRoot !== "undefined"
156
- ) {
157
- const rootArray: number[] = noteTreeAcc.currentRoot;
158
- rootBytes = new Uint8Array(rootArray);
159
- } else {
160
- console.error("NoteTree account shape (unknown):", noteTreeAcc);
161
- throw new Error("Unknown NoteTree layout in IDL / account");
162
- }
163
-
164
- const nullifier = new Uint8Array(32).fill(3); // demo nullifier
165
-
166
- const beforeVault = BigInt(await connection.getBalance(vault));
167
- const beforeRelayer = BigInt(
168
- await connection.getBalance(relayer.publicKey)
169
- );
170
- const beforeRecipient = BigInt(
171
- await connection.getBalance(recipient.publicKey)
172
- );
173
-
174
- // 6) Withdraw via SDK
175
- await withdrawViaRelayer({
176
- program,
177
- relayer,
178
- recipient: recipient.publicKey,
179
- denomIndex: DENOM_INDEX,
180
- root: rootBytes,
181
- nullifier,
182
- });
183
-
184
- const afterVault = BigInt(await connection.getBalance(vault));
185
- const afterRelayer = BigInt(
186
- await connection.getBalance(relayer.publicKey)
187
- );
188
- const afterRecipient = BigInt(
189
- await connection.getBalance(recipient.publicKey)
190
- );
191
-
192
- const vaultDelta = beforeVault - afterVault;
193
- const relayerDelta = afterRelayer - beforeRelayer;
194
- const recipientDelta = afterRecipient - beforeRecipient;
195
-
196
- // 7) Assertions
197
- assert(
198
- vaultDelta === amountLamports,
199
- `Vault did not decrease by amount: got ${vaultDelta.toString()} expected ${amountLamports.toString()}`
200
- );
201
- assert(
202
- relayerDelta === fee,
203
- `Relayer did not receive correct fee: got ${relayerDelta.toString()} expected ${fee.toString()}`
204
- );
205
- assert(
206
- recipientDelta === toUser,
207
- `Recipient did not receive correct amount: got ${recipientDelta.toString()} expected ${toUser.toString()}`
208
- );
209
- });
210
- });