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
package/src/index.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, VersionedTransaction } from '@solana/web3.js';
|
|
2
|
+
import { deposit } from './deposit.js';
|
|
3
|
+
import { getBalanceFromUtxos, getUtxos, localstorageKey } from './getUtxos.js';
|
|
4
|
+
import { getBalanceFromUtxosSPL, getUtxosSPL } from './getUtxosSPL.js';
|
|
5
|
+
|
|
6
|
+
import { LSK_ENCRYPTED_OUTPUTS, LSK_FETCH_OFFSET, SplList, TokenList, tokens, USDC_MINT } from './utils/constants.js';
|
|
7
|
+
import { logger, type LoggerFn, setLogger } from './utils/logger.js';
|
|
8
|
+
import { EncryptionService } from './utils/encryption.js';
|
|
9
|
+
import { WasmFactory } from '@lightprotocol/hasher.rs';
|
|
10
|
+
import bs58 from 'bs58'
|
|
11
|
+
import { withdraw } from './withdraw.js';
|
|
12
|
+
import { LocalStorage } from "node-localstorage";
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { depositSPL } from './depositSPL.js';
|
|
15
|
+
import { withdrawSPL } from './withdrawSPL.js';
|
|
16
|
+
import { getAssociatedTokenAddress } from '@solana/spl-token';
|
|
17
|
+
|
|
18
|
+
let storage = new LocalStorage(path.join(process.cwd(), "cache"));
|
|
19
|
+
|
|
20
|
+
export class PrivacyCash {
|
|
21
|
+
private connection: Connection
|
|
22
|
+
public publicKey: PublicKey
|
|
23
|
+
private encryptionService: EncryptionService
|
|
24
|
+
private keypair: Keypair
|
|
25
|
+
private isRuning?: boolean = false
|
|
26
|
+
private status: string = ''
|
|
27
|
+
constructor({ RPC_url, owner, enableDebug }: {
|
|
28
|
+
RPC_url: string,
|
|
29
|
+
owner: string | number[] | Uint8Array | Keypair,
|
|
30
|
+
enableDebug?: boolean
|
|
31
|
+
}) {
|
|
32
|
+
let keypair = getSolanaKeypair(owner)
|
|
33
|
+
if (!keypair) {
|
|
34
|
+
throw new Error('param "owner" is not a valid Private Key or Keypair')
|
|
35
|
+
}
|
|
36
|
+
this.keypair = keypair
|
|
37
|
+
this.connection = new Connection(RPC_url, 'confirmed')
|
|
38
|
+
this.publicKey = keypair.publicKey
|
|
39
|
+
this.encryptionService = new EncryptionService();
|
|
40
|
+
this.encryptionService.deriveEncryptionKeyFromWallet(this.keypair);
|
|
41
|
+
if (!enableDebug) {
|
|
42
|
+
this.startStatusRender()
|
|
43
|
+
this.setLogger((level, message) => {
|
|
44
|
+
if (level == 'info') {
|
|
45
|
+
this.status = message
|
|
46
|
+
} else if (level == 'error') {
|
|
47
|
+
console.log('error message: ', message)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setLogger(loger: LoggerFn) {
|
|
54
|
+
setLogger(loger)
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clears the cache of utxos.
|
|
60
|
+
*
|
|
61
|
+
* By default, downloaded utxos will be cached in the local storage. Thus the next time when you makes another
|
|
62
|
+
* deposit or withdraw or getPrivateBalance, the SDK only fetches the utxos that are not in the cache.
|
|
63
|
+
*
|
|
64
|
+
* This method clears the cache of utxos.
|
|
65
|
+
*/
|
|
66
|
+
async clearCache() {
|
|
67
|
+
if (!this.publicKey) {
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
storage.removeItem(LSK_FETCH_OFFSET + localstorageKey(this.publicKey))
|
|
71
|
+
storage.removeItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(this.publicKey))
|
|
72
|
+
// spl
|
|
73
|
+
for (let token of tokens) {
|
|
74
|
+
let ata = await getAssociatedTokenAddress(
|
|
75
|
+
token.pubkey,
|
|
76
|
+
this.publicKey
|
|
77
|
+
);
|
|
78
|
+
storage.removeItem(LSK_FETCH_OFFSET + localstorageKey(ata))
|
|
79
|
+
storage.removeItem(LSK_ENCRYPTED_OUTPUTS + localstorageKey(ata))
|
|
80
|
+
}
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Deposit SOL to the Privacy Cash.
|
|
86
|
+
*
|
|
87
|
+
* Lamports is the amount of SOL in lamports. e.g. if you want to deposit 0.01 SOL (10000000 lamports), call deposit({ lamports: 10000000 })
|
|
88
|
+
*/
|
|
89
|
+
async deposit({ lamports }: {
|
|
90
|
+
lamports: number
|
|
91
|
+
}) {
|
|
92
|
+
this.isRuning = true
|
|
93
|
+
logger.info('start depositting')
|
|
94
|
+
let lightWasm = await WasmFactory.getInstance()
|
|
95
|
+
let res = await deposit({
|
|
96
|
+
lightWasm,
|
|
97
|
+
amount_in_lamports: lamports,
|
|
98
|
+
connection: this.connection,
|
|
99
|
+
encryptionService: this.encryptionService,
|
|
100
|
+
publicKey: this.publicKey,
|
|
101
|
+
transactionSigner: async (tx: VersionedTransaction) => {
|
|
102
|
+
tx.sign([this.keypair])
|
|
103
|
+
return tx
|
|
104
|
+
},
|
|
105
|
+
keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
|
|
106
|
+
storage
|
|
107
|
+
})
|
|
108
|
+
this.isRuning = false
|
|
109
|
+
return res
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Deposit USDC to the Privacy Cash.
|
|
114
|
+
*/
|
|
115
|
+
async depositUSDC({ base_units }: {
|
|
116
|
+
base_units: number
|
|
117
|
+
}) {
|
|
118
|
+
this.isRuning = true
|
|
119
|
+
logger.info('start depositting')
|
|
120
|
+
let lightWasm = await WasmFactory.getInstance()
|
|
121
|
+
let res = await depositSPL({
|
|
122
|
+
mintAddress: USDC_MINT,
|
|
123
|
+
lightWasm,
|
|
124
|
+
base_units: base_units,
|
|
125
|
+
connection: this.connection,
|
|
126
|
+
encryptionService: this.encryptionService,
|
|
127
|
+
publicKey: this.publicKey,
|
|
128
|
+
transactionSigner: async (tx: VersionedTransaction) => {
|
|
129
|
+
tx.sign([this.keypair])
|
|
130
|
+
return tx
|
|
131
|
+
},
|
|
132
|
+
keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
|
|
133
|
+
storage
|
|
134
|
+
})
|
|
135
|
+
this.isRuning = false
|
|
136
|
+
return res
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Withdraw SOL from the Privacy Cash.
|
|
141
|
+
*
|
|
142
|
+
* Lamports is the amount of SOL in lamports. e.g. if you want to withdraw 0.01 SOL (10000000 lamports), call withdraw({ lamports: 10000000 })
|
|
143
|
+
*/
|
|
144
|
+
async withdraw({ lamports, recipientAddress }: {
|
|
145
|
+
lamports: number,
|
|
146
|
+
recipientAddress?: string
|
|
147
|
+
}) {
|
|
148
|
+
this.isRuning = true
|
|
149
|
+
logger.info('start withdrawing')
|
|
150
|
+
let lightWasm = await WasmFactory.getInstance()
|
|
151
|
+
let recipient = recipientAddress ? new PublicKey(recipientAddress) : this.publicKey
|
|
152
|
+
let res = await withdraw({
|
|
153
|
+
lightWasm,
|
|
154
|
+
amount_in_lamports: lamports,
|
|
155
|
+
connection: this.connection,
|
|
156
|
+
encryptionService: this.encryptionService,
|
|
157
|
+
publicKey: this.publicKey,
|
|
158
|
+
recipient,
|
|
159
|
+
keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
|
|
160
|
+
storage
|
|
161
|
+
})
|
|
162
|
+
logger.debug(`Withdraw successful. Recipient ${recipient} received ${res.amount_in_lamports / LAMPORTS_PER_SOL} SOL, with ${res.fee_in_lamports / LAMPORTS_PER_SOL} SOL relayers fees`)
|
|
163
|
+
this.isRuning = false
|
|
164
|
+
return res
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Withdraw USDC from the Privacy Cash.
|
|
169
|
+
*
|
|
170
|
+
* base_units is the amount of USDC in base unit. e.g. if you want to withdraw 1 USDC (1,000,000 base unit), call withdraw({ base_units: 1000000, recipientAddress: 'some_address' })
|
|
171
|
+
*/
|
|
172
|
+
async withdrawUSDC({ base_units, recipientAddress }: {
|
|
173
|
+
base_units: number,
|
|
174
|
+
recipientAddress?: string
|
|
175
|
+
}) {
|
|
176
|
+
this.isRuning = true
|
|
177
|
+
logger.info('start withdrawing')
|
|
178
|
+
let lightWasm = await WasmFactory.getInstance()
|
|
179
|
+
let recipient = recipientAddress ? new PublicKey(recipientAddress) : this.publicKey
|
|
180
|
+
let res = await withdrawSPL({
|
|
181
|
+
mintAddress: USDC_MINT,
|
|
182
|
+
lightWasm,
|
|
183
|
+
base_units,
|
|
184
|
+
connection: this.connection,
|
|
185
|
+
encryptionService: this.encryptionService,
|
|
186
|
+
publicKey: this.publicKey,
|
|
187
|
+
recipient,
|
|
188
|
+
keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
|
|
189
|
+
storage
|
|
190
|
+
})
|
|
191
|
+
logger.debug(`Withdraw successful. Recipient ${recipient} received ${base_units} USDC units`)
|
|
192
|
+
this.isRuning = false
|
|
193
|
+
return res
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns the amount of lamports current wallet has in Privacy Cash.
|
|
198
|
+
*/
|
|
199
|
+
async getPrivateBalance(abortSignal?: AbortSignal) {
|
|
200
|
+
logger.info('getting private balance')
|
|
201
|
+
this.isRuning = true
|
|
202
|
+
let utxos = await getUtxos({ publicKey: this.publicKey, connection: this.connection, encryptionService: this.encryptionService, storage, abortSignal })
|
|
203
|
+
this.isRuning = false
|
|
204
|
+
return getBalanceFromUtxos(utxos)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns the amount of base unites current wallet has in Privacy Cash.
|
|
209
|
+
*/
|
|
210
|
+
async getPrivateBalanceUSDC() {
|
|
211
|
+
logger.info('getting private balance')
|
|
212
|
+
this.isRuning = true
|
|
213
|
+
let utxos = await getUtxosSPL({ publicKey: this.publicKey, connection: this.connection, encryptionService: this.encryptionService, storage, mintAddress: USDC_MINT })
|
|
214
|
+
this.isRuning = false
|
|
215
|
+
return getBalanceFromUtxosSPL(utxos)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Returns the amount of base unites current wallet has in Privacy Cash.
|
|
220
|
+
*/
|
|
221
|
+
async getPrivateBalanceSpl(mintAddress: PublicKey | string) {
|
|
222
|
+
this.isRuning = true
|
|
223
|
+
let utxos = await getUtxosSPL({
|
|
224
|
+
publicKey: this.publicKey,
|
|
225
|
+
connection: this.connection,
|
|
226
|
+
encryptionService: this.encryptionService,
|
|
227
|
+
storage,
|
|
228
|
+
mintAddress
|
|
229
|
+
})
|
|
230
|
+
this.isRuning = false
|
|
231
|
+
return getBalanceFromUtxosSPL(utxos)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns true if the code is running in a browser.
|
|
236
|
+
*/
|
|
237
|
+
isBrowser() {
|
|
238
|
+
return typeof window !== "undefined"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async startStatusRender() {
|
|
242
|
+
let frames = ['-', '\\', '|', '/'];
|
|
243
|
+
let i = 0
|
|
244
|
+
while (true) {
|
|
245
|
+
if (this.isRuning) {
|
|
246
|
+
let k = i % frames.length
|
|
247
|
+
i++
|
|
248
|
+
stdWrite(this.status, frames[k])
|
|
249
|
+
}
|
|
250
|
+
await new Promise(r => setTimeout(r, 250));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Deposit SPL to the Privacy Cash.
|
|
256
|
+
*/
|
|
257
|
+
async depositSPL({ base_units, mintAddress, amount }: {
|
|
258
|
+
base_units?: number,
|
|
259
|
+
amount?: number,
|
|
260
|
+
mintAddress: PublicKey | string
|
|
261
|
+
}) {
|
|
262
|
+
this.isRuning = true
|
|
263
|
+
logger.info('start depositting')
|
|
264
|
+
let lightWasm = await WasmFactory.getInstance()
|
|
265
|
+
let res = await depositSPL({
|
|
266
|
+
lightWasm,
|
|
267
|
+
base_units,
|
|
268
|
+
amount,
|
|
269
|
+
connection: this.connection,
|
|
270
|
+
encryptionService: this.encryptionService,
|
|
271
|
+
publicKey: this.publicKey,
|
|
272
|
+
transactionSigner: async (tx: VersionedTransaction) => {
|
|
273
|
+
tx.sign([this.keypair])
|
|
274
|
+
return tx
|
|
275
|
+
},
|
|
276
|
+
keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
|
|
277
|
+
storage,
|
|
278
|
+
mintAddress
|
|
279
|
+
})
|
|
280
|
+
this.isRuning = false
|
|
281
|
+
return res
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Withdraw SPL from the Privacy Cash.
|
|
286
|
+
*/
|
|
287
|
+
async withdrawSPL({ base_units, mintAddress, recipientAddress, amount }: {
|
|
288
|
+
base_units?: number,
|
|
289
|
+
amount?: number,
|
|
290
|
+
mintAddress: PublicKey | string,
|
|
291
|
+
recipientAddress?: string
|
|
292
|
+
}) {
|
|
293
|
+
this.isRuning = true
|
|
294
|
+
logger.info('start withdrawing')
|
|
295
|
+
let lightWasm = await WasmFactory.getInstance()
|
|
296
|
+
let recipient = recipientAddress ? new PublicKey(recipientAddress) : this.publicKey
|
|
297
|
+
|
|
298
|
+
let res = await withdrawSPL({
|
|
299
|
+
lightWasm,
|
|
300
|
+
base_units,
|
|
301
|
+
amount,
|
|
302
|
+
connection: this.connection,
|
|
303
|
+
encryptionService: this.encryptionService,
|
|
304
|
+
publicKey: this.publicKey,
|
|
305
|
+
recipient,
|
|
306
|
+
keyBasePath: path.join(import.meta.dirname, '..', 'circuit2', 'transaction2'),
|
|
307
|
+
storage,
|
|
308
|
+
mintAddress
|
|
309
|
+
})
|
|
310
|
+
logger.debug(`Withdraw successful. Recipient ${recipient} received ${base_units} USDC units`)
|
|
311
|
+
this.isRuning = false
|
|
312
|
+
return res
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getSolanaKeypair(
|
|
319
|
+
secret: string | number[] | Uint8Array | Keypair
|
|
320
|
+
): Keypair | null {
|
|
321
|
+
try {
|
|
322
|
+
if (secret instanceof Keypair) {
|
|
323
|
+
return secret;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let keyArray: Uint8Array;
|
|
327
|
+
|
|
328
|
+
if (typeof secret === "string") {
|
|
329
|
+
keyArray = bs58.decode(secret);
|
|
330
|
+
} else if (secret instanceof Uint8Array) {
|
|
331
|
+
keyArray = secret;
|
|
332
|
+
} else {
|
|
333
|
+
// number[]
|
|
334
|
+
keyArray = Uint8Array.from(secret);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (keyArray.length !== 32 && keyArray.length !== 64) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return Keypair.fromSecretKey(keyArray);
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stdWrite(status: string, frame: string) {
|
|
347
|
+
let blue = "\x1b[34m";
|
|
348
|
+
let reset = "\x1b[0m";
|
|
349
|
+
process.stdout.write(`${frame}status: ${blue}${status}${reset}\r`);
|
|
350
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keypair module for ZK Cash
|
|
3
|
+
*
|
|
4
|
+
* Provides cryptographic keypair functionality for the ZK Cash system
|
|
5
|
+
* Based on: https://github.com/tornadocash/tornado-nova
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import BN from 'bn.js';
|
|
9
|
+
import { ethers } from 'ethers';
|
|
10
|
+
import * as hasher from '@lightprotocol/hasher.rs';
|
|
11
|
+
|
|
12
|
+
// Field size constant
|
|
13
|
+
const FIELD_SIZE = new BN(
|
|
14
|
+
'21888242871839275222246405745257275088548364400416034343698204186575808495617'
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Simplified version of Keypair
|
|
19
|
+
*/
|
|
20
|
+
export class Keypair {
|
|
21
|
+
public privkey: BN;
|
|
22
|
+
public pubkey: BN;
|
|
23
|
+
private lightWasm: hasher.LightWasm;
|
|
24
|
+
|
|
25
|
+
constructor(privkeyHex: string, lightWasm: hasher.LightWasm) {
|
|
26
|
+
const rawDecimal = BigInt(privkeyHex);
|
|
27
|
+
this.privkey = new BN((rawDecimal % BigInt(FIELD_SIZE.toString())).toString());
|
|
28
|
+
this.lightWasm = lightWasm;
|
|
29
|
+
// TODO: lazily compute pubkey
|
|
30
|
+
this.pubkey = new BN(this.lightWasm.poseidonHashString([this.privkey.toString()]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sign a message using keypair private key
|
|
35
|
+
*
|
|
36
|
+
* @param {string|number|BigNumber} commitment a hex string with commitment
|
|
37
|
+
* @param {string|number|BigNumber} merklePath a hex string with merkle path
|
|
38
|
+
* @returns {BigNumber} a hex string with signature
|
|
39
|
+
*/
|
|
40
|
+
sign(commitment: string, merklePath: string): string {
|
|
41
|
+
return this.lightWasm.poseidonHashString([this.privkey.toString(), commitment, merklePath]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static async generateNew(lightWasm: hasher.LightWasm): Promise<Keypair> {
|
|
45
|
+
// Tornado Cash Nova uses ethers.js to generate a random private key
|
|
46
|
+
// We can't generate Solana keypairs because it won't fit in the field size
|
|
47
|
+
// It's OK to use ethereum secret keys, because the secret key is only used for the proof generation.
|
|
48
|
+
// Namely, it's used to guarantee the uniqueness of the nullifier.
|
|
49
|
+
const wallet = ethers.Wallet.createRandom();
|
|
50
|
+
return new Keypair(wallet.privateKey, lightWasm);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXO (Unspent Transaction Output) module for ZK Cash
|
|
3
|
+
*
|
|
4
|
+
* Provides UTXO functionality for the ZK Cash system
|
|
5
|
+
* Based on: https://github.com/tornadocash/tornado-nova
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import BN from 'bn.js';
|
|
9
|
+
import { Keypair } from './keypair.js';
|
|
10
|
+
import * as hasher from '@lightprotocol/hasher.rs';
|
|
11
|
+
import { ethers } from 'ethers';
|
|
12
|
+
import { getMintAddressField } from '../utils/utils.js';
|
|
13
|
+
import { PublicKey } from '@solana/web3.js';
|
|
14
|
+
/**
|
|
15
|
+
* Simplified Utxo class inspired by Tornado Cash Nova
|
|
16
|
+
* Based on: https://github.com/tornadocash/tornado-nova/blob/f9264eeffe48bf5e04e19d8086ee6ec58cdf0d9e/src/utxo.js
|
|
17
|
+
*/
|
|
18
|
+
export class Utxo {
|
|
19
|
+
amount: BN;
|
|
20
|
+
blinding: BN;
|
|
21
|
+
keypair: Keypair;
|
|
22
|
+
index: number;
|
|
23
|
+
mintAddress: string;
|
|
24
|
+
version: 'v1' | 'v2';
|
|
25
|
+
private lightWasm: hasher.LightWasm;
|
|
26
|
+
|
|
27
|
+
constructor({
|
|
28
|
+
lightWasm,
|
|
29
|
+
amount = new BN(0),
|
|
30
|
+
/**
|
|
31
|
+
* Tornado nova doesn't use solana eddsa with curve 25519 but their own "keypair"
|
|
32
|
+
* which is:
|
|
33
|
+
* - private key: random [31;u8]
|
|
34
|
+
* - public key: PoseidonHash(privateKey)
|
|
35
|
+
*
|
|
36
|
+
* Generate a new keypair for each UTXO
|
|
37
|
+
*/
|
|
38
|
+
keypair,
|
|
39
|
+
blinding = new BN(Math.floor(Math.random() * 1000000000)), // Use fixed value for consistency instead of randomBN()
|
|
40
|
+
index = 0,
|
|
41
|
+
mintAddress = '11111111111111111111111111111112', // Default to Solana native SOL mint address,
|
|
42
|
+
version = 'v2'
|
|
43
|
+
}: {
|
|
44
|
+
lightWasm: hasher.LightWasm,
|
|
45
|
+
amount?: BN | number | string,
|
|
46
|
+
keypair?: Keypair,
|
|
47
|
+
blinding?: BN | number | string,
|
|
48
|
+
index?: number,
|
|
49
|
+
mintAddress?: string,
|
|
50
|
+
version?: 'v1' | 'v2'
|
|
51
|
+
}) {
|
|
52
|
+
this.amount = new BN(amount.toString());
|
|
53
|
+
this.blinding = new BN(blinding.toString());
|
|
54
|
+
this.lightWasm = lightWasm;
|
|
55
|
+
this.keypair = keypair || new Keypair(ethers.Wallet.createRandom().privateKey, lightWasm);
|
|
56
|
+
this.index = index;
|
|
57
|
+
this.mintAddress = mintAddress;
|
|
58
|
+
this.version = version;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getCommitment(): Promise<string> {
|
|
62
|
+
// return this.lightWasm.poseidonHashString([this.amount.toString(), this.keypair.pubkey.toString(), this.blinding.toString(), this.mintAddress]);
|
|
63
|
+
const mintAddressField = getMintAddressField(new PublicKey(this.mintAddress));
|
|
64
|
+
return this.lightWasm.poseidonHashString([
|
|
65
|
+
this.amount.toString(),
|
|
66
|
+
this.keypair.pubkey.toString(),
|
|
67
|
+
this.blinding.toString(),
|
|
68
|
+
mintAddressField
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getNullifier(): Promise<string> {
|
|
73
|
+
const commitmentValue = await this.getCommitment();
|
|
74
|
+
const signature = this.keypair.sign(commitmentValue, new BN(this.index).toString());
|
|
75
|
+
|
|
76
|
+
return this.lightWasm.poseidonHashString([commitmentValue, new BN(this.index).toString(), signature]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Log all the UTXO's public properties and derived values in JSON format
|
|
81
|
+
* @returns Promise that resolves once all logging is complete
|
|
82
|
+
*/
|
|
83
|
+
async log(): Promise<void> {
|
|
84
|
+
// Prepare the UTXO data object
|
|
85
|
+
const utxoData: any = {
|
|
86
|
+
amount: this.amount.toString(),
|
|
87
|
+
blinding: this.blinding.toString(),
|
|
88
|
+
index: this.index,
|
|
89
|
+
mintAddress: this.mintAddress,
|
|
90
|
+
keypair: {
|
|
91
|
+
pubkey: this.keypair.pubkey.toString()
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Add derived values
|
|
96
|
+
try {
|
|
97
|
+
utxoData.commitment = await this.getCommitment();
|
|
98
|
+
utxoData.nullifier = await this.getNullifier();
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
utxoData.error = error.message;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Output as formatted JSON
|
|
104
|
+
console.log(JSON.stringify(utxoData, null, 2));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Connection,
|
|
3
|
+
Keypair,
|
|
4
|
+
PublicKey,
|
|
5
|
+
SystemProgram,
|
|
6
|
+
AddressLookupTableProgram,
|
|
7
|
+
Transaction,
|
|
8
|
+
sendAndConfirmTransaction,
|
|
9
|
+
ComputeBudgetProgram,
|
|
10
|
+
VersionedTransaction,
|
|
11
|
+
TransactionMessage
|
|
12
|
+
} from '@solana/web3.js';
|
|
13
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
14
|
+
import { logger } from './logger.js';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper function to use an existing ALT (recommended for production)
|
|
19
|
+
* Use create_alt.ts to create the ALT once, then hardcode the address and use this function
|
|
20
|
+
*/
|
|
21
|
+
export async function useExistingALT(
|
|
22
|
+
connection: Connection,
|
|
23
|
+
altAddress: PublicKey
|
|
24
|
+
): Promise<{ value: any } | null> {
|
|
25
|
+
try {
|
|
26
|
+
logger.debug(`Using existing ALT: ${altAddress.toString()}`);
|
|
27
|
+
const altAccount = await connection.getAddressLookupTable(altAddress);
|
|
28
|
+
|
|
29
|
+
if (altAccount.value) {
|
|
30
|
+
logger.debug(`✅ ALT found with ${altAccount.value.state.addresses.length} addresses`);
|
|
31
|
+
} else {
|
|
32
|
+
logger.error('❌ ALT not found');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return altAccount;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error getting existing ALT:', error);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function getProtocolAddressesWithMint(
|
|
44
|
+
programId: PublicKey,
|
|
45
|
+
authority: PublicKey,
|
|
46
|
+
treeAta: PublicKey,
|
|
47
|
+
feeRecipient: PublicKey,
|
|
48
|
+
feeRecipientAta: PublicKey
|
|
49
|
+
): PublicKey[] {
|
|
50
|
+
// Derive global config PDA
|
|
51
|
+
const [globalConfigAccount] = PublicKey.findProgramAddressSync(
|
|
52
|
+
[Buffer.from('global_config')],
|
|
53
|
+
programId
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Derive tree accounts
|
|
57
|
+
const [treeAccount] = PublicKey.findProgramAddressSync(
|
|
58
|
+
[Buffer.from('merkle_tree')],
|
|
59
|
+
programId
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
// Core program accounts (constant)
|
|
64
|
+
programId,
|
|
65
|
+
treeAccount,
|
|
66
|
+
treeAta,
|
|
67
|
+
globalConfigAccount,
|
|
68
|
+
authority,
|
|
69
|
+
feeRecipient,
|
|
70
|
+
feeRecipientAta,
|
|
71
|
+
|
|
72
|
+
// System programs (constant)
|
|
73
|
+
SystemProgram.programId,
|
|
74
|
+
ComputeBudgetProgram.programId,
|
|
75
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
76
|
+
TOKEN_PROGRAM_ID,
|
|
77
|
+
];
|
|
78
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { PublicKey } from '@solana/web3.js';
|
|
2
|
+
import BN from 'bn.js';
|
|
3
|
+
|
|
4
|
+
export const FIELD_SIZE = new BN('21888242871839275222246405745257275088548364400416034343698204186575808495617')
|
|
5
|
+
|
|
6
|
+
export const PROGRAM_ID = process.env.NEXT_PUBLIC_PROGRAM_ID ? new PublicKey(process.env.NEXT_PUBLIC_PROGRAM_ID) : new PublicKey('SHLD11qQaY6h2N3ozXUWJEJitk1zGm9tvvAC7rjxpsp');
|
|
7
|
+
|
|
8
|
+
export const FEE_RECIPIENT = new PublicKey('AWexibGxNFKTa1b5R5MN4PJr9HWnWRwf8EW9g8cLx3dM')
|
|
9
|
+
|
|
10
|
+
export const FETCH_UTXOS_GROUP_SIZE = 20_000
|
|
11
|
+
|
|
12
|
+
export const TRANSACT_IX_DISCRIMINATOR = Buffer.from([217, 149, 130, 143, 221, 52, 252, 119]);
|
|
13
|
+
|
|
14
|
+
export const TRANSACT_SPL_IX_DISCRIMINATOR = Buffer.from([154, 66, 244, 204, 78, 225, 163, 151]);
|
|
15
|
+
|
|
16
|
+
export const MERKLE_TREE_DEPTH = 26;
|
|
17
|
+
|
|
18
|
+
export const ALT_ADDRESS = process.env.NEXT_PUBLIC_ALT_ADDRESS ? new PublicKey(process.env.NEXT_PUBLIC_ALT_ADDRESS) : new PublicKey('HEN49U2ySJ85Vc78qprSW9y6mFDhs1NczRxyppNHjofe');
|
|
19
|
+
|
|
20
|
+
export const RELAYER_API_URL = process.env.NEXT_PUBLIC_RELAYER_API_URL ?? 'https://api3.privacycash.org';
|
|
21
|
+
|
|
22
|
+
export const SIGN_MESSAGE = `Privacy Money account sign in`
|
|
23
|
+
|
|
24
|
+
// localStorage cache keys
|
|
25
|
+
export const LSK_FETCH_OFFSET = 'fetch_offset'
|
|
26
|
+
export const LSK_ENCRYPTED_OUTPUTS = 'encrypted_outputs'
|
|
27
|
+
|
|
28
|
+
export const USDC_MINT = process.env.NEXT_PUBLIC_USDC_MINT ? new PublicKey(process.env.NEXT_PUBLIC_USDC_MINT) : new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
|
|
29
|
+
|
|
30
|
+
// Tree addresses for SOL (NOVASOL), NOVA, and legacy SOL
|
|
31
|
+
export const SOL_TREE_ADDRESS = process.env.NEXT_PUBLIC_SOL_TREE ? new PublicKey(process.env.NEXT_PUBLIC_SOL_TREE) : new PublicKey('DUnk81NBY7gUYRekT7GAwTU7jvabiVVwcMKZkRDs3dW9')
|
|
32
|
+
export const NOVA_TREE_ADDRESS = process.env.NEXT_PUBLIC_NOVA_TREE ? new PublicKey(process.env.NEXT_PUBLIC_NOVA_TREE) : new PublicKey('9CJ8jwAXJ7GrYtrMmD4gXBpTC59DMX7a91yMVpz25enZ')
|
|
33
|
+
// NOVASOL is the SOL tree used in Nova Privacy program
|
|
34
|
+
export const NOVASOL_TREE_ADDRESS = process.env.NEXT_PUBLIC_NOVASOL_TREE ? new PublicKey(process.env.NEXT_PUBLIC_NOVASOL_TREE) : new PublicKey('FZfbN2fY3aj7hTwh87dKMfuVwpaEipY3TuGpmYJnsWcG')
|
|
35
|
+
export const NOVA_MINT = process.env.NEXT_PUBLIC_NOVA_MINT ? new PublicKey(process.env.NEXT_PUBLIC_NOVA_MINT) : new PublicKey('3SkFJRqMPTKZLqKK1MmY2mvAm711FGAtJ9ZbL6r1coin')
|
|
36
|
+
|
|
37
|
+
const tokenList = ['sol', 'usdc', 'usdt', 'zec', 'ore', 'nova'] as const;
|
|
38
|
+
export type TokenList = typeof tokenList[number];
|
|
39
|
+
const splList = ['usdc', 'usdt', 'zec', 'ore', 'nova'] as const;
|
|
40
|
+
export type SplList = typeof splList[number];
|
|
41
|
+
export type Token = {
|
|
42
|
+
name: TokenList
|
|
43
|
+
prefix: string
|
|
44
|
+
units_per_token: number
|
|
45
|
+
pubkey: PublicKey
|
|
46
|
+
}
|
|
47
|
+
export const tokens: Token[] = [
|
|
48
|
+
{
|
|
49
|
+
name: 'sol',
|
|
50
|
+
pubkey: new PublicKey('So11111111111111111111111111111111111111112'),
|
|
51
|
+
prefix: '',
|
|
52
|
+
units_per_token: 1e9
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'usdc',
|
|
56
|
+
pubkey: process.env.NEXT_PUBLIC_USDC_MINT ? new PublicKey(process.env.NEXT_PUBLIC_USDC_MINT) : new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
|
|
57
|
+
prefix: 'usdc_',
|
|
58
|
+
units_per_token: 1e6
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'usdt',
|
|
62
|
+
pubkey: process.env.NEXT_PUBLIC_USDT_MINT ? new PublicKey(process.env.NEXT_PUBLIC_USDT_MINT) : new PublicKey('Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'),
|
|
63
|
+
prefix: 'usdt_',
|
|
64
|
+
units_per_token: 1e6
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'zec',
|
|
68
|
+
pubkey: process.env.NEXT_PUBLIC_ZEC_MINT ? new PublicKey(process.env.NEXT_PUBLIC_ZEC_MINT) : new PublicKey('A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS'),
|
|
69
|
+
prefix: 'zec_',
|
|
70
|
+
units_per_token: 1e8
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'ore',
|
|
74
|
+
pubkey: process.env.NEXT_PUBLIC_ORE_MINT ? new PublicKey(process.env.NEXT_PUBLIC_ORE_MINT) : new PublicKey('oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp'),
|
|
75
|
+
prefix: 'ore_',
|
|
76
|
+
units_per_token: 1e11
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'nova',
|
|
80
|
+
pubkey: NOVA_MINT,
|
|
81
|
+
prefix: 'nova_',
|
|
82
|
+
units_per_token: 1e9
|
|
83
|
+
},
|
|
84
|
+
]
|