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.
- package/.github/workflows/npm-publish.yml +55 -0
- package/PUBLISH.md +122 -0
- package/README.md +177 -0
- package/__tests__/e2e.test.ts +56 -0
- package/__tests__/e2espl.test.ts +73 -0
- package/__tests__/encryption.test.ts +1635 -0
- package/circuit2/transaction2.wasm +0 -0
- package/circuit2/transaction2.zkey +0 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +12 -0
- package/dist/deposit.d.ts +18 -0
- package/dist/deposit.js +392 -0
- package/dist/depositSPL.d.ts +20 -0
- package/dist/depositSPL.js +448 -0
- package/dist/exportUtils.d.ts +11 -0
- package/dist/exportUtils.js +11 -0
- package/dist/getUtxos.d.ts +29 -0
- package/dist/getUtxos.js +294 -0
- package/dist/getUtxosSPL.d.ts +33 -0
- package/dist/getUtxosSPL.js +395 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +302 -0
- package/dist/models/keypair.d.ts +26 -0
- package/dist/models/keypair.js +43 -0
- package/dist/models/utxo.d.ts +49 -0
- package/dist/models/utxo.js +85 -0
- package/dist/utils/address_lookup_table.d.ts +9 -0
- package/dist/utils/address_lookup_table.js +45 -0
- package/dist/utils/constants.d.ts +31 -0
- package/dist/utils/constants.js +62 -0
- package/dist/utils/encryption.d.ts +107 -0
- package/dist/utils/encryption.js +376 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/merkle_tree.d.ts +92 -0
- package/dist/utils/merkle_tree.js +186 -0
- package/dist/utils/node-shim.d.ts +5 -0
- package/dist/utils/node-shim.js +5 -0
- package/dist/utils/prover.d.ts +36 -0
- package/dist/utils/prover.js +147 -0
- package/dist/utils/utils.d.ts +69 -0
- package/dist/utils/utils.js +182 -0
- package/dist/withdraw.d.ts +21 -0
- package/dist/withdraw.js +270 -0
- package/dist/withdrawSPL.d.ts +23 -0
- package/dist/withdrawSPL.js +306 -0
- package/package.json +77 -0
- package/setup-git.sh +51 -0
- package/setup-github.sh +36 -0
- package/src/config.ts +22 -0
- package/src/deposit.ts +487 -0
- package/src/depositSPL.ts +567 -0
- package/src/exportUtils.ts +13 -0
- package/src/getUtxos.ts +396 -0
- package/src/getUtxosSPL.ts +528 -0
- package/src/index.ts +350 -0
- package/src/models/keypair.ts +52 -0
- package/src/models/utxo.ts +106 -0
- package/src/utils/address_lookup_table.ts +78 -0
- package/src/utils/constants.ts +84 -0
- package/src/utils/encryption.ts +464 -0
- package/src/utils/logger.ts +42 -0
- package/src/utils/merkle_tree.ts +207 -0
- package/src/utils/node-shim.ts +6 -0
- package/src/utils/prover.ts +222 -0
- package/src/utils/utils.ts +242 -0
- package/src/withdraw.ts +332 -0
- package/src/withdrawSPL.ts +394 -0
- 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,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
|
+
}
|