nova-privacy-sdk 1.0.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.
Files changed (69) hide show
  1. package/.github/workflows/npm-publish.yml +55 -0
  2. package/PUBLISH.md +122 -0
  3. package/README.md +177 -0
  4. package/__tests__/e2e.test.ts +56 -0
  5. package/__tests__/e2espl.test.ts +73 -0
  6. package/__tests__/encryption.test.ts +1635 -0
  7. package/circuit2/transaction2.wasm +0 -0
  8. package/circuit2/transaction2.zkey +0 -0
  9. package/dist/config.d.ts +9 -0
  10. package/dist/config.js +12 -0
  11. package/dist/deposit.d.ts +18 -0
  12. package/dist/deposit.js +392 -0
  13. package/dist/depositSPL.d.ts +20 -0
  14. package/dist/depositSPL.js +448 -0
  15. package/dist/exportUtils.d.ts +11 -0
  16. package/dist/exportUtils.js +11 -0
  17. package/dist/getUtxos.d.ts +29 -0
  18. package/dist/getUtxos.js +294 -0
  19. package/dist/getUtxosSPL.d.ts +33 -0
  20. package/dist/getUtxosSPL.js +395 -0
  21. package/dist/index.d.ts +125 -0
  22. package/dist/index.js +302 -0
  23. package/dist/models/keypair.d.ts +26 -0
  24. package/dist/models/keypair.js +43 -0
  25. package/dist/models/utxo.d.ts +49 -0
  26. package/dist/models/utxo.js +85 -0
  27. package/dist/utils/address_lookup_table.d.ts +9 -0
  28. package/dist/utils/address_lookup_table.js +45 -0
  29. package/dist/utils/constants.d.ts +31 -0
  30. package/dist/utils/constants.js +62 -0
  31. package/dist/utils/encryption.d.ts +107 -0
  32. package/dist/utils/encryption.js +376 -0
  33. package/dist/utils/logger.d.ts +9 -0
  34. package/dist/utils/logger.js +35 -0
  35. package/dist/utils/merkle_tree.d.ts +92 -0
  36. package/dist/utils/merkle_tree.js +186 -0
  37. package/dist/utils/node-shim.d.ts +5 -0
  38. package/dist/utils/node-shim.js +5 -0
  39. package/dist/utils/prover.d.ts +36 -0
  40. package/dist/utils/prover.js +147 -0
  41. package/dist/utils/utils.d.ts +69 -0
  42. package/dist/utils/utils.js +182 -0
  43. package/dist/withdraw.d.ts +21 -0
  44. package/dist/withdraw.js +270 -0
  45. package/dist/withdrawSPL.d.ts +23 -0
  46. package/dist/withdrawSPL.js +306 -0
  47. package/package.json +77 -0
  48. package/setup-git.sh +51 -0
  49. package/setup-github.sh +36 -0
  50. package/src/config.ts +22 -0
  51. package/src/deposit.ts +487 -0
  52. package/src/depositSPL.ts +567 -0
  53. package/src/exportUtils.ts +13 -0
  54. package/src/getUtxos.ts +396 -0
  55. package/src/getUtxosSPL.ts +528 -0
  56. package/src/index.ts +350 -0
  57. package/src/models/keypair.ts +52 -0
  58. package/src/models/utxo.ts +106 -0
  59. package/src/utils/address_lookup_table.ts +78 -0
  60. package/src/utils/constants.ts +84 -0
  61. package/src/utils/encryption.ts +464 -0
  62. package/src/utils/logger.ts +42 -0
  63. package/src/utils/merkle_tree.ts +207 -0
  64. package/src/utils/node-shim.ts +6 -0
  65. package/src/utils/prover.ts +222 -0
  66. package/src/utils/utils.ts +242 -0
  67. package/src/withdraw.ts +332 -0
  68. package/src/withdrawSPL.ts +394 -0
  69. package/tsconfig.json +28 -0
@@ -0,0 +1,207 @@
1
+ import * as hasher from '@lightprotocol/hasher.rs';
2
+ export const DEFAULT_ZERO = 0;
3
+
4
+ /**
5
+ * @callback hashFunction
6
+ * @param left Left leaf
7
+ * @param right Right leaf
8
+ */
9
+ /**
10
+ * Merkle tree
11
+ */
12
+ export class MerkleTree {
13
+ /**
14
+ * Constructor
15
+ * @param {number} levels Number of levels in the tree
16
+ * @param {Array} [elements] Initial elements
17
+ * @param {Object} options
18
+ * @param {hashFunction} [options.hashFunction] Function used to hash 2 leaves
19
+ * @param [options.zeroElement] Value for non-existent leaves
20
+ */
21
+ levels: number;
22
+ capacity: number;
23
+ zeroElement;
24
+ _zeros: string[];
25
+ _layers: string[][];
26
+ _lightWasm: hasher.LightWasm;
27
+
28
+ constructor(
29
+ levels: number,
30
+ lightWasm: hasher.LightWasm,
31
+ elements: string[] = [],
32
+ { zeroElement = DEFAULT_ZERO } = {},
33
+ ) {
34
+ this.levels = levels;
35
+ this.capacity = 2 ** levels;
36
+ this.zeroElement = zeroElement.toString();
37
+ this._lightWasm = lightWasm;
38
+ if (elements.length > this.capacity) {
39
+ throw new Error("Tree is full");
40
+ }
41
+ this._zeros = [];
42
+ this._layers = [];
43
+ this._layers[0] = elements;
44
+ this._zeros[0] = this.zeroElement;
45
+
46
+ for (let i = 1; i <= levels; i++) {
47
+ this._zeros[i] = this._lightWasm.poseidonHashString([
48
+ this._zeros[i - 1],
49
+ this._zeros[i - 1],
50
+ ]);
51
+ }
52
+ this._rebuild();
53
+ }
54
+
55
+ _rebuild() {
56
+ for (let level = 1; level <= this.levels; level++) {
57
+ this._layers[level] = [];
58
+ for (let i = 0; i < Math.ceil(this._layers[level - 1].length / 2); i++) {
59
+ this._layers[level][i] = this._lightWasm.poseidonHashString([
60
+ this._layers[level - 1][i * 2],
61
+ i * 2 + 1 < this._layers[level - 1].length
62
+ ? this._layers[level - 1][i * 2 + 1]
63
+ : this._zeros[level - 1],
64
+ ]);
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get tree root
71
+ * @returns {*}
72
+ */
73
+ root() {
74
+ return this._layers[this.levels].length > 0
75
+ ? this._layers[this.levels][0]
76
+ : this._zeros[this.levels];
77
+ }
78
+
79
+ /**
80
+ * Insert new element into the tree
81
+ * @param element Element to insert
82
+ */
83
+
84
+ insert(element: string) {
85
+ if (this._layers[0].length >= this.capacity) {
86
+ throw new Error("Tree is full");
87
+ }
88
+ this.update(this._layers[0].length, element);
89
+ }
90
+
91
+ /**
92
+ * Insert multiple elements into the tree. Tree will be fully rebuilt during this operation.
93
+ * @param {Array} elements Elements to insert
94
+ */
95
+ bulkInsert(elements: string[]) {
96
+ if (this._layers[0].length + elements.length > this.capacity) {
97
+ throw new Error("Tree is full");
98
+ }
99
+ this._layers[0].push(...elements);
100
+ this._rebuild();
101
+ }
102
+
103
+ // TODO: update does not work debug
104
+ /**
105
+ * Change an element in the tree
106
+ * @param {number} index Index of element to change
107
+ * @param element Updated element value
108
+ */
109
+ update(index: number, element: string) {
110
+ // index 0 and 1 and element is the commitment hash
111
+ if (
112
+ isNaN(Number(index)) ||
113
+ index < 0 ||
114
+ index > this._layers[0].length ||
115
+ index >= this.capacity
116
+ ) {
117
+ throw new Error("Insert index out of bounds: " + index);
118
+ }
119
+ this._layers[0][index] = element;
120
+ for (let level = 1; level <= this.levels; level++) {
121
+ index >>= 1;
122
+ this._layers[level][index] = this._lightWasm.poseidonHashString([
123
+ this._layers[level - 1][index * 2],
124
+ index * 2 + 1 < this._layers[level - 1].length
125
+ ? this._layers[level - 1][index * 2 + 1]
126
+ : this._zeros[level - 1],
127
+ ]);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get merkle path to a leaf
133
+ * @param {number} index Leaf index to generate path for
134
+ * @returns {{pathElements: number[], pathIndex: number[]}} An object containing adjacent elements and left-right index
135
+ */
136
+ path(index: number) {
137
+ if (isNaN(Number(index)) || index < 0 || index >= this._layers[0].length) {
138
+ throw new Error("Index out of bounds: " + index);
139
+ }
140
+ const pathElements: string[] = [];
141
+ const pathIndices: number[] = [];
142
+ for (let level = 0; level < this.levels; level++) {
143
+ pathIndices[level] = index % 2;
144
+ pathElements[level] =
145
+ (index ^ 1) < this._layers[level].length
146
+ ? this._layers[level][index ^ 1]
147
+ : this._zeros[level];
148
+ index >>= 1;
149
+ }
150
+ return {
151
+ pathElements,
152
+ pathIndices,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Find an element in the tree
158
+ * @param element An element to find
159
+ * @param comparator A function that checks leaf value equality
160
+ * @returns {number} Index if element is found, otherwise -1
161
+ */
162
+ indexOf(element: string, comparator: Function | null = null) {
163
+ if (comparator) {
164
+ return this._layers[0].findIndex((el: string) => comparator(element, el));
165
+ } else {
166
+ return this._layers[0].indexOf(element);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Returns a copy of non-zero tree elements
172
+ * @returns {Object[]}
173
+ */
174
+ elements() {
175
+ return this._layers[0].slice();
176
+ }
177
+
178
+ /**
179
+ * Serialize entire tree state including intermediate layers into a plain object
180
+ * Deserializing it back will not require to recompute any hashes
181
+ * Elements are not converted to a plain type, this is responsibility of the caller
182
+ */
183
+ serialize() {
184
+ return {
185
+ levels: this.levels,
186
+ _zeros: this._zeros,
187
+ _layers: this._layers,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Deserialize data into a MerkleTree instance
193
+ * Make sure to provide the same hashFunction as was used in the source tree,
194
+ * otherwise the tree state will be invalid
195
+ *
196
+ * @param data
197
+ * @param hashFunction
198
+ * @returns {MerkleTree}
199
+ */
200
+ static deserialize(data: any, hashFunction: Function) {
201
+ const instance = Object.assign(Object.create(this.prototype), data);
202
+ instance._hash = hashFunction;
203
+ instance.capacity = 2 ** instance.levels;
204
+ instance.zeroElement = instance._zeros[0];
205
+ return instance;
206
+ }
207
+ }
@@ -0,0 +1,6 @@
1
+ import { LocalStorage } from "node-localstorage";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { fileURLToPath } from "url";
5
+
6
+ export { LocalStorage, path, fs, fileURLToPath };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * ZK Proof Generation Utilities
3
+ *
4
+ * This file provides functions for generating zero-knowledge proofs for privacy-preserving
5
+ * transactions on Solana. It handles both snarkjs and zkutil proof generation workflows.
6
+ *
7
+ * Inspired by: https://github.com/tornadocash/tornado-nova/blob/f9264eeffe48bf5e04e19d8086ee6ec58cdf0d9e/src/prover.js
8
+ */
9
+
10
+ /// <reference types="node" />
11
+
12
+ import * as anchor from "@coral-xyz/anchor";
13
+ import { wtns, groth16 } from 'snarkjs'
14
+ import { FIELD_SIZE } from './constants.js'
15
+
16
+ // @ts-ignore - ignore TypeScript errors for ffjavascript
17
+ import { utils } from 'ffjavascript'
18
+
19
+ // Type definitions for external modules
20
+ type WtnsModule = {
21
+ debug: (input: any, wasmFile: string, wtnsFile: string, symFile: string, options: any, logger: any) => Promise<void>
22
+ exportJson: (wtnsFile: string) => Promise<any>
23
+ }
24
+
25
+ type Groth16Module = {
26
+ fullProve: (
27
+ input: any,
28
+ wasmFile: string,
29
+ zkeyFile: string,
30
+ logger?: any,
31
+ wtnsCalcOptions?: { singleThread?: boolean },
32
+ proverOptions?: { singleThread?: boolean }
33
+ ) => Promise<{ proof: Proof; publicSignals: string[] }>
34
+ verify: (vkeyData: any, publicSignals: any, proof: Proof) => Promise<boolean>
35
+ }
36
+
37
+ type UtilsModule = {
38
+ stringifyBigInts: (obj: any) => any
39
+ unstringifyBigInts: (obj: any) => any
40
+ }
41
+
42
+ // Cast imported modules to their types
43
+ const wtnsTyped = wtns as unknown as WtnsModule
44
+ const groth16Typed = groth16 as unknown as Groth16Module
45
+ const utilsTyped = utils as unknown as UtilsModule
46
+
47
+
48
+ // Define interfaces for the proof structures
49
+ interface Proof {
50
+ pi_a: string[];
51
+ pi_b: string[][];
52
+ pi_c: string[];
53
+ protocol: string;
54
+ curve: string;
55
+ }
56
+
57
+ interface ProofResult {
58
+ proof: Proof
59
+ }
60
+
61
+ /**
62
+ * Generates a ZK proof using snarkjs and formats it for use on-chain
63
+ *
64
+ * @param input The circuit inputs to generate a proof for
65
+ * @param keyBasePath The base path for the circuit keys (.wasm and .zkey files)
66
+ * @param options Optional proof generation options (e.g., singleThread for Deno/Bun)
67
+ * @returns A proof object with formatted proof elements and public signals
68
+ */
69
+ async function prove(input: any, keyBasePath: string, options?: { singleThread?: boolean }): Promise<{
70
+ proof: Proof
71
+ publicSignals: string[];
72
+ }> {
73
+ // Detect if we should use single-threaded mode (for Deno/Bun compatibility)
74
+ const useSingleThread = options?.singleThread ?? shouldUseSingleThread();
75
+
76
+ // Single-thread options need to be passed to BOTH witness calculation AND proving
77
+ const singleThreadOpts = useSingleThread ? { singleThread: true } : undefined;
78
+
79
+ // Call fullProve with all parameters:
80
+ // 1. input, 2. wasmFile, 3. zkeyFile, 4. logger, 5. wtnsCalcOptions, 6. proverOptions
81
+ return await groth16Typed.fullProve(
82
+ utilsTyped.stringifyBigInts(input),
83
+ `${keyBasePath}.wasm`,
84
+ `${keyBasePath}.zkey`,
85
+ undefined, // logger parameter
86
+ singleThreadOpts, // wtnsCalcOptions (5th param) - for witness calculation
87
+ singleThreadOpts // proverOptions (6th param) - for proving
88
+ )
89
+ }
90
+
91
+ /**
92
+ * Detect if single-threaded mode should be used
93
+ */
94
+ function shouldUseSingleThread(): boolean {
95
+ // @ts-ignore - Deno global
96
+ if (typeof Deno !== 'undefined') {
97
+ return true; // Deno has worker issues
98
+ }
99
+ // @ts-ignore - Bun global
100
+ if (typeof Bun !== 'undefined') {
101
+ return true; // Bun may have worker issues
102
+ }
103
+ return false;
104
+ }
105
+
106
+ export function parseProofToBytesArray(
107
+ proof: Proof,
108
+ compressed: boolean = false,
109
+ ): {
110
+ proofA: number[];
111
+ proofB: number[][];
112
+ proofC: number[];
113
+ } {
114
+ const proofJson = JSON.stringify(proof, null, 1);
115
+ const mydata = JSON.parse(proofJson.toString());
116
+ try {
117
+ for (const i in mydata) {
118
+ if (i == "pi_a" || i == "pi_c") {
119
+ for (const j in mydata[i]) {
120
+ mydata[i][j] = Array.from(
121
+ utils.leInt2Buff(utils.unstringifyBigInts(mydata[i][j]), 32),
122
+ ).reverse();
123
+ }
124
+ } else if (i == "pi_b") {
125
+ for (const j in mydata[i]) {
126
+ for (const z in mydata[i][j]) {
127
+ mydata[i][j][z] = Array.from(
128
+ utils.leInt2Buff(utils.unstringifyBigInts(mydata[i][j][z]), 32),
129
+ );
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ if (compressed) {
136
+ const proofA = mydata.pi_a[0];
137
+ // negate proof by reversing the bitmask
138
+ const proofAIsPositive = yElementIsPositiveG1(
139
+ new anchor.BN(mydata.pi_a[1]),
140
+ )
141
+ ? false
142
+ : true;
143
+ proofA[0] = addBitmaskToByte(proofA[0], proofAIsPositive);
144
+ const proofB = mydata.pi_b[0].flat().reverse();
145
+ const proofBY = mydata.pi_b[1].flat().reverse();
146
+ const proofBIsPositive = yElementIsPositiveG2(
147
+ new anchor.BN(proofBY.slice(0, 32)),
148
+ new anchor.BN(proofBY.slice(32, 64)),
149
+ );
150
+ proofB[0] = addBitmaskToByte(proofB[0], proofBIsPositive);
151
+ const proofC = mydata.pi_c[0];
152
+ const proofCIsPositive = yElementIsPositiveG1(
153
+ new anchor.BN(mydata.pi_c[1]),
154
+ );
155
+ proofC[0] = addBitmaskToByte(proofC[0], proofCIsPositive);
156
+ return {
157
+ proofA,
158
+ proofB,
159
+ proofC,
160
+ };
161
+ }
162
+ return {
163
+ proofA: [mydata.pi_a[0], mydata.pi_a[1]].flat(),
164
+ proofB: [
165
+ mydata.pi_b[0].flat().reverse(),
166
+ mydata.pi_b[1].flat().reverse(),
167
+ ].flat(),
168
+ proofC: [mydata.pi_c[0], mydata.pi_c[1]].flat(),
169
+ };
170
+ } catch (error: any) {
171
+ console.error("Error while parsing the proof.", error.message);
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ // mainly used to parse the public signals of groth16 fullProve
177
+ export function parseToBytesArray(publicSignals: string[]): number[][] {
178
+ const publicInputsJson = JSON.stringify(publicSignals, null, 1);
179
+ const publicInputsBytesJson = JSON.parse(publicInputsJson.toString());
180
+ try {
181
+ const publicInputsBytes = new Array<Array<number>>();
182
+ for (const i in publicInputsBytesJson) {
183
+ const ref: Array<number> = Array.from([
184
+ ...utils.leInt2Buff(utils.unstringifyBigInts(publicInputsBytesJson[i]), 32),
185
+ ]).reverse();
186
+ publicInputsBytes.push(ref);
187
+ }
188
+
189
+ return publicInputsBytes;
190
+ } catch (error: any) {
191
+ console.error("Error while parsing public inputs.", error.message);
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ function yElementIsPositiveG1(yElement: anchor.BN): boolean {
197
+ return yElement.lte(FIELD_SIZE.sub(yElement));
198
+ }
199
+
200
+ function yElementIsPositiveG2(yElement1: anchor.BN, yElement2: anchor.BN): boolean {
201
+ const fieldMidpoint = FIELD_SIZE.div(new anchor.BN(2));
202
+
203
+ // Compare the first component of the y coordinate
204
+ if (yElement1.lt(fieldMidpoint)) {
205
+ return true;
206
+ } else if (yElement1.gt(fieldMidpoint)) {
207
+ return false;
208
+ }
209
+
210
+ // If the first component is equal to the midpoint, compare the second component
211
+ return yElement2.lt(fieldMidpoint);
212
+ }
213
+
214
+ function addBitmaskToByte(byte: number, yIsPositive: boolean): number {
215
+ if (!yIsPositive) {
216
+ return (byte |= 1 << 7);
217
+ } else {
218
+ return byte;
219
+ }
220
+ }
221
+
222
+ export { prove, type Proof }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Utility functions for ZK Cash
3
+ *
4
+ * Provides common utility functions for the ZK Cash system
5
+ * Based on: https://github.com/tornadocash/tornado-nova
6
+ */
7
+
8
+ import BN from 'bn.js';
9
+ import { Utxo } from '../models/utxo.js';
10
+ import * as borsh from 'borsh';
11
+ import { sha256 } from '@ethersproject/sha2';
12
+ import { PublicKey } from '@solana/web3.js';
13
+ import { RELAYER_API_URL, PROGRAM_ID, SOL_TREE_ADDRESS, NOVA_MINT, NOVA_TREE_ADDRESS, NOVASOL_TREE_ADDRESS } from './constants.js';
14
+ import { logger } from './logger.js';
15
+ import { getConfig } from '../config.js';
16
+
17
+ /**
18
+ * Calculate deposit fee based on deposit amount and fee rate
19
+ * @param depositAmount Amount being deposited in lamports
20
+ * @returns Fee amount in lamports
21
+ */
22
+ export async function calculateDepositFee(depositAmount: number) {
23
+ return Math.floor(depositAmount * (await getConfig('deposit_fee_rate')) / 10000);
24
+ }
25
+
26
+ /**
27
+ * Calculate withdrawal fee based on withdrawal amount and fee rate
28
+ * @param withdrawalAmount Amount being withdrawn in lamports
29
+ * @returns Fee amount in lamports
30
+ */
31
+ export async function calculateWithdrawalFee(withdrawalAmount: number) {
32
+ return Math.floor(withdrawalAmount * (await getConfig('withdraw_fee_rate')) / 10000);
33
+ }
34
+
35
+ /**
36
+ * Mock encryption function - in real implementation this would be proper encryption
37
+ * For testing, we just return a fixed prefix to ensure consistent extDataHash
38
+ * @param value Value to encrypt
39
+ * @returns Encrypted string representation
40
+ */
41
+ export function mockEncrypt(value: Utxo): string {
42
+ return JSON.stringify(value);
43
+ }
44
+
45
+ /**
46
+ * Calculates the hash of ext data using Borsh serialization
47
+ * @param extData External data object containing recipient, amount, encrypted outputs, fee, fee recipient, and mint address
48
+ * @returns The hash as a Uint8Array (32 bytes)
49
+ */
50
+ export function getExtDataHash(extData: {
51
+ recipient: string | PublicKey;
52
+ extAmount: string | number | BN;
53
+ encryptedOutput1?: string | Uint8Array; // Optional for Account Data Separation
54
+ encryptedOutput2?: string | Uint8Array; // Optional for Account Data Separation
55
+ fee: string | number | BN;
56
+ feeRecipient: string | PublicKey;
57
+ mintAddress: string | PublicKey;
58
+ }): Uint8Array {
59
+ // Convert all inputs to their appropriate types
60
+ const recipient = extData.recipient instanceof PublicKey
61
+ ? extData.recipient
62
+ : new PublicKey(extData.recipient);
63
+
64
+ const feeRecipient = extData.feeRecipient instanceof PublicKey
65
+ ? extData.feeRecipient
66
+ : new PublicKey(extData.feeRecipient);
67
+
68
+ const mintAddress = extData.mintAddress instanceof PublicKey
69
+ ? extData.mintAddress
70
+ : new PublicKey(extData.mintAddress);
71
+
72
+ // Convert to BN for proper i64/u64 handling
73
+ const extAmount = new BN(extData.extAmount.toString());
74
+ const fee = new BN(extData.fee.toString());
75
+
76
+ // Handle encrypted outputs - they might not be present in Account Data Separation approach
77
+ const encryptedOutput1 = extData.encryptedOutput1
78
+ ? Buffer.from(extData.encryptedOutput1 as any)
79
+ : Buffer.alloc(0); // Empty buffer if not provided
80
+ const encryptedOutput2 = extData.encryptedOutput2
81
+ ? Buffer.from(extData.encryptedOutput2 as any)
82
+ : Buffer.alloc(0); // Empty buffer if not provided
83
+
84
+ // Define the borsh schema matching the Rust struct
85
+ const schema = {
86
+ struct: {
87
+ recipient: { array: { type: 'u8', len: 32 } },
88
+ extAmount: 'i64',
89
+ encryptedOutput1: { array: { type: 'u8' } },
90
+ encryptedOutput2: { array: { type: 'u8' } },
91
+ fee: 'u64',
92
+ feeRecipient: { array: { type: 'u8', len: 32 } },
93
+ mintAddress: { array: { type: 'u8', len: 32 } },
94
+ }
95
+ };
96
+
97
+ const value = {
98
+ recipient: recipient.toBytes(),
99
+ extAmount: extAmount, // BN instance - Borsh handles it correctly with i64 type
100
+ encryptedOutput1: encryptedOutput1,
101
+ encryptedOutput2: encryptedOutput2,
102
+ fee: fee, // BN instance - Borsh handles it correctly with u64 type
103
+ feeRecipient: feeRecipient.toBytes(),
104
+ mintAddress: mintAddress.toBytes(),
105
+ };
106
+
107
+ // Serialize with Borsh
108
+ const serializedData = borsh.serialize(schema, value);
109
+
110
+ // Calculate the SHA-256 hash
111
+ const hashHex = sha256(serializedData);
112
+ // Convert from hex string to Uint8Array
113
+ return Buffer.from(hashHex.slice(2), 'hex');
114
+ }
115
+
116
+
117
+ // Function to fetch Merkle proof from API for a given commitment
118
+ export async function fetchMerkleProof(commitment: string, tokenName?: string): Promise<{ pathElements: string[], pathIndices: number[] }> {
119
+ try {
120
+ logger.debug(`Fetching Merkle proof for commitment: ${commitment}`);
121
+ let url = `${RELAYER_API_URL}/merkle/proof/${commitment}`
122
+ if (tokenName) {
123
+ url += '?token=' + tokenName
124
+ }
125
+ const response = await fetch(url);
126
+ if (!response.ok) {
127
+ throw new Error(`Failed to fetch Merkle proof: ${url}`);
128
+ }
129
+ const data = await response.json() as { pathElements: string[], pathIndices: number[] };
130
+ logger.debug(`✓ Fetched Merkle proof with ${data.pathElements.length} elements`);
131
+ return data;
132
+ } catch (error) {
133
+ console.error(`Failed to fetch Merkle proof for commitment ${commitment}:`, error);
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ // Find nullifier PDAs for the given proof
139
+ export function findNullifierPDAs(proof: any) {
140
+ const [nullifier0PDA] = PublicKey.findProgramAddressSync(
141
+ [Buffer.from("nullifier0"), Buffer.from(proof.inputNullifiers[0])],
142
+ PROGRAM_ID
143
+ );
144
+
145
+ const [nullifier1PDA] = PublicKey.findProgramAddressSync(
146
+ [Buffer.from("nullifier1"), Buffer.from(proof.inputNullifiers[1])],
147
+ PROGRAM_ID
148
+ );
149
+
150
+ return { nullifier0PDA, nullifier1PDA };
151
+ }
152
+
153
+ // Function to query remote tree state from indexer API
154
+ export async function queryRemoteTreeState(tokenName?: string): Promise<{ root: string, nextIndex: number }> {
155
+ try {
156
+ logger.debug('Fetching Merkle root and nextIndex from API...');
157
+ let url = `${RELAYER_API_URL}/merkle/root`
158
+ if (tokenName) {
159
+ url += '?token=' + tokenName
160
+ }
161
+ const response = await fetch(url);
162
+ if (!response.ok) {
163
+ throw new Error(`Failed to fetch Merkle root and nextIndex: ${response.status} ${response.statusText}`);
164
+ }
165
+ const data = await response.json() as { root: string, nextIndex: number };
166
+ logger.debug(`Fetched root from API: ${data.root}`);
167
+ logger.debug(`Fetched nextIndex from API: ${data.nextIndex}`);
168
+ return data;
169
+ } catch (error) {
170
+ console.error('Failed to fetch root and nextIndex from API:', error);
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ export function getProgramAccounts() {
176
+ // Use NOVASOL tree address for SOL transactions (NOVA's SOL tree)
177
+ const treeAccount = NOVASOL_TREE_ADDRESS
178
+
179
+ const [treeTokenAccount] = PublicKey.findProgramAddressSync(
180
+ [Buffer.from('tree_token')],
181
+ PROGRAM_ID
182
+ );
183
+
184
+ const [globalConfigAccount] = PublicKey.findProgramAddressSync(
185
+ [Buffer.from('global_config')],
186
+ PROGRAM_ID
187
+ );
188
+ return { treeAccount, treeTokenAccount, globalConfigAccount }
189
+ }
190
+
191
+ /**
192
+ * Get tree account address for a given token/mint
193
+ * Uses specific addresses for SOL (NOVASOL), NOVA, and other tokens
194
+ */
195
+ export function getTreeAccountForToken(mintAddress?: PublicKey): PublicKey {
196
+ if (!mintAddress) {
197
+ // SOL tree - use NOVASOL tree address
198
+ return NOVASOL_TREE_ADDRESS
199
+ }
200
+
201
+ // Check if it's NOVA token and use specific tree address
202
+ if (mintAddress.equals(NOVA_MINT)) {
203
+ return NOVA_TREE_ADDRESS
204
+ }
205
+
206
+ // For other SPL tokens, derive PDA with mint address
207
+ const [treeAccount] = PublicKey.findProgramAddressSync(
208
+ [Buffer.from('merkle_tree'), mintAddress.toBuffer()],
209
+ PROGRAM_ID
210
+ )
211
+ return treeAccount
212
+ }
213
+
214
+
215
+ export function findCrossCheckNullifierPDAs(proof: any) {
216
+ const [nullifier2PDA] = PublicKey.findProgramAddressSync(
217
+ [Buffer.from("nullifier0"), Buffer.from(proof.inputNullifiers[1])],
218
+ PROGRAM_ID
219
+ );
220
+
221
+ const [nullifier3PDA] = PublicKey.findProgramAddressSync(
222
+ [Buffer.from("nullifier1"), Buffer.from(proof.inputNullifiers[0])],
223
+ PROGRAM_ID
224
+ );
225
+
226
+ return { nullifier2PDA, nullifier3PDA };
227
+ }
228
+
229
+ export function getMintAddressField(mint: PublicKey): string {
230
+ const mintStr = mint.toString();
231
+
232
+ // Special case for SOL (system program)
233
+ if (mintStr === '11111111111111111111111111111112') {
234
+ return mintStr;
235
+ }
236
+
237
+ // For SPL tokens (USDC, USDT, etc): use first 31 bytes (248 bits)
238
+ // This provides better collision resistance than 8 bytes while still fitting in the field
239
+ // We will only suppport private SOL, USDC and USDT send, so there won't be any collision.
240
+ const mintBytes = mint.toBytes();
241
+ return new BN(mintBytes.slice(0, 31), 'be').toString();
242
+ }