@hula-privacy/mixer 0.2.0 → 0.3.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/dist/index.d.mts +933 -0
- package/dist/index.d.ts +933 -0
- package/dist/index.js +2587 -0
- package/dist/index.mjs +2480 -0
- package/package.json +1 -8
- package/src/api.ts +5 -1
- package/src/crypto.ts +32 -12
- package/src/idl.ts +838 -0
- package/src/merkle.ts +3 -1
- package/src/transaction.ts +3 -4
- package/src/types.ts +2 -0
- package/src/utxo.ts +26 -11
- package/src/wallet.ts +238 -36
package/dist/index.js
ADDED
|
@@ -0,0 +1,2587 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DOMAIN_ENCRYPTION: () => DOMAIN_ENCRYPTION,
|
|
34
|
+
DOMAIN_NULLIFIER: () => DOMAIN_NULLIFIER,
|
|
35
|
+
DOMAIN_OWNER: () => DOMAIN_OWNER,
|
|
36
|
+
DOMAIN_VIEWING: () => DOMAIN_VIEWING,
|
|
37
|
+
FIELD_PRIME: () => FIELD_PRIME,
|
|
38
|
+
HulaWallet: () => HulaWallet,
|
|
39
|
+
MAX_LEAVES: () => MAX_LEAVES,
|
|
40
|
+
MERKLE_TREE_DEPTH: () => MERKLE_TREE_DEPTH,
|
|
41
|
+
MERKLE_TREE_SEED: () => MERKLE_TREE_SEED,
|
|
42
|
+
NULLIFIER_SEED: () => NULLIFIER_SEED,
|
|
43
|
+
NUM_INPUT_UTXOS: () => NUM_INPUT_UTXOS,
|
|
44
|
+
NUM_OUTPUT_UTXOS: () => NUM_OUTPUT_UTXOS,
|
|
45
|
+
POOL_SEED: () => POOL_SEED,
|
|
46
|
+
PROGRAM_ID: () => PROGRAM_ID,
|
|
47
|
+
PROOF_SIZE: () => PROOF_SIZE,
|
|
48
|
+
RelayerClient: () => RelayerClient,
|
|
49
|
+
TOKEN_2022_PROGRAM_ID: () => TOKEN_2022_PROGRAM_ID,
|
|
50
|
+
VAULT_SEED: () => VAULT_SEED,
|
|
51
|
+
bigIntToBytes: () => bigIntToBytes,
|
|
52
|
+
bigIntToBytes32: () => bigIntToBytes32,
|
|
53
|
+
buildTransaction: () => buildTransaction,
|
|
54
|
+
buildTransactionAccounts: () => buildTransactionAccounts,
|
|
55
|
+
bytesToBigInt: () => bytesToBigInt,
|
|
56
|
+
bytesToHex: () => bytesToHex,
|
|
57
|
+
calculateBalance: () => calculateBalance,
|
|
58
|
+
computeCommitment: () => computeCommitment,
|
|
59
|
+
computeMerklePathFromLeaves: () => computeMerklePathFromLeaves,
|
|
60
|
+
computeMerkleRoot: () => computeMerkleRoot,
|
|
61
|
+
computeNullifier: () => computeNullifier,
|
|
62
|
+
computeNullifierFromKeys: () => computeNullifierFromKeys,
|
|
63
|
+
computeZeros: () => computeZeros,
|
|
64
|
+
createDummyUTXO: () => createDummyUTXO,
|
|
65
|
+
createUTXO: () => createUTXO,
|
|
66
|
+
decryptNote: () => decryptNote,
|
|
67
|
+
deriveKeys: () => deriveKeys,
|
|
68
|
+
deriveSpendingKeyFromSignature: () => deriveSpendingKeyFromSignature,
|
|
69
|
+
deserializeEncryptedNote: () => deserializeEncryptedNote,
|
|
70
|
+
deserializeUTXO: () => deserializeUTXO,
|
|
71
|
+
encryptNote: () => encryptNote,
|
|
72
|
+
fetchMerklePath: () => fetchMerklePath,
|
|
73
|
+
fetchMerkleRoot: () => fetchMerkleRoot,
|
|
74
|
+
formatCommitment: () => formatCommitment,
|
|
75
|
+
generateProof: () => generateProof,
|
|
76
|
+
generateProofInMemory: () => generateProofInMemory,
|
|
77
|
+
generateSpendingKey: () => generateSpendingKey,
|
|
78
|
+
getCircuitPaths: () => getCircuitPaths,
|
|
79
|
+
getCurrentTreeIndex: () => getCurrentTreeIndex,
|
|
80
|
+
getEmptyTreeRoot: () => getEmptyTreeRoot,
|
|
81
|
+
getKeyDerivationMessage: () => getKeyDerivationMessage,
|
|
82
|
+
getMerkleTreePDA: () => getMerkleTreePDA,
|
|
83
|
+
getNextLeafIndex: () => getNextLeafIndex,
|
|
84
|
+
getNullifierPDA: () => getNullifierPDA,
|
|
85
|
+
getPoolPDA: () => getPoolPDA,
|
|
86
|
+
getPoseidon: () => getPoseidon,
|
|
87
|
+
getRelayerClient: () => getRelayerClient,
|
|
88
|
+
getVaultPDA: () => getVaultPDA,
|
|
89
|
+
getZeroAtLevel: () => getZeroAtLevel,
|
|
90
|
+
hexToBytes: () => hexToBytes,
|
|
91
|
+
initHulaSDK: () => initHulaSDK,
|
|
92
|
+
initPoseidon: () => initPoseidon,
|
|
93
|
+
isPoseidonInitialized: () => isPoseidonInitialized,
|
|
94
|
+
parseProof: () => parseProof,
|
|
95
|
+
poseidonHash: () => poseidonHash,
|
|
96
|
+
pubkeyToBigInt: () => pubkeyToBigInt,
|
|
97
|
+
scanNotesForUTXOs: () => scanNotesForUTXOs,
|
|
98
|
+
selectUTXOs: () => selectUTXOs,
|
|
99
|
+
serializeEncryptedNote: () => serializeEncryptedNote,
|
|
100
|
+
serializeUTXO: () => serializeUTXO,
|
|
101
|
+
setCircuitPaths: () => setCircuitPaths,
|
|
102
|
+
setDefaultRelayerUrl: () => setDefaultRelayerUrl,
|
|
103
|
+
syncUTXOs: () => syncUTXOs,
|
|
104
|
+
toAnchorPublicInputs: () => toAnchorPublicInputs,
|
|
105
|
+
verifyCircuitFiles: () => verifyCircuitFiles,
|
|
106
|
+
verifyMerklePath: () => verifyMerklePath
|
|
107
|
+
});
|
|
108
|
+
module.exports = __toCommonJS(index_exports);
|
|
109
|
+
|
|
110
|
+
// src/wallet.ts
|
|
111
|
+
var import_web34 = require("@solana/web3.js");
|
|
112
|
+
var import_anchor2 = require("@coral-xyz/anchor");
|
|
113
|
+
|
|
114
|
+
// src/idl.ts
|
|
115
|
+
var HulaPrivacyIdl = {
|
|
116
|
+
"address": "tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA",
|
|
117
|
+
"metadata": {
|
|
118
|
+
"name": "hula_privacy",
|
|
119
|
+
"version": "0.1.0",
|
|
120
|
+
"spec": "0.1.0",
|
|
121
|
+
"description": "Privacy wallet for Solana using ZK proofs"
|
|
122
|
+
},
|
|
123
|
+
"instructions": [
|
|
124
|
+
{
|
|
125
|
+
"name": "initialize_new_tree",
|
|
126
|
+
"docs": [
|
|
127
|
+
"Initialize a new merkle tree when the current one is full"
|
|
128
|
+
],
|
|
129
|
+
"discriminator": [
|
|
130
|
+
42,
|
|
131
|
+
154,
|
|
132
|
+
29,
|
|
133
|
+
105,
|
|
134
|
+
242,
|
|
135
|
+
162,
|
|
136
|
+
191,
|
|
137
|
+
224
|
|
138
|
+
],
|
|
139
|
+
"accounts": [
|
|
140
|
+
{
|
|
141
|
+
"name": "payer",
|
|
142
|
+
"docs": [
|
|
143
|
+
"The payer for account creation"
|
|
144
|
+
],
|
|
145
|
+
"writable": true,
|
|
146
|
+
"signer": true
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "pool",
|
|
150
|
+
"docs": [
|
|
151
|
+
"Global privacy pool account"
|
|
152
|
+
],
|
|
153
|
+
"writable": true,
|
|
154
|
+
"pda": {
|
|
155
|
+
"seeds": [
|
|
156
|
+
{
|
|
157
|
+
"kind": "const",
|
|
158
|
+
"value": [
|
|
159
|
+
104,
|
|
160
|
+
117,
|
|
161
|
+
108,
|
|
162
|
+
97,
|
|
163
|
+
95,
|
|
164
|
+
112,
|
|
165
|
+
111,
|
|
166
|
+
111,
|
|
167
|
+
108
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "current_tree",
|
|
175
|
+
"docs": [
|
|
176
|
+
"The current (full) merkle tree"
|
|
177
|
+
],
|
|
178
|
+
"pda": {
|
|
179
|
+
"seeds": [
|
|
180
|
+
{
|
|
181
|
+
"kind": "const",
|
|
182
|
+
"value": [
|
|
183
|
+
109,
|
|
184
|
+
101,
|
|
185
|
+
114,
|
|
186
|
+
107,
|
|
187
|
+
108,
|
|
188
|
+
101,
|
|
189
|
+
95,
|
|
190
|
+
116,
|
|
191
|
+
114,
|
|
192
|
+
101,
|
|
193
|
+
101
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"kind": "account",
|
|
198
|
+
"path": "pool.current_tree_index",
|
|
199
|
+
"account": "PrivacyPool"
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "new_tree",
|
|
206
|
+
"docs": [
|
|
207
|
+
"New merkle tree account (PDA with tree_index)"
|
|
208
|
+
],
|
|
209
|
+
"writable": true,
|
|
210
|
+
"pda": {
|
|
211
|
+
"seeds": [
|
|
212
|
+
{
|
|
213
|
+
"kind": "const",
|
|
214
|
+
"value": [
|
|
215
|
+
109,
|
|
216
|
+
101,
|
|
217
|
+
114,
|
|
218
|
+
107,
|
|
219
|
+
108,
|
|
220
|
+
101,
|
|
221
|
+
95,
|
|
222
|
+
116,
|
|
223
|
+
114,
|
|
224
|
+
101,
|
|
225
|
+
101
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"kind": "arg",
|
|
230
|
+
"path": "tree_index"
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "system_program",
|
|
237
|
+
"docs": [
|
|
238
|
+
"System program"
|
|
239
|
+
],
|
|
240
|
+
"address": "11111111111111111111111111111111"
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
"args": [
|
|
244
|
+
{
|
|
245
|
+
"name": "tree_index",
|
|
246
|
+
"type": "u32"
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "initialize_pool",
|
|
252
|
+
"docs": [
|
|
253
|
+
"Initialize the global privacy pool"
|
|
254
|
+
],
|
|
255
|
+
"discriminator": [
|
|
256
|
+
95,
|
|
257
|
+
180,
|
|
258
|
+
10,
|
|
259
|
+
172,
|
|
260
|
+
84,
|
|
261
|
+
174,
|
|
262
|
+
232,
|
|
263
|
+
40
|
|
264
|
+
],
|
|
265
|
+
"accounts": [
|
|
266
|
+
{
|
|
267
|
+
"name": "authority",
|
|
268
|
+
"docs": [
|
|
269
|
+
"The authority creating the pool (pays for account creation)"
|
|
270
|
+
],
|
|
271
|
+
"writable": true,
|
|
272
|
+
"signer": true
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "pool",
|
|
276
|
+
"docs": [
|
|
277
|
+
"Global privacy pool account (PDA)"
|
|
278
|
+
],
|
|
279
|
+
"writable": true,
|
|
280
|
+
"pda": {
|
|
281
|
+
"seeds": [
|
|
282
|
+
{
|
|
283
|
+
"kind": "const",
|
|
284
|
+
"value": [
|
|
285
|
+
104,
|
|
286
|
+
117,
|
|
287
|
+
108,
|
|
288
|
+
97,
|
|
289
|
+
95,
|
|
290
|
+
112,
|
|
291
|
+
111,
|
|
292
|
+
111,
|
|
293
|
+
108
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"name": "merkle_tree",
|
|
301
|
+
"docs": [
|
|
302
|
+
"Merkle tree account (PDA with tree index 0)"
|
|
303
|
+
],
|
|
304
|
+
"writable": true,
|
|
305
|
+
"pda": {
|
|
306
|
+
"seeds": [
|
|
307
|
+
{
|
|
308
|
+
"kind": "const",
|
|
309
|
+
"value": [
|
|
310
|
+
109,
|
|
311
|
+
101,
|
|
312
|
+
114,
|
|
313
|
+
107,
|
|
314
|
+
108,
|
|
315
|
+
101,
|
|
316
|
+
95,
|
|
317
|
+
116,
|
|
318
|
+
114,
|
|
319
|
+
101,
|
|
320
|
+
101
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"kind": "const",
|
|
325
|
+
"value": [
|
|
326
|
+
0,
|
|
327
|
+
0,
|
|
328
|
+
0,
|
|
329
|
+
0
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "system_program",
|
|
337
|
+
"docs": [
|
|
338
|
+
"System program"
|
|
339
|
+
],
|
|
340
|
+
"address": "11111111111111111111111111111111"
|
|
341
|
+
}
|
|
342
|
+
],
|
|
343
|
+
"args": []
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"name": "transact",
|
|
347
|
+
"docs": [
|
|
348
|
+
"Execute a private transaction"
|
|
349
|
+
],
|
|
350
|
+
"discriminator": [
|
|
351
|
+
217,
|
|
352
|
+
149,
|
|
353
|
+
130,
|
|
354
|
+
143,
|
|
355
|
+
221,
|
|
356
|
+
52,
|
|
357
|
+
252,
|
|
358
|
+
119
|
|
359
|
+
],
|
|
360
|
+
"accounts": [
|
|
361
|
+
{
|
|
362
|
+
"name": "payer",
|
|
363
|
+
"writable": true,
|
|
364
|
+
"signer": true
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"name": "mint"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"name": "pool",
|
|
371
|
+
"writable": true,
|
|
372
|
+
"pda": {
|
|
373
|
+
"seeds": [
|
|
374
|
+
{
|
|
375
|
+
"kind": "const",
|
|
376
|
+
"value": [
|
|
377
|
+
104,
|
|
378
|
+
117,
|
|
379
|
+
108,
|
|
380
|
+
97,
|
|
381
|
+
95,
|
|
382
|
+
112,
|
|
383
|
+
111,
|
|
384
|
+
111,
|
|
385
|
+
108
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"name": "input_tree",
|
|
393
|
+
"docs": [
|
|
394
|
+
"Input tree - where the input UTXOs come from (for merkle root verification)",
|
|
395
|
+
"If None, defaults to current tree"
|
|
396
|
+
],
|
|
397
|
+
"pda": {
|
|
398
|
+
"seeds": [
|
|
399
|
+
{
|
|
400
|
+
"kind": "const",
|
|
401
|
+
"value": [
|
|
402
|
+
109,
|
|
403
|
+
101,
|
|
404
|
+
114,
|
|
405
|
+
107,
|
|
406
|
+
108,
|
|
407
|
+
101,
|
|
408
|
+
95,
|
|
409
|
+
116,
|
|
410
|
+
114,
|
|
411
|
+
101,
|
|
412
|
+
101
|
|
413
|
+
]
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
"kind": "arg",
|
|
417
|
+
"path": "input_tree_index.unwrap_or(pool.current_tree_index)"
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"name": "merkle_tree",
|
|
424
|
+
"docs": [
|
|
425
|
+
"Output tree - where new commitments will be inserted (current active tree)"
|
|
426
|
+
],
|
|
427
|
+
"writable": true,
|
|
428
|
+
"pda": {
|
|
429
|
+
"seeds": [
|
|
430
|
+
{
|
|
431
|
+
"kind": "const",
|
|
432
|
+
"value": [
|
|
433
|
+
109,
|
|
434
|
+
101,
|
|
435
|
+
114,
|
|
436
|
+
107,
|
|
437
|
+
108,
|
|
438
|
+
101,
|
|
439
|
+
95,
|
|
440
|
+
116,
|
|
441
|
+
114,
|
|
442
|
+
101,
|
|
443
|
+
101
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"kind": "account",
|
|
448
|
+
"path": "pool.current_tree_index",
|
|
449
|
+
"account": "PrivacyPool"
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"name": "vault",
|
|
456
|
+
"docs": [
|
|
457
|
+
"Vault for this mint (created if needed)"
|
|
458
|
+
],
|
|
459
|
+
"writable": true,
|
|
460
|
+
"pda": {
|
|
461
|
+
"seeds": [
|
|
462
|
+
{
|
|
463
|
+
"kind": "const",
|
|
464
|
+
"value": [
|
|
465
|
+
118,
|
|
466
|
+
97,
|
|
467
|
+
117,
|
|
468
|
+
108,
|
|
469
|
+
116
|
|
470
|
+
]
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"kind": "account",
|
|
474
|
+
"path": "mint"
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
"name": "depositor_token_account",
|
|
481
|
+
"docs": [
|
|
482
|
+
"Depositor's token account (optional)"
|
|
483
|
+
],
|
|
484
|
+
"writable": true,
|
|
485
|
+
"optional": true
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
"name": "depositor",
|
|
489
|
+
"signer": true,
|
|
490
|
+
"optional": true
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"name": "recipient_token_account",
|
|
494
|
+
"docs": [
|
|
495
|
+
"Recipient's token account (optional)"
|
|
496
|
+
],
|
|
497
|
+
"writable": true,
|
|
498
|
+
"optional": true
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"name": "fee_recipient_token_account",
|
|
502
|
+
"docs": [
|
|
503
|
+
"Fee recipient's token account (optional)"
|
|
504
|
+
],
|
|
505
|
+
"writable": true,
|
|
506
|
+
"optional": true
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
"name": "nullifier_account_0",
|
|
510
|
+
"docs": [
|
|
511
|
+
"Nullifier account 0 (optional - for first input UTXO)"
|
|
512
|
+
],
|
|
513
|
+
"writable": true,
|
|
514
|
+
"optional": true
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"name": "nullifier_account_1",
|
|
518
|
+
"docs": [
|
|
519
|
+
"Nullifier account 1 (optional - for second input UTXO)"
|
|
520
|
+
],
|
|
521
|
+
"writable": true,
|
|
522
|
+
"optional": true
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"name": "token_program",
|
|
526
|
+
"address": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
"name": "system_program",
|
|
530
|
+
"address": "11111111111111111111111111111111"
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
"args": [
|
|
534
|
+
{
|
|
535
|
+
"name": "input_tree_index",
|
|
536
|
+
"type": {
|
|
537
|
+
"option": "u32"
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"name": "proof",
|
|
542
|
+
"type": {
|
|
543
|
+
"array": [
|
|
544
|
+
"u8",
|
|
545
|
+
256
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"name": "public_inputs",
|
|
551
|
+
"type": {
|
|
552
|
+
"defined": {
|
|
553
|
+
"name": "PublicInputs"
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
"name": "encrypted_notes",
|
|
559
|
+
"type": {
|
|
560
|
+
"vec": "bytes"
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
],
|
|
566
|
+
"accounts": [
|
|
567
|
+
{
|
|
568
|
+
"name": "MerkleTreeAccount",
|
|
569
|
+
"discriminator": [
|
|
570
|
+
147,
|
|
571
|
+
200,
|
|
572
|
+
34,
|
|
573
|
+
248,
|
|
574
|
+
131,
|
|
575
|
+
187,
|
|
576
|
+
248,
|
|
577
|
+
253
|
|
578
|
+
]
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
"name": "PrivacyPool",
|
|
582
|
+
"discriminator": [
|
|
583
|
+
133,
|
|
584
|
+
184,
|
|
585
|
+
191,
|
|
586
|
+
79,
|
|
587
|
+
252,
|
|
588
|
+
142,
|
|
589
|
+
190,
|
|
590
|
+
150
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
"errors": [
|
|
595
|
+
{
|
|
596
|
+
"code": 6e3,
|
|
597
|
+
"name": "Groth16MulFailed",
|
|
598
|
+
"msg": "Groth16 multiplication failed"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
"code": 6001,
|
|
602
|
+
"name": "Groth16AddFailed",
|
|
603
|
+
"msg": "Groth16 addition failed"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"code": 6002,
|
|
607
|
+
"name": "Groth16PairingFailed",
|
|
608
|
+
"msg": "Groth16 pairing failed"
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
"code": 6003,
|
|
612
|
+
"name": "InvalidPublicInputs",
|
|
613
|
+
"msg": "Invalid public inputs"
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
"code": 6004,
|
|
617
|
+
"name": "ProofVerificationFailed",
|
|
618
|
+
"msg": "Proof verification failed"
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"code": 6005,
|
|
622
|
+
"name": "PoolPaused",
|
|
623
|
+
"msg": "Pool is paused"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"code": 6006,
|
|
627
|
+
"name": "InvalidMint",
|
|
628
|
+
"msg": "Invalid mint"
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"code": 6007,
|
|
632
|
+
"name": "Unauthorized",
|
|
633
|
+
"msg": "Unauthorized"
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
"code": 6008,
|
|
637
|
+
"name": "TreeFull",
|
|
638
|
+
"msg": "Merkle tree is full"
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
"code": 6009,
|
|
642
|
+
"name": "InvalidRoot",
|
|
643
|
+
"msg": "Invalid merkle root"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"code": 6010,
|
|
647
|
+
"name": "NullifierAlreadySpent",
|
|
648
|
+
"msg": "Nullifier already spent"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
"code": 6011,
|
|
652
|
+
"name": "NullifierAlreadyUsed",
|
|
653
|
+
"msg": "Nullifier already used - double spend attempt"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
"code": 6012,
|
|
657
|
+
"name": "MissingNullifierAccount",
|
|
658
|
+
"msg": "Missing nullifier account"
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"code": 6013,
|
|
662
|
+
"name": "InvalidNullifierAccount",
|
|
663
|
+
"msg": "Invalid nullifier account"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"code": 6014,
|
|
667
|
+
"name": "InvalidPublicAmount",
|
|
668
|
+
"msg": "Invalid public amount"
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"code": 6015,
|
|
672
|
+
"name": "ArithmeticOverflow",
|
|
673
|
+
"msg": "Arithmetic overflow"
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"code": 6016,
|
|
677
|
+
"name": "InvalidRecipient",
|
|
678
|
+
"msg": "Invalid recipient"
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
"code": 6017,
|
|
682
|
+
"name": "InvalidFee",
|
|
683
|
+
"msg": "Invalid fee"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
"code": 6018,
|
|
687
|
+
"name": "TreeNotFull",
|
|
688
|
+
"msg": "Tree is not full yet"
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"code": 6019,
|
|
692
|
+
"name": "InvalidTreeIndex",
|
|
693
|
+
"msg": "Invalid tree index"
|
|
694
|
+
}
|
|
695
|
+
],
|
|
696
|
+
"types": [
|
|
697
|
+
{
|
|
698
|
+
"name": "MerkleTreeAccount",
|
|
699
|
+
"docs": [
|
|
700
|
+
"Incremental Merkle Tree for UTXO commitments",
|
|
701
|
+
"Uses a sparse representation - only stores filled subtrees"
|
|
702
|
+
],
|
|
703
|
+
"type": {
|
|
704
|
+
"kind": "struct",
|
|
705
|
+
"fields": [
|
|
706
|
+
{
|
|
707
|
+
"name": "pool",
|
|
708
|
+
"docs": [
|
|
709
|
+
"The privacy pool this tree belongs to"
|
|
710
|
+
],
|
|
711
|
+
"type": "pubkey"
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
"name": "tree_index",
|
|
715
|
+
"docs": [
|
|
716
|
+
"Tree index (which tree in the sequence)"
|
|
717
|
+
],
|
|
718
|
+
"type": "u32"
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
"name": "next_index",
|
|
722
|
+
"docs": [
|
|
723
|
+
"Current number of leaves inserted"
|
|
724
|
+
],
|
|
725
|
+
"type": "u32"
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
"name": "root",
|
|
729
|
+
"docs": [
|
|
730
|
+
"Current root of the tree"
|
|
731
|
+
],
|
|
732
|
+
"type": {
|
|
733
|
+
"array": [
|
|
734
|
+
"u8",
|
|
735
|
+
32
|
|
736
|
+
]
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
"name": "filled_subtrees",
|
|
741
|
+
"docs": [
|
|
742
|
+
"Filled subtrees at each level (for incremental insertion)",
|
|
743
|
+
"filled_subtrees[i] = the rightmost filled node at level i"
|
|
744
|
+
],
|
|
745
|
+
"type": {
|
|
746
|
+
"array": [
|
|
747
|
+
{
|
|
748
|
+
"array": [
|
|
749
|
+
"u8",
|
|
750
|
+
32
|
|
751
|
+
]
|
|
752
|
+
},
|
|
753
|
+
10
|
|
754
|
+
]
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
"name": "root_history",
|
|
759
|
+
"docs": [
|
|
760
|
+
"Historical roots for verification (ring buffer)",
|
|
761
|
+
"Allows verifying proofs against recent roots"
|
|
762
|
+
],
|
|
763
|
+
"type": {
|
|
764
|
+
"array": [
|
|
765
|
+
{
|
|
766
|
+
"array": [
|
|
767
|
+
"u8",
|
|
768
|
+
32
|
|
769
|
+
]
|
|
770
|
+
},
|
|
771
|
+
32
|
|
772
|
+
]
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
"name": "root_history_index",
|
|
777
|
+
"docs": [
|
|
778
|
+
"Current position in root history ring buffer"
|
|
779
|
+
],
|
|
780
|
+
"type": "u8"
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
"name": "bump",
|
|
784
|
+
"docs": [
|
|
785
|
+
"Bump seed for PDA"
|
|
786
|
+
],
|
|
787
|
+
"type": "u8"
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"name": "PrivacyPool",
|
|
794
|
+
"docs": [
|
|
795
|
+
"Global Privacy Pool Account",
|
|
796
|
+
"Single pool for all tokens - vaults are created per mint lazily"
|
|
797
|
+
],
|
|
798
|
+
"type": {
|
|
799
|
+
"kind": "struct",
|
|
800
|
+
"fields": [
|
|
801
|
+
{
|
|
802
|
+
"name": "authority",
|
|
803
|
+
"docs": [
|
|
804
|
+
"Authority that can update pool settings"
|
|
805
|
+
],
|
|
806
|
+
"type": "pubkey"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"name": "commitment_count",
|
|
810
|
+
"docs": [
|
|
811
|
+
"Total number of commitments across all trees"
|
|
812
|
+
],
|
|
813
|
+
"type": "u64"
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"name": "merkle_root",
|
|
817
|
+
"docs": [
|
|
818
|
+
"Current merkle root (of the active tree)"
|
|
819
|
+
],
|
|
820
|
+
"type": {
|
|
821
|
+
"array": [
|
|
822
|
+
"u8",
|
|
823
|
+
32
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
"name": "bump",
|
|
829
|
+
"docs": [
|
|
830
|
+
"Pool bump seed"
|
|
831
|
+
],
|
|
832
|
+
"type": "u8"
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
"name": "paused",
|
|
836
|
+
"docs": [
|
|
837
|
+
"Whether the pool is paused"
|
|
838
|
+
],
|
|
839
|
+
"type": "bool"
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"name": "current_tree_index",
|
|
843
|
+
"docs": [
|
|
844
|
+
"Current active tree index (0-based)"
|
|
845
|
+
],
|
|
846
|
+
"type": "u32"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
"name": "_reserved",
|
|
850
|
+
"docs": [
|
|
851
|
+
"Reserved for future use"
|
|
852
|
+
],
|
|
853
|
+
"type": "bytes"
|
|
854
|
+
}
|
|
855
|
+
]
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
"name": "PublicInputs",
|
|
860
|
+
"docs": [
|
|
861
|
+
"Public inputs for the transaction circuit"
|
|
862
|
+
],
|
|
863
|
+
"type": {
|
|
864
|
+
"kind": "struct",
|
|
865
|
+
"fields": [
|
|
866
|
+
{
|
|
867
|
+
"name": "merkle_root",
|
|
868
|
+
"type": {
|
|
869
|
+
"array": [
|
|
870
|
+
"u8",
|
|
871
|
+
32
|
|
872
|
+
]
|
|
873
|
+
}
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
"name": "nullifiers",
|
|
877
|
+
"type": {
|
|
878
|
+
"array": [
|
|
879
|
+
{
|
|
880
|
+
"array": [
|
|
881
|
+
"u8",
|
|
882
|
+
32
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
2
|
|
886
|
+
]
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
"name": "output_commitments",
|
|
891
|
+
"type": {
|
|
892
|
+
"array": [
|
|
893
|
+
{
|
|
894
|
+
"array": [
|
|
895
|
+
"u8",
|
|
896
|
+
32
|
|
897
|
+
]
|
|
898
|
+
},
|
|
899
|
+
2
|
|
900
|
+
]
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
"name": "public_deposit",
|
|
905
|
+
"type": "u64"
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
"name": "public_withdraw",
|
|
909
|
+
"type": "u64"
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
"name": "recipient",
|
|
913
|
+
"type": "pubkey"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
"name": "mint_token_address",
|
|
917
|
+
"type": "pubkey"
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
"name": "fee",
|
|
921
|
+
"type": "u64"
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
],
|
|
927
|
+
"constants": [
|
|
928
|
+
{
|
|
929
|
+
"name": "MERKLE_TREE_SEED",
|
|
930
|
+
"type": "bytes",
|
|
931
|
+
"value": "[109, 101, 114, 107, 108, 101, 95, 116, 114, 101, 101]"
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
"name": "NULLIFIER_SEED",
|
|
935
|
+
"type": "bytes",
|
|
936
|
+
"value": "[110, 117, 108, 108, 105, 102, 105, 101, 114]"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"name": "POOL_SEED",
|
|
940
|
+
"type": "bytes",
|
|
941
|
+
"value": "[104, 117, 108, 97, 95, 112, 111, 111, 108]"
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
"name": "VAULT_SEED",
|
|
945
|
+
"type": "bytes",
|
|
946
|
+
"value": "[118, 97, 117, 108, 116]"
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
};
|
|
950
|
+
var idl_default = HulaPrivacyIdl;
|
|
951
|
+
|
|
952
|
+
// src/crypto.ts
|
|
953
|
+
var import_circomlibjs = require("circomlibjs");
|
|
954
|
+
var nacl = __toESM(require("tweetnacl"));
|
|
955
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
956
|
+
|
|
957
|
+
// src/constants.ts
|
|
958
|
+
var import_web3 = require("@solana/web3.js");
|
|
959
|
+
var PROGRAM_ID = new import_web3.PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
|
|
960
|
+
var TOKEN_2022_PROGRAM_ID = new import_web3.PublicKey(
|
|
961
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
962
|
+
);
|
|
963
|
+
var POOL_SEED = Buffer.from("hula_pool");
|
|
964
|
+
var VAULT_SEED = Buffer.from("vault");
|
|
965
|
+
var MERKLE_TREE_SEED = Buffer.from("merkle_tree");
|
|
966
|
+
var NULLIFIER_SEED = Buffer.from("nullifier");
|
|
967
|
+
var NUM_INPUT_UTXOS = 2;
|
|
968
|
+
var NUM_OUTPUT_UTXOS = 2;
|
|
969
|
+
var MERKLE_TREE_DEPTH = 10;
|
|
970
|
+
var MAX_LEAVES = 1 << MERKLE_TREE_DEPTH;
|
|
971
|
+
var PROOF_SIZE = 256;
|
|
972
|
+
var DOMAIN_OWNER = 0n;
|
|
973
|
+
var DOMAIN_VIEWING = 1n;
|
|
974
|
+
var DOMAIN_NULLIFIER = 2n;
|
|
975
|
+
var DOMAIN_ENCRYPTION = 3n;
|
|
976
|
+
var FIELD_PRIME = BigInt(
|
|
977
|
+
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
|
978
|
+
);
|
|
979
|
+
function getPoolPDA() {
|
|
980
|
+
return import_web3.PublicKey.findProgramAddressSync([POOL_SEED], PROGRAM_ID);
|
|
981
|
+
}
|
|
982
|
+
function getMerkleTreePDA(treeIndex = 0) {
|
|
983
|
+
const treeIndexBuffer = Buffer.alloc(4);
|
|
984
|
+
treeIndexBuffer.writeUInt32LE(treeIndex, 0);
|
|
985
|
+
return import_web3.PublicKey.findProgramAddressSync(
|
|
986
|
+
[MERKLE_TREE_SEED, treeIndexBuffer],
|
|
987
|
+
PROGRAM_ID
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
function getVaultPDA(mint) {
|
|
991
|
+
return import_web3.PublicKey.findProgramAddressSync(
|
|
992
|
+
[VAULT_SEED, mint.toBytes()],
|
|
993
|
+
PROGRAM_ID
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
function getNullifierPDA(nullifier) {
|
|
997
|
+
return import_web3.PublicKey.findProgramAddressSync(
|
|
998
|
+
[NULLIFIER_SEED, nullifier],
|
|
999
|
+
PROGRAM_ID
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/crypto.ts
|
|
1004
|
+
var poseidonInstance = null;
|
|
1005
|
+
async function initPoseidon() {
|
|
1006
|
+
if (!poseidonInstance) {
|
|
1007
|
+
poseidonInstance = await (0, import_circomlibjs.buildPoseidon)();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function isPoseidonInitialized() {
|
|
1011
|
+
return poseidonInstance !== null;
|
|
1012
|
+
}
|
|
1013
|
+
function getPoseidon() {
|
|
1014
|
+
if (!poseidonInstance) {
|
|
1015
|
+
throw new Error("Poseidon not initialized. Call initPoseidon() first.");
|
|
1016
|
+
}
|
|
1017
|
+
return poseidonInstance;
|
|
1018
|
+
}
|
|
1019
|
+
function poseidonHash(inputs) {
|
|
1020
|
+
const poseidon = getPoseidon();
|
|
1021
|
+
const hash = poseidon(inputs.map((x) => poseidon.F.e(x)));
|
|
1022
|
+
return poseidon.F.toObject(hash);
|
|
1023
|
+
}
|
|
1024
|
+
function bytesToBigInt(bytes) {
|
|
1025
|
+
let result = 0n;
|
|
1026
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1027
|
+
result = (result << 8n) + BigInt(bytes[i]);
|
|
1028
|
+
}
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
function bigIntToBytes(value, length) {
|
|
1032
|
+
const bytes = new Uint8Array(length);
|
|
1033
|
+
let v = value;
|
|
1034
|
+
for (let i = length - 1; i >= 0; i--) {
|
|
1035
|
+
bytes[i] = Number(v & 0xffn);
|
|
1036
|
+
v >>= 8n;
|
|
1037
|
+
}
|
|
1038
|
+
return bytes;
|
|
1039
|
+
}
|
|
1040
|
+
function bigIntToBytes32(value) {
|
|
1041
|
+
return bigIntToBytes(value, 32);
|
|
1042
|
+
}
|
|
1043
|
+
function hexToBytes(hex) {
|
|
1044
|
+
const cleaned = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
1045
|
+
const bytes = new Uint8Array(cleaned.length / 2);
|
|
1046
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1047
|
+
bytes[i] = parseInt(cleaned.substr(i * 2, 2), 16);
|
|
1048
|
+
}
|
|
1049
|
+
return bytes;
|
|
1050
|
+
}
|
|
1051
|
+
function bytesToHex(bytes) {
|
|
1052
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1053
|
+
}
|
|
1054
|
+
function pubkeyToBigInt(pubkey) {
|
|
1055
|
+
return bytesToBigInt(pubkey.toBytes());
|
|
1056
|
+
}
|
|
1057
|
+
function generateSpendingKey() {
|
|
1058
|
+
const randomBytes2 = nacl.randomBytes(32);
|
|
1059
|
+
return bytesToBigInt(randomBytes2) % 2n ** 253n;
|
|
1060
|
+
}
|
|
1061
|
+
function deriveKeys(spendingKey) {
|
|
1062
|
+
const owner = poseidonHash([spendingKey, DOMAIN_OWNER]);
|
|
1063
|
+
const viewingKey = poseidonHash([spendingKey, DOMAIN_VIEWING]);
|
|
1064
|
+
const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
|
|
1065
|
+
const encryptionSeed = poseidonHash([spendingKey, DOMAIN_ENCRYPTION]);
|
|
1066
|
+
const seedBytes = bigIntToBytes(encryptionSeed, 32);
|
|
1067
|
+
const hashedSeed = (0, import_sha256.sha256)(seedBytes);
|
|
1068
|
+
const encryptionKeyPair = nacl.box.keyPair.fromSecretKey(hashedSeed);
|
|
1069
|
+
return {
|
|
1070
|
+
spendingKey,
|
|
1071
|
+
owner,
|
|
1072
|
+
viewingKey,
|
|
1073
|
+
nullifierKey,
|
|
1074
|
+
encryptionKeyPair
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function deriveSpendingKeyFromSignature(signature) {
|
|
1078
|
+
if (signature.length < 64) {
|
|
1079
|
+
throw new Error("Signature must be at least 64 bytes");
|
|
1080
|
+
}
|
|
1081
|
+
const hash = (0, import_sha256.sha256)(signature);
|
|
1082
|
+
return bytesToBigInt(hash) % FIELD_PRIME;
|
|
1083
|
+
}
|
|
1084
|
+
function encryptNote(noteData, recipientEncryptionPubKey) {
|
|
1085
|
+
const mintPrefix = noteData.mintTokenAddress.toString(16).padStart(64, "0").slice(0, 16);
|
|
1086
|
+
const payload = {
|
|
1087
|
+
v: noteData.value.toString(),
|
|
1088
|
+
// value
|
|
1089
|
+
m: mintPrefix,
|
|
1090
|
+
// mint (first 8 bytes as hex)
|
|
1091
|
+
s: noteData.secret.toString()
|
|
1092
|
+
// secret
|
|
1093
|
+
// leafIndex omitted - recipient queries relayer with computed commitment
|
|
1094
|
+
};
|
|
1095
|
+
const message = new TextEncoder().encode(JSON.stringify(payload));
|
|
1096
|
+
const nonce = nacl.randomBytes(24);
|
|
1097
|
+
const ephemeralKeyPair = nacl.box.keyPair();
|
|
1098
|
+
const ciphertext = nacl.box(
|
|
1099
|
+
message,
|
|
1100
|
+
nonce,
|
|
1101
|
+
recipientEncryptionPubKey,
|
|
1102
|
+
ephemeralKeyPair.secretKey
|
|
1103
|
+
);
|
|
1104
|
+
return {
|
|
1105
|
+
ephemeralPubKey: ephemeralKeyPair.publicKey,
|
|
1106
|
+
ciphertext,
|
|
1107
|
+
nonce
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function decryptNote(encryptedNote, encryptionSecretKey) {
|
|
1111
|
+
try {
|
|
1112
|
+
const decrypted = nacl.box.open(
|
|
1113
|
+
encryptedNote.ciphertext,
|
|
1114
|
+
encryptedNote.nonce,
|
|
1115
|
+
encryptedNote.ephemeralPubKey,
|
|
1116
|
+
encryptionSecretKey
|
|
1117
|
+
);
|
|
1118
|
+
if (!decrypted) return null;
|
|
1119
|
+
const noteData = JSON.parse(new TextDecoder().decode(decrypted));
|
|
1120
|
+
if (noteData.v !== void 0) {
|
|
1121
|
+
return {
|
|
1122
|
+
value: BigInt(noteData.v),
|
|
1123
|
+
mintPrefix: noteData.m,
|
|
1124
|
+
// First 8 bytes as hex string
|
|
1125
|
+
secret: BigInt(noteData.s)
|
|
1126
|
+
};
|
|
1127
|
+
} else {
|
|
1128
|
+
return {
|
|
1129
|
+
value: BigInt(noteData.value),
|
|
1130
|
+
mintPrefix: BigInt(noteData.mintTokenAddress).toString(16).padStart(64, "0").slice(0, 16),
|
|
1131
|
+
secret: BigInt(noteData.secret)
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function serializeEncryptedNote(note) {
|
|
1139
|
+
const totalLength = 32 + 24 + 2 + note.ciphertext.length;
|
|
1140
|
+
const buffer = new Uint8Array(totalLength);
|
|
1141
|
+
buffer.set(note.ephemeralPubKey, 0);
|
|
1142
|
+
buffer.set(note.nonce, 32);
|
|
1143
|
+
buffer[56] = note.ciphertext.length >> 8 & 255;
|
|
1144
|
+
buffer[57] = note.ciphertext.length & 255;
|
|
1145
|
+
buffer.set(note.ciphertext, 58);
|
|
1146
|
+
return buffer;
|
|
1147
|
+
}
|
|
1148
|
+
function deserializeEncryptedNote(data) {
|
|
1149
|
+
const ephemeralPubKey = data.slice(0, 32);
|
|
1150
|
+
const nonce = data.slice(32, 56);
|
|
1151
|
+
const ciphertextLength = data[56] << 8 | data[57];
|
|
1152
|
+
const ciphertext = data.slice(58, 58 + ciphertextLength);
|
|
1153
|
+
return { ephemeralPubKey, nonce, ciphertext };
|
|
1154
|
+
}
|
|
1155
|
+
function formatCommitment(value, length = 16) {
|
|
1156
|
+
if (value instanceof Uint8Array) {
|
|
1157
|
+
return `0x${bytesToHex(value).slice(0, length)}...`;
|
|
1158
|
+
}
|
|
1159
|
+
return `0x${value.toString(16).padStart(64, "0").slice(0, length)}...`;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/api.ts
|
|
1163
|
+
var DEFAULT_API_URL = "http://localhost:3001";
|
|
1164
|
+
var RelayerClient = class {
|
|
1165
|
+
baseUrl;
|
|
1166
|
+
constructor(baseUrl = DEFAULT_API_URL) {
|
|
1167
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
1168
|
+
}
|
|
1169
|
+
async fetch(path2, init) {
|
|
1170
|
+
const response = await fetch(`${this.baseUrl}${path2}`, {
|
|
1171
|
+
...init,
|
|
1172
|
+
headers: {
|
|
1173
|
+
"Content-Type": "application/json",
|
|
1174
|
+
...init?.headers
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
if (!response.ok) {
|
|
1178
|
+
const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
1179
|
+
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
1180
|
+
}
|
|
1181
|
+
return response.json();
|
|
1182
|
+
}
|
|
1183
|
+
// ============================================================================
|
|
1184
|
+
// Health & Stats
|
|
1185
|
+
// ============================================================================
|
|
1186
|
+
/**
|
|
1187
|
+
* Health check
|
|
1188
|
+
*/
|
|
1189
|
+
async health() {
|
|
1190
|
+
return this.fetch("/health");
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Get protocol stats
|
|
1194
|
+
*/
|
|
1195
|
+
async getStats() {
|
|
1196
|
+
return this.fetch("/api/stats");
|
|
1197
|
+
}
|
|
1198
|
+
// ============================================================================
|
|
1199
|
+
// Pool
|
|
1200
|
+
// ============================================================================
|
|
1201
|
+
/**
|
|
1202
|
+
* Get pool data
|
|
1203
|
+
*/
|
|
1204
|
+
async getPool() {
|
|
1205
|
+
return this.fetch("/api/pool");
|
|
1206
|
+
}
|
|
1207
|
+
// ============================================================================
|
|
1208
|
+
// Trees
|
|
1209
|
+
// ============================================================================
|
|
1210
|
+
/**
|
|
1211
|
+
* Get all merkle trees
|
|
1212
|
+
*/
|
|
1213
|
+
async getTrees() {
|
|
1214
|
+
return this.fetch("/api/trees");
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Get tree by index
|
|
1218
|
+
*/
|
|
1219
|
+
async getTree(treeIndex) {
|
|
1220
|
+
return this.fetch(`/api/trees/${treeIndex}`);
|
|
1221
|
+
}
|
|
1222
|
+
// ============================================================================
|
|
1223
|
+
// Leaves / Commitments
|
|
1224
|
+
// ============================================================================
|
|
1225
|
+
/**
|
|
1226
|
+
* Get leaves with cursor pagination
|
|
1227
|
+
*/
|
|
1228
|
+
async getLeaves(cursor, limit, treeIndex) {
|
|
1229
|
+
const params = new URLSearchParams();
|
|
1230
|
+
if (cursor) params.set("cursor", cursor);
|
|
1231
|
+
if (limit) params.set("limit", String(limit));
|
|
1232
|
+
if (treeIndex !== void 0) params.set("treeIndex", String(treeIndex));
|
|
1233
|
+
return this.fetch(`/api/leaves?${params}`);
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Get leaves after a specific index (for syncing)
|
|
1237
|
+
*/
|
|
1238
|
+
async getLeavesAfter(treeIndex, afterLeafIndex, limit) {
|
|
1239
|
+
const params = new URLSearchParams();
|
|
1240
|
+
params.set("treeIndex", String(treeIndex));
|
|
1241
|
+
params.set("afterLeafIndex", String(afterLeafIndex));
|
|
1242
|
+
if (limit) params.set("limit", String(limit));
|
|
1243
|
+
return this.fetch(`/api/leaves/after?${params}`);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Get all leaves for a tree (auto-paginated)
|
|
1247
|
+
*/
|
|
1248
|
+
async getAllLeavesForTree(treeIndex) {
|
|
1249
|
+
const allLeaves = [];
|
|
1250
|
+
let cursor;
|
|
1251
|
+
while (true) {
|
|
1252
|
+
const response = await this.getLeaves(cursor, 100, treeIndex);
|
|
1253
|
+
allLeaves.push(...response.items);
|
|
1254
|
+
if (!response.hasMore || !response.nextCursor) break;
|
|
1255
|
+
cursor = response.nextCursor;
|
|
1256
|
+
}
|
|
1257
|
+
return allLeaves;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Get all commitment values for a tree as bigints
|
|
1261
|
+
*/
|
|
1262
|
+
async getCommitmentsForTree(treeIndex) {
|
|
1263
|
+
const leaves = await this.getAllLeavesForTree(treeIndex);
|
|
1264
|
+
leaves.sort((a, b) => a.leafIndex - b.leafIndex);
|
|
1265
|
+
return leaves.map((l) => {
|
|
1266
|
+
const commitment = l.commitment.startsWith("0x") ? l.commitment : `0x${l.commitment}`;
|
|
1267
|
+
return BigInt(commitment);
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
// ============================================================================
|
|
1271
|
+
// Transactions
|
|
1272
|
+
// ============================================================================
|
|
1273
|
+
/**
|
|
1274
|
+
* Get transactions with cursor pagination
|
|
1275
|
+
*/
|
|
1276
|
+
async getTransactions(cursor, limit, mint) {
|
|
1277
|
+
const params = new URLSearchParams();
|
|
1278
|
+
if (cursor) params.set("cursor", cursor);
|
|
1279
|
+
if (limit) params.set("limit", String(limit));
|
|
1280
|
+
if (mint) params.set("mint", mint);
|
|
1281
|
+
return this.fetch(`/api/transactions?${params}`);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Get transaction by signature
|
|
1285
|
+
*/
|
|
1286
|
+
async getTransaction(signature) {
|
|
1287
|
+
return this.fetch(`/api/transactions/${signature}`);
|
|
1288
|
+
}
|
|
1289
|
+
// ============================================================================
|
|
1290
|
+
// Nullifiers
|
|
1291
|
+
// ============================================================================
|
|
1292
|
+
/**
|
|
1293
|
+
* Check if nullifier is spent
|
|
1294
|
+
*/
|
|
1295
|
+
async checkNullifier(nullifier) {
|
|
1296
|
+
return this.fetch(`/api/nullifiers/check?nullifier=${encodeURIComponent(nullifier)}`);
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Check multiple nullifiers at once
|
|
1300
|
+
*/
|
|
1301
|
+
async checkNullifiersBatch(nullifiers) {
|
|
1302
|
+
return this.fetch("/api/nullifiers/check-batch", {
|
|
1303
|
+
method: "POST",
|
|
1304
|
+
body: JSON.stringify({ nullifiers })
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
// ============================================================================
|
|
1308
|
+
// Encrypted Notes
|
|
1309
|
+
// ============================================================================
|
|
1310
|
+
/**
|
|
1311
|
+
* Get encrypted notes with cursor pagination
|
|
1312
|
+
*/
|
|
1313
|
+
async getNotes(cursor, limit, afterSlot) {
|
|
1314
|
+
const params = new URLSearchParams();
|
|
1315
|
+
if (cursor) params.set("cursor", cursor);
|
|
1316
|
+
if (limit) params.set("limit", String(limit));
|
|
1317
|
+
if (afterSlot) params.set("afterSlot", afterSlot);
|
|
1318
|
+
return this.fetch(`/api/notes?${params}`);
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Get all notes (auto-paginated)
|
|
1322
|
+
*/
|
|
1323
|
+
async getAllNotes(afterSlot) {
|
|
1324
|
+
const allNotes = [];
|
|
1325
|
+
let cursor;
|
|
1326
|
+
while (true) {
|
|
1327
|
+
const response = await this.getNotes(cursor, 100, afterSlot);
|
|
1328
|
+
allNotes.push(...response.items);
|
|
1329
|
+
if (!response.hasMore || !response.nextCursor) break;
|
|
1330
|
+
cursor = response.nextCursor;
|
|
1331
|
+
}
|
|
1332
|
+
return allNotes;
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
var defaultClient = null;
|
|
1336
|
+
function getRelayerClient(url) {
|
|
1337
|
+
if (url) {
|
|
1338
|
+
return new RelayerClient(url);
|
|
1339
|
+
}
|
|
1340
|
+
if (!defaultClient) {
|
|
1341
|
+
defaultClient = new RelayerClient();
|
|
1342
|
+
}
|
|
1343
|
+
return defaultClient;
|
|
1344
|
+
}
|
|
1345
|
+
function setDefaultRelayerUrl(url) {
|
|
1346
|
+
defaultClient = new RelayerClient(url);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/utxo.ts
|
|
1350
|
+
var import_web32 = require("@solana/web3.js");
|
|
1351
|
+
function computeCommitment(value, mintTokenAddress, owner, secret) {
|
|
1352
|
+
return poseidonHash([value, mintTokenAddress, owner, secret]);
|
|
1353
|
+
}
|
|
1354
|
+
function computeNullifier(spendingKey, commitment, leafIndex) {
|
|
1355
|
+
const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
|
|
1356
|
+
return poseidonHash([nullifierKey, commitment, BigInt(leafIndex)]);
|
|
1357
|
+
}
|
|
1358
|
+
function computeNullifierFromKeys(keys, commitment, leafIndex) {
|
|
1359
|
+
return poseidonHash([keys.nullifierKey, commitment, BigInt(leafIndex)]);
|
|
1360
|
+
}
|
|
1361
|
+
function createUTXO(value, mintTokenAddress, owner, leafIndex, treeIndex = 0) {
|
|
1362
|
+
const secret = generateSpendingKey();
|
|
1363
|
+
const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
|
|
1364
|
+
return {
|
|
1365
|
+
value,
|
|
1366
|
+
mintTokenAddress,
|
|
1367
|
+
owner,
|
|
1368
|
+
secret,
|
|
1369
|
+
commitment,
|
|
1370
|
+
leafIndex,
|
|
1371
|
+
treeIndex
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function createDummyUTXO() {
|
|
1375
|
+
return {
|
|
1376
|
+
value: 0n,
|
|
1377
|
+
mintTokenAddress: 0n,
|
|
1378
|
+
owner: 0n,
|
|
1379
|
+
secret: 0n,
|
|
1380
|
+
commitment: 0n,
|
|
1381
|
+
leafIndex: 0,
|
|
1382
|
+
treeIndex: 0
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function serializeUTXO(utxo, mint, createdTx, spent = false, spentTx) {
|
|
1386
|
+
return {
|
|
1387
|
+
value: utxo.value.toString(),
|
|
1388
|
+
mintTokenAddress: utxo.mintTokenAddress.toString(),
|
|
1389
|
+
owner: utxo.owner.toString(),
|
|
1390
|
+
secret: utxo.secret.toString(),
|
|
1391
|
+
commitment: utxo.commitment.toString(),
|
|
1392
|
+
leafIndex: utxo.leafIndex,
|
|
1393
|
+
treeIndex: utxo.treeIndex,
|
|
1394
|
+
mint,
|
|
1395
|
+
spent,
|
|
1396
|
+
spentTx,
|
|
1397
|
+
createdTx,
|
|
1398
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function deserializeUTXO(data) {
|
|
1402
|
+
return {
|
|
1403
|
+
value: BigInt(data.value),
|
|
1404
|
+
mintTokenAddress: BigInt(data.mintTokenAddress),
|
|
1405
|
+
owner: BigInt(data.owner),
|
|
1406
|
+
secret: BigInt(data.secret),
|
|
1407
|
+
commitment: BigInt(data.commitment),
|
|
1408
|
+
leafIndex: data.leafIndex,
|
|
1409
|
+
treeIndex: data.treeIndex
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function scanNotesForUTXOs(notes, walletKeys, commitments) {
|
|
1413
|
+
const foundUTXOs = [];
|
|
1414
|
+
for (const note of notes) {
|
|
1415
|
+
try {
|
|
1416
|
+
const noteBytes = hexToBytes(note.encryptedData);
|
|
1417
|
+
const encryptedNote = deserializeEncryptedNote(noteBytes);
|
|
1418
|
+
const decrypted = decryptNote(encryptedNote, walletKeys.encryptionKeyPair.secretKey);
|
|
1419
|
+
if (!decrypted) continue;
|
|
1420
|
+
const mintAddress = note.mint;
|
|
1421
|
+
const mintTokenAddress = pubkeyToBigInt(new import_web32.PublicKey(mintAddress));
|
|
1422
|
+
const commitment = computeCommitment(
|
|
1423
|
+
decrypted.value,
|
|
1424
|
+
mintTokenAddress,
|
|
1425
|
+
walletKeys.owner,
|
|
1426
|
+
decrypted.secret
|
|
1427
|
+
);
|
|
1428
|
+
let foundLeafIndex;
|
|
1429
|
+
const treeCommitments = commitments.get(note.treeIndex);
|
|
1430
|
+
if (treeCommitments) {
|
|
1431
|
+
for (const [leafIndex, onChainCommitment] of treeCommitments.entries()) {
|
|
1432
|
+
if (onChainCommitment === commitment) {
|
|
1433
|
+
foundLeafIndex = leafIndex;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (foundLeafIndex === void 0) {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
foundUTXOs.push({
|
|
1442
|
+
value: decrypted.value.toString(),
|
|
1443
|
+
mintTokenAddress: mintTokenAddress.toString(),
|
|
1444
|
+
owner: walletKeys.owner.toString(),
|
|
1445
|
+
secret: decrypted.secret.toString(),
|
|
1446
|
+
commitment: commitment.toString(),
|
|
1447
|
+
leafIndex: foundLeafIndex,
|
|
1448
|
+
treeIndex: note.treeIndex,
|
|
1449
|
+
mint: note.mint,
|
|
1450
|
+
spent: false,
|
|
1451
|
+
createdTx: note.txSignature,
|
|
1452
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1453
|
+
});
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return foundUTXOs;
|
|
1458
|
+
}
|
|
1459
|
+
async function syncUTXOs(walletKeys, existingUTXOs, relayerUrl, afterSlot) {
|
|
1460
|
+
const client = getRelayerClient(relayerUrl);
|
|
1461
|
+
const notes = await client.getAllNotes(afterSlot);
|
|
1462
|
+
const pool = await client.getPool();
|
|
1463
|
+
const commitments = /* @__PURE__ */ new Map();
|
|
1464
|
+
for (let treeIndex = 0; treeIndex <= pool.currentTreeIndex; treeIndex++) {
|
|
1465
|
+
const leaves = await client.getCommitmentsForTree(treeIndex);
|
|
1466
|
+
const treeMap = /* @__PURE__ */ new Map();
|
|
1467
|
+
leaves.forEach((commitment, index) => {
|
|
1468
|
+
treeMap.set(index, commitment);
|
|
1469
|
+
});
|
|
1470
|
+
commitments.set(treeIndex, treeMap);
|
|
1471
|
+
}
|
|
1472
|
+
const newUTXOs = scanNotesForUTXOs(
|
|
1473
|
+
notes.map((n) => ({
|
|
1474
|
+
encryptedData: n.encryptedData,
|
|
1475
|
+
treeIndex: n.treeIndex,
|
|
1476
|
+
mint: n.mint,
|
|
1477
|
+
txSignature: n.txSignature
|
|
1478
|
+
})),
|
|
1479
|
+
walletKeys,
|
|
1480
|
+
commitments
|
|
1481
|
+
);
|
|
1482
|
+
const allUTXOs = [...newUTXOs, ...existingUTXOs];
|
|
1483
|
+
const unspentUTXOs = allUTXOs.filter((u) => !u.spent);
|
|
1484
|
+
const nullifiersToCheck = [];
|
|
1485
|
+
const utxoByNullifier = /* @__PURE__ */ new Map();
|
|
1486
|
+
for (const utxo of unspentUTXOs) {
|
|
1487
|
+
const commitment = BigInt(utxo.commitment);
|
|
1488
|
+
const nullifier = computeNullifier(
|
|
1489
|
+
walletKeys.spendingKey,
|
|
1490
|
+
commitment,
|
|
1491
|
+
utxo.leafIndex
|
|
1492
|
+
);
|
|
1493
|
+
const nullifierStr = nullifier.toString(16).padStart(64, "0");
|
|
1494
|
+
nullifiersToCheck.push(nullifierStr);
|
|
1495
|
+
utxoByNullifier.set(nullifierStr, utxo.commitment);
|
|
1496
|
+
}
|
|
1497
|
+
const spentUTXOs = [];
|
|
1498
|
+
if (nullifiersToCheck.length > 0) {
|
|
1499
|
+
const batchSize = 50;
|
|
1500
|
+
for (let i = 0; i < nullifiersToCheck.length; i += batchSize) {
|
|
1501
|
+
const batch = nullifiersToCheck.slice(i, i + batchSize);
|
|
1502
|
+
const result = await client.checkNullifiersBatch(batch);
|
|
1503
|
+
for (const r of result.results) {
|
|
1504
|
+
if (r.spent) {
|
|
1505
|
+
const utxoId = utxoByNullifier.get(r.nullifier);
|
|
1506
|
+
if (utxoId) {
|
|
1507
|
+
spentUTXOs.push(utxoId);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return { newUTXOs, spentUTXOs };
|
|
1514
|
+
}
|
|
1515
|
+
function selectUTXOs(utxos, targetAmount, mint) {
|
|
1516
|
+
const filtered = mint !== void 0 ? utxos.filter((u) => u.mintTokenAddress === mint) : utxos;
|
|
1517
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
1518
|
+
if (a.value > b.value) return -1;
|
|
1519
|
+
if (a.value < b.value) return 1;
|
|
1520
|
+
return 0;
|
|
1521
|
+
});
|
|
1522
|
+
const selected = [];
|
|
1523
|
+
let total = 0n;
|
|
1524
|
+
for (const utxo of sorted) {
|
|
1525
|
+
if (total >= targetAmount) break;
|
|
1526
|
+
selected.push(utxo);
|
|
1527
|
+
total += utxo.value;
|
|
1528
|
+
}
|
|
1529
|
+
if (total < targetAmount) {
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
`Insufficient balance. Need ${targetAmount}, have ${total}`
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
return selected;
|
|
1535
|
+
}
|
|
1536
|
+
function calculateBalance(utxos, mint) {
|
|
1537
|
+
const filtered = mint !== void 0 ? utxos.filter((u) => u.mintTokenAddress === mint) : utxos;
|
|
1538
|
+
return filtered.reduce((sum, u) => sum + u.value, 0n);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/transaction.ts
|
|
1542
|
+
var import_web33 = require("@solana/web3.js");
|
|
1543
|
+
var import_spl_token = require("@solana/spl-token");
|
|
1544
|
+
var import_anchor = require("@coral-xyz/anchor");
|
|
1545
|
+
|
|
1546
|
+
// src/merkle.ts
|
|
1547
|
+
var ZEROS = [];
|
|
1548
|
+
function computeZeros() {
|
|
1549
|
+
if (!isPoseidonInitialized()) {
|
|
1550
|
+
throw new Error("Poseidon not initialized. Call initPoseidon() first.");
|
|
1551
|
+
}
|
|
1552
|
+
if (ZEROS.length > 0) {
|
|
1553
|
+
return ZEROS;
|
|
1554
|
+
}
|
|
1555
|
+
const zeros = [0n];
|
|
1556
|
+
const poseidon = getPoseidon();
|
|
1557
|
+
for (let i = 0; i < MERKLE_TREE_DEPTH; i++) {
|
|
1558
|
+
const hash = poseidon([zeros[i], zeros[i]]);
|
|
1559
|
+
zeros.push(poseidon.F.toObject(hash));
|
|
1560
|
+
}
|
|
1561
|
+
ZEROS = zeros;
|
|
1562
|
+
return zeros;
|
|
1563
|
+
}
|
|
1564
|
+
function getZeroAtLevel(level) {
|
|
1565
|
+
if (ZEROS.length === 0) {
|
|
1566
|
+
computeZeros();
|
|
1567
|
+
}
|
|
1568
|
+
return ZEROS[level];
|
|
1569
|
+
}
|
|
1570
|
+
function getEmptyTreeRoot() {
|
|
1571
|
+
if (ZEROS.length === 0) {
|
|
1572
|
+
computeZeros();
|
|
1573
|
+
}
|
|
1574
|
+
return ZEROS[MERKLE_TREE_DEPTH];
|
|
1575
|
+
}
|
|
1576
|
+
function computeMerklePathFromLeaves(leafIndex, leaves) {
|
|
1577
|
+
if (!isPoseidonInitialized()) {
|
|
1578
|
+
throw new Error("Poseidon not initialized. Call initPoseidon() first.");
|
|
1579
|
+
}
|
|
1580
|
+
const zeros = computeZeros();
|
|
1581
|
+
const pathElements = [];
|
|
1582
|
+
const pathIndices = [];
|
|
1583
|
+
const poseidon = getPoseidon();
|
|
1584
|
+
let currentLevel = [...leaves];
|
|
1585
|
+
const treeSize = 1 << MERKLE_TREE_DEPTH;
|
|
1586
|
+
while (currentLevel.length < treeSize) {
|
|
1587
|
+
currentLevel.push(zeros[0]);
|
|
1588
|
+
}
|
|
1589
|
+
let targetIndex = leafIndex;
|
|
1590
|
+
for (let level = 0; level < MERKLE_TREE_DEPTH; level++) {
|
|
1591
|
+
const isLeft = (targetIndex & 1) === 0;
|
|
1592
|
+
pathIndices.push(isLeft ? 0 : 1);
|
|
1593
|
+
const siblingIndex = isLeft ? targetIndex + 1 : targetIndex - 1;
|
|
1594
|
+
pathElements.push(currentLevel[siblingIndex]);
|
|
1595
|
+
const nextLevel = [];
|
|
1596
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
1597
|
+
const left = currentLevel[i];
|
|
1598
|
+
const right = currentLevel[i + 1] ?? zeros[level];
|
|
1599
|
+
const hash = poseidon([left, right]);
|
|
1600
|
+
nextLevel.push(poseidon.F.toObject(hash));
|
|
1601
|
+
}
|
|
1602
|
+
currentLevel = nextLevel;
|
|
1603
|
+
targetIndex = Math.floor(targetIndex / 2);
|
|
1604
|
+
}
|
|
1605
|
+
return { pathElements, pathIndices, root: currentLevel[0] };
|
|
1606
|
+
}
|
|
1607
|
+
function computeMerkleRoot(leaves) {
|
|
1608
|
+
if (leaves.length === 0) {
|
|
1609
|
+
return getEmptyTreeRoot();
|
|
1610
|
+
}
|
|
1611
|
+
const { root } = computeMerklePathFromLeaves(0, leaves);
|
|
1612
|
+
return root;
|
|
1613
|
+
}
|
|
1614
|
+
function verifyMerklePath(leafValue, path2) {
|
|
1615
|
+
if (!isPoseidonInitialized()) {
|
|
1616
|
+
throw new Error("Poseidon not initialized. Call initPoseidon() first.");
|
|
1617
|
+
}
|
|
1618
|
+
let current = leafValue;
|
|
1619
|
+
const poseidon = getPoseidon();
|
|
1620
|
+
for (let i = 0; i < path2.pathElements.length; i++) {
|
|
1621
|
+
const sibling = path2.pathElements[i];
|
|
1622
|
+
const isLeft = path2.pathIndices[i] === 0;
|
|
1623
|
+
const left = isLeft ? current : sibling;
|
|
1624
|
+
const right = isLeft ? sibling : current;
|
|
1625
|
+
const hash = poseidon([left, right]);
|
|
1626
|
+
current = poseidon.F.toObject(hash);
|
|
1627
|
+
}
|
|
1628
|
+
return current === path2.root;
|
|
1629
|
+
}
|
|
1630
|
+
async function fetchMerklePath(treeIndex, leafIndex, relayerUrl) {
|
|
1631
|
+
const client = getRelayerClient(relayerUrl);
|
|
1632
|
+
const leaves = await client.getCommitmentsForTree(treeIndex);
|
|
1633
|
+
if (leafIndex >= leaves.length) {
|
|
1634
|
+
throw new Error(`Leaf index ${leafIndex} not found in tree ${treeIndex} (has ${leaves.length} leaves)`);
|
|
1635
|
+
}
|
|
1636
|
+
return computeMerklePathFromLeaves(leafIndex, leaves);
|
|
1637
|
+
}
|
|
1638
|
+
async function fetchMerkleRoot(treeIndex, relayerUrl) {
|
|
1639
|
+
const client = getRelayerClient(relayerUrl);
|
|
1640
|
+
const tree = await client.getTree(treeIndex);
|
|
1641
|
+
const rootStr = tree.root.startsWith("0x") ? tree.root : `0x${tree.root}`;
|
|
1642
|
+
return BigInt(rootStr);
|
|
1643
|
+
}
|
|
1644
|
+
async function getNextLeafIndex(treeIndex, relayerUrl) {
|
|
1645
|
+
const client = getRelayerClient(relayerUrl);
|
|
1646
|
+
const tree = await client.getTree(treeIndex);
|
|
1647
|
+
return tree.nextIndex;
|
|
1648
|
+
}
|
|
1649
|
+
async function getCurrentTreeIndex(relayerUrl) {
|
|
1650
|
+
const client = getRelayerClient(relayerUrl);
|
|
1651
|
+
const pool = await client.getPool();
|
|
1652
|
+
return pool.currentTreeIndex;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/proof.ts
|
|
1656
|
+
var import_fs = require("fs");
|
|
1657
|
+
var import_child_process = require("child_process");
|
|
1658
|
+
var import_path = __toESM(require("path"));
|
|
1659
|
+
var circuitWasmPath = null;
|
|
1660
|
+
var circuitZkeyPath = null;
|
|
1661
|
+
function setCircuitPaths(wasmPath, zkeyPath) {
|
|
1662
|
+
circuitWasmPath = wasmPath;
|
|
1663
|
+
circuitZkeyPath = zkeyPath;
|
|
1664
|
+
}
|
|
1665
|
+
function getCircuitPaths() {
|
|
1666
|
+
if (circuitWasmPath && circuitZkeyPath) {
|
|
1667
|
+
return { wasmPath: circuitWasmPath, zkeyPath: circuitZkeyPath };
|
|
1668
|
+
}
|
|
1669
|
+
const possibleBasePaths = [
|
|
1670
|
+
import_path.default.join(process.cwd(), "circuits", "build"),
|
|
1671
|
+
import_path.default.join(process.cwd(), "..", "circuits", "build"),
|
|
1672
|
+
import_path.default.join(process.cwd(), "assets")
|
|
1673
|
+
];
|
|
1674
|
+
for (const basePath of possibleBasePaths) {
|
|
1675
|
+
const wasmPath = import_path.default.join(basePath, "transaction_js", "transaction.wasm");
|
|
1676
|
+
const zkeyPath = import_path.default.join(basePath, "keys", "transaction_final.zkey");
|
|
1677
|
+
if ((0, import_fs.existsSync)(wasmPath) && (0, import_fs.existsSync)(zkeyPath)) {
|
|
1678
|
+
return { wasmPath, zkeyPath };
|
|
1679
|
+
}
|
|
1680
|
+
const altWasmPath = import_path.default.join(basePath, "transaction.wasm");
|
|
1681
|
+
const altZkeyPath = import_path.default.join(basePath, "transaction_final.zkey");
|
|
1682
|
+
if ((0, import_fs.existsSync)(altWasmPath) && (0, import_fs.existsSync)(altZkeyPath)) {
|
|
1683
|
+
return { wasmPath: altWasmPath, zkeyPath: altZkeyPath };
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
"Circuit files not found. Either:\n1. Run 'make' in circuits/ directory to build them, or\n2. Call setCircuitPaths() with the correct paths"
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
function verifyCircuitFiles() {
|
|
1691
|
+
const { wasmPath, zkeyPath } = getCircuitPaths();
|
|
1692
|
+
if (!(0, import_fs.existsSync)(wasmPath)) {
|
|
1693
|
+
throw new Error(`Circuit WASM not found: ${wasmPath}`);
|
|
1694
|
+
}
|
|
1695
|
+
if (!(0, import_fs.existsSync)(zkeyPath)) {
|
|
1696
|
+
throw new Error(`Circuit zkey not found: ${zkeyPath}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function toBigEndianBytes(decimalStr) {
|
|
1700
|
+
let hex = BigInt(decimalStr).toString(16);
|
|
1701
|
+
hex = hex.padStart(64, "0");
|
|
1702
|
+
const bytes = new Uint8Array(32);
|
|
1703
|
+
for (let i = 0; i < 32; i++) {
|
|
1704
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1705
|
+
}
|
|
1706
|
+
return bytes;
|
|
1707
|
+
}
|
|
1708
|
+
function parseProof(proofJson) {
|
|
1709
|
+
const proof = new Uint8Array(PROOF_SIZE);
|
|
1710
|
+
const pi_a_x = toBigEndianBytes(proofJson.pi_a[0]);
|
|
1711
|
+
const pi_a_y = toBigEndianBytes(proofJson.pi_a[1]);
|
|
1712
|
+
proof.set(pi_a_x, 0);
|
|
1713
|
+
proof.set(pi_a_y, 32);
|
|
1714
|
+
const pi_b_x0 = toBigEndianBytes(proofJson.pi_b[0][1]);
|
|
1715
|
+
const pi_b_x1 = toBigEndianBytes(proofJson.pi_b[0][0]);
|
|
1716
|
+
const pi_b_y0 = toBigEndianBytes(proofJson.pi_b[1][1]);
|
|
1717
|
+
const pi_b_y1 = toBigEndianBytes(proofJson.pi_b[1][0]);
|
|
1718
|
+
proof.set(pi_b_x0, 64);
|
|
1719
|
+
proof.set(pi_b_x1, 96);
|
|
1720
|
+
proof.set(pi_b_y0, 128);
|
|
1721
|
+
proof.set(pi_b_y1, 160);
|
|
1722
|
+
const pi_c_x = toBigEndianBytes(proofJson.pi_c[0]);
|
|
1723
|
+
const pi_c_y = toBigEndianBytes(proofJson.pi_c[1]);
|
|
1724
|
+
proof.set(pi_c_x, 192);
|
|
1725
|
+
proof.set(pi_c_y, 224);
|
|
1726
|
+
return proof;
|
|
1727
|
+
}
|
|
1728
|
+
async function generateProof(circuitInputs) {
|
|
1729
|
+
verifyCircuitFiles();
|
|
1730
|
+
const { wasmPath, zkeyPath } = getCircuitPaths();
|
|
1731
|
+
const tempDir = import_path.default.dirname(zkeyPath);
|
|
1732
|
+
const timestamp = Date.now();
|
|
1733
|
+
const inputPath = import_path.default.join(tempDir, `temp_input_${timestamp}.json`);
|
|
1734
|
+
const witnessPath = import_path.default.join(tempDir, `temp_witness_${timestamp}.wtns`);
|
|
1735
|
+
const proofPath = import_path.default.join(tempDir, `temp_proof_${timestamp}.json`);
|
|
1736
|
+
const publicPath = import_path.default.join(tempDir, `temp_public_${timestamp}.json`);
|
|
1737
|
+
try {
|
|
1738
|
+
(0, import_fs.writeFileSync)(inputPath, JSON.stringify(circuitInputs));
|
|
1739
|
+
const witnessGenScript = import_path.default.join(
|
|
1740
|
+
import_path.default.dirname(wasmPath),
|
|
1741
|
+
"generate_witness.cjs"
|
|
1742
|
+
);
|
|
1743
|
+
if (!(0, import_fs.existsSync)(witnessGenScript)) {
|
|
1744
|
+
throw new Error(`Witness generation script not found: ${witnessGenScript}`);
|
|
1745
|
+
}
|
|
1746
|
+
(0, import_child_process.execSync)(`node ${witnessGenScript} ${wasmPath} ${inputPath} ${witnessPath}`, {
|
|
1747
|
+
stdio: "pipe",
|
|
1748
|
+
cwd: tempDir
|
|
1749
|
+
});
|
|
1750
|
+
(0, import_child_process.execSync)(
|
|
1751
|
+
`npx snarkjs groth16 prove ${zkeyPath} ${witnessPath} ${proofPath} ${publicPath}`,
|
|
1752
|
+
{
|
|
1753
|
+
stdio: "pipe",
|
|
1754
|
+
cwd: tempDir
|
|
1755
|
+
}
|
|
1756
|
+
);
|
|
1757
|
+
const proofJson = JSON.parse((0, import_fs.readFileSync)(proofPath, "utf-8"));
|
|
1758
|
+
const publicSignals = JSON.parse((0, import_fs.readFileSync)(publicPath, "utf-8"));
|
|
1759
|
+
const proof = parseProof(proofJson);
|
|
1760
|
+
return { proof, publicSignals };
|
|
1761
|
+
} finally {
|
|
1762
|
+
const cleanup = (filePath) => {
|
|
1763
|
+
try {
|
|
1764
|
+
if ((0, import_fs.existsSync)(filePath)) (0, import_fs.unlinkSync)(filePath);
|
|
1765
|
+
} catch {
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
cleanup(inputPath);
|
|
1769
|
+
cleanup(witnessPath);
|
|
1770
|
+
cleanup(proofPath);
|
|
1771
|
+
cleanup(publicPath);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async function generateProofInMemory(circuitInputs) {
|
|
1775
|
+
verifyCircuitFiles();
|
|
1776
|
+
const { wasmPath, zkeyPath } = getCircuitPaths();
|
|
1777
|
+
const snarkjs = await import("snarkjs");
|
|
1778
|
+
const { proof: proofData, publicSignals } = await snarkjs.groth16.fullProve(
|
|
1779
|
+
circuitInputs,
|
|
1780
|
+
wasmPath,
|
|
1781
|
+
zkeyPath
|
|
1782
|
+
);
|
|
1783
|
+
const proof = parseProof(proofData);
|
|
1784
|
+
return { proof, publicSignals };
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/transaction.ts
|
|
1788
|
+
async function buildTransaction(request, walletKeys, relayerUrl) {
|
|
1789
|
+
const client = getRelayerClient(relayerUrl);
|
|
1790
|
+
const pool = await client.getPool();
|
|
1791
|
+
const currentTreeIndex = pool.currentTreeIndex;
|
|
1792
|
+
const depositAmount = request.depositAmount ?? 0n;
|
|
1793
|
+
const withdrawAmount = request.withdrawAmount ?? 0n;
|
|
1794
|
+
const fee = request.fee ?? 0n;
|
|
1795
|
+
const inputUtxos = request.inputUtxos ?? [];
|
|
1796
|
+
const outputs = request.outputs ?? [];
|
|
1797
|
+
if (depositAmount === 0n && inputUtxos.length === 0) {
|
|
1798
|
+
throw new Error("Either depositAmount or inputUtxos is required");
|
|
1799
|
+
}
|
|
1800
|
+
if (withdrawAmount > 0n && !request.recipient) {
|
|
1801
|
+
throw new Error("recipient is required when withdrawing");
|
|
1802
|
+
}
|
|
1803
|
+
let inputTreeIndex = currentTreeIndex;
|
|
1804
|
+
if (inputUtxos.length > 0) {
|
|
1805
|
+
inputTreeIndex = inputUtxos[0].treeIndex;
|
|
1806
|
+
for (const utxo of inputUtxos) {
|
|
1807
|
+
if (utxo.treeIndex !== inputTreeIndex) {
|
|
1808
|
+
throw new Error("All input UTXOs must be from the same tree");
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
const leaves = await client.getCommitmentsForTree(inputTreeIndex);
|
|
1813
|
+
let merkleRoot;
|
|
1814
|
+
if (inputUtxos.length > 0) {
|
|
1815
|
+
merkleRoot = await fetchMerkleRoot(inputTreeIndex, relayerUrl);
|
|
1816
|
+
} else {
|
|
1817
|
+
merkleRoot = await fetchMerkleRoot(currentTreeIndex, relayerUrl);
|
|
1818
|
+
}
|
|
1819
|
+
const nextLeafIndex = await getNextLeafIndex(currentTreeIndex, relayerUrl);
|
|
1820
|
+
const totalInputValue = inputUtxos.reduce((sum, u) => sum + u.value, 0n);
|
|
1821
|
+
const totalAvailable = totalInputValue + depositAmount;
|
|
1822
|
+
const mintBigInt = pubkeyToBigInt(request.mint);
|
|
1823
|
+
const outputUtxos = [];
|
|
1824
|
+
let totalOutputValue = 0n;
|
|
1825
|
+
let outputIndex = 0;
|
|
1826
|
+
for (const output of outputs) {
|
|
1827
|
+
const owner = output.owner === "self" ? walletKeys.owner : output.owner;
|
|
1828
|
+
const utxo = createUTXO(
|
|
1829
|
+
output.amount,
|
|
1830
|
+
mintBigInt,
|
|
1831
|
+
owner,
|
|
1832
|
+
nextLeafIndex + outputIndex,
|
|
1833
|
+
currentTreeIndex
|
|
1834
|
+
);
|
|
1835
|
+
outputUtxos.push(utxo);
|
|
1836
|
+
totalOutputValue += output.amount;
|
|
1837
|
+
outputIndex++;
|
|
1838
|
+
}
|
|
1839
|
+
const totalOut = withdrawAmount + fee + totalOutputValue;
|
|
1840
|
+
const changeAmount = totalAvailable - totalOut;
|
|
1841
|
+
if (changeAmount < 0n) {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
`Insufficient value. Have ${totalAvailable} (inputs: ${totalInputValue}, deposit: ${depositAmount}), need ${totalOut} (outputs: ${totalOutputValue}, withdraw: ${withdrawAmount}, fee: ${fee})`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
if (changeAmount > 0n) {
|
|
1847
|
+
if (outputUtxos.length >= NUM_OUTPUT_UTXOS) {
|
|
1848
|
+
throw new Error("No output slot available for change");
|
|
1849
|
+
}
|
|
1850
|
+
const changeUtxo = createUTXO(
|
|
1851
|
+
changeAmount,
|
|
1852
|
+
mintBigInt,
|
|
1853
|
+
walletKeys.owner,
|
|
1854
|
+
nextLeafIndex + outputIndex,
|
|
1855
|
+
currentTreeIndex
|
|
1856
|
+
);
|
|
1857
|
+
outputUtxos.push(changeUtxo);
|
|
1858
|
+
totalOutputValue += changeAmount;
|
|
1859
|
+
}
|
|
1860
|
+
while (outputUtxos.length < NUM_OUTPUT_UTXOS) {
|
|
1861
|
+
outputUtxos.push(createDummyUTXO());
|
|
1862
|
+
}
|
|
1863
|
+
const circuitInputs = buildCircuitInputs(
|
|
1864
|
+
merkleRoot,
|
|
1865
|
+
inputUtxos,
|
|
1866
|
+
outputUtxos,
|
|
1867
|
+
walletKeys,
|
|
1868
|
+
leaves,
|
|
1869
|
+
depositAmount,
|
|
1870
|
+
withdrawAmount,
|
|
1871
|
+
request.recipient ?? import_web33.PublicKey.default,
|
|
1872
|
+
request.mint,
|
|
1873
|
+
fee
|
|
1874
|
+
);
|
|
1875
|
+
const { proof } = await generateProof(circuitInputs);
|
|
1876
|
+
const nullifiers = circuitInputs.nullifiers.map((n) => BigInt(n));
|
|
1877
|
+
const publicInputs = {
|
|
1878
|
+
merkleRoot: Array.from(bigIntToBytes32(merkleRoot)),
|
|
1879
|
+
nullifiers: nullifiers.map((n) => Array.from(bigIntToBytes32(n))),
|
|
1880
|
+
outputCommitments: outputUtxos.map((u) => Array.from(bigIntToBytes32(u.commitment))),
|
|
1881
|
+
publicDeposit: depositAmount,
|
|
1882
|
+
publicWithdraw: withdrawAmount,
|
|
1883
|
+
recipient: request.recipient ?? import_web33.PublicKey.default,
|
|
1884
|
+
mintTokenAddress: request.mint,
|
|
1885
|
+
fee
|
|
1886
|
+
};
|
|
1887
|
+
const encryptedNotes = [];
|
|
1888
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
1889
|
+
const output = outputs[i];
|
|
1890
|
+
const utxo = outputUtxos[i];
|
|
1891
|
+
if (output.encryptionPubKey && utxo.value > 0n) {
|
|
1892
|
+
const encrypted = encryptNote(
|
|
1893
|
+
{
|
|
1894
|
+
value: utxo.value,
|
|
1895
|
+
mintTokenAddress: utxo.mintTokenAddress,
|
|
1896
|
+
secret: utxo.secret
|
|
1897
|
+
// leafIndex omitted - recipient queries relayer with computed commitment
|
|
1898
|
+
},
|
|
1899
|
+
output.encryptionPubKey
|
|
1900
|
+
);
|
|
1901
|
+
encryptedNotes.push(Buffer.from(serializeEncryptedNote(encrypted)));
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (changeAmount > 0n) {
|
|
1905
|
+
const changeIndex = outputs.length;
|
|
1906
|
+
const changeUtxo = outputUtxos[changeIndex];
|
|
1907
|
+
const encrypted = encryptNote(
|
|
1908
|
+
{
|
|
1909
|
+
value: changeUtxo.value,
|
|
1910
|
+
mintTokenAddress: changeUtxo.mintTokenAddress,
|
|
1911
|
+
secret: changeUtxo.secret
|
|
1912
|
+
// leafIndex omitted - we already know it locally
|
|
1913
|
+
},
|
|
1914
|
+
walletKeys.encryptionKeyPair.publicKey
|
|
1915
|
+
);
|
|
1916
|
+
encryptedNotes.push(Buffer.from(serializeEncryptedNote(encrypted)));
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
proof,
|
|
1920
|
+
publicInputs,
|
|
1921
|
+
encryptedNotes,
|
|
1922
|
+
outputUtxos: outputUtxos.filter((u) => u.value > 0n),
|
|
1923
|
+
nullifiers,
|
|
1924
|
+
inputTreeIndex,
|
|
1925
|
+
outputTreeIndex: currentTreeIndex
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function buildCircuitInputs(merkleRoot, inputUtxos, outputUtxos, walletKeys, leaves, depositAmount, withdrawAmount, recipient, mint, fee) {
|
|
1929
|
+
const inputSpendingKeys = [];
|
|
1930
|
+
const inputValues = [];
|
|
1931
|
+
const inputSecrets = [];
|
|
1932
|
+
const inputLeafIndices = [];
|
|
1933
|
+
const inputPathElements = [];
|
|
1934
|
+
const inputPathIndices = [];
|
|
1935
|
+
const nullifiers = [];
|
|
1936
|
+
for (let i = 0; i < NUM_INPUT_UTXOS; i++) {
|
|
1937
|
+
if (i < inputUtxos.length) {
|
|
1938
|
+
const utxo = inputUtxos[i];
|
|
1939
|
+
inputSpendingKeys.push(walletKeys.spendingKey.toString());
|
|
1940
|
+
inputValues.push(utxo.value.toString());
|
|
1941
|
+
inputSecrets.push(utxo.secret.toString());
|
|
1942
|
+
inputLeafIndices.push(utxo.leafIndex.toString());
|
|
1943
|
+
const { pathElements, pathIndices } = computeMerklePathFromLeaves(
|
|
1944
|
+
utxo.leafIndex,
|
|
1945
|
+
leaves
|
|
1946
|
+
);
|
|
1947
|
+
inputPathElements.push(pathElements.map((e) => e.toString()));
|
|
1948
|
+
inputPathIndices.push(pathIndices);
|
|
1949
|
+
const nullifier = computeNullifier(
|
|
1950
|
+
walletKeys.spendingKey,
|
|
1951
|
+
utxo.commitment,
|
|
1952
|
+
utxo.leafIndex
|
|
1953
|
+
);
|
|
1954
|
+
nullifiers.push(nullifier.toString());
|
|
1955
|
+
} else {
|
|
1956
|
+
inputSpendingKeys.push("0");
|
|
1957
|
+
inputValues.push("0");
|
|
1958
|
+
inputSecrets.push("0");
|
|
1959
|
+
inputLeafIndices.push("0");
|
|
1960
|
+
inputPathElements.push(Array(MERKLE_TREE_DEPTH).fill("0"));
|
|
1961
|
+
inputPathIndices.push(Array(MERKLE_TREE_DEPTH).fill(0));
|
|
1962
|
+
nullifiers.push("0");
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return {
|
|
1966
|
+
merkleRoot: merkleRoot.toString(),
|
|
1967
|
+
nullifiers,
|
|
1968
|
+
outputCommitments: outputUtxos.map((u) => u.commitment.toString()),
|
|
1969
|
+
publicDeposit: depositAmount.toString(),
|
|
1970
|
+
publicWithdraw: withdrawAmount.toString(),
|
|
1971
|
+
recipient: pubkeyToBigInt(recipient).toString(),
|
|
1972
|
+
mintTokenAddress: pubkeyToBigInt(mint).toString(),
|
|
1973
|
+
fee: fee.toString(),
|
|
1974
|
+
inputSpendingKeys,
|
|
1975
|
+
inputValues,
|
|
1976
|
+
inputSecrets,
|
|
1977
|
+
inputLeafIndices,
|
|
1978
|
+
inputPathElements,
|
|
1979
|
+
inputPathIndices,
|
|
1980
|
+
outputValues: outputUtxos.map((u) => u.value.toString()),
|
|
1981
|
+
outputOwners: outputUtxos.map((u) => u.owner.toString()),
|
|
1982
|
+
outputSecrets: outputUtxos.map((u) => u.secret.toString())
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
function buildTransactionAccounts(builtTx, payer, mint, depositAmount, withdrawAmount, recipient) {
|
|
1986
|
+
const [poolPda] = getPoolPDA();
|
|
1987
|
+
const [vaultPda] = getVaultPDA(mint);
|
|
1988
|
+
const [inputTreePda] = getMerkleTreePDA(builtTx.inputTreeIndex);
|
|
1989
|
+
const [outputTreePda] = getMerkleTreePDA(builtTx.outputTreeIndex);
|
|
1990
|
+
const preInstructions = [];
|
|
1991
|
+
let depositorTokenAccount = null;
|
|
1992
|
+
let depositor = null;
|
|
1993
|
+
if (depositAmount > 0n) {
|
|
1994
|
+
depositorTokenAccount = (0, import_spl_token.getAssociatedTokenAddressSync)(
|
|
1995
|
+
mint,
|
|
1996
|
+
payer,
|
|
1997
|
+
false,
|
|
1998
|
+
TOKEN_2022_PROGRAM_ID
|
|
1999
|
+
);
|
|
2000
|
+
depositor = payer;
|
|
2001
|
+
}
|
|
2002
|
+
let recipientTokenAccount = null;
|
|
2003
|
+
if (withdrawAmount > 0n && recipient) {
|
|
2004
|
+
recipientTokenAccount = (0, import_spl_token.getAssociatedTokenAddressSync)(
|
|
2005
|
+
mint,
|
|
2006
|
+
recipient,
|
|
2007
|
+
false,
|
|
2008
|
+
TOKEN_2022_PROGRAM_ID
|
|
2009
|
+
);
|
|
2010
|
+
const createAtaIx = (0, import_spl_token.createAssociatedTokenAccountIdempotentInstruction)(
|
|
2011
|
+
payer,
|
|
2012
|
+
recipientTokenAccount,
|
|
2013
|
+
recipient,
|
|
2014
|
+
mint,
|
|
2015
|
+
TOKEN_2022_PROGRAM_ID
|
|
2016
|
+
);
|
|
2017
|
+
preInstructions.push(createAtaIx);
|
|
2018
|
+
}
|
|
2019
|
+
const nullifierPdas = [];
|
|
2020
|
+
for (const nullifier of builtTx.nullifiers) {
|
|
2021
|
+
if (nullifier === 0n) {
|
|
2022
|
+
nullifierPdas.push(null);
|
|
2023
|
+
} else {
|
|
2024
|
+
const nullifierBytes = bigIntToBytes32(nullifier);
|
|
2025
|
+
const [nullifierPda] = getNullifierPDA(nullifierBytes);
|
|
2026
|
+
nullifierPdas.push(nullifierPda);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
const inputTreeIndex = builtTx.inputTreeIndex !== builtTx.outputTreeIndex ? builtTx.inputTreeIndex : null;
|
|
2030
|
+
return {
|
|
2031
|
+
accounts: {
|
|
2032
|
+
payer,
|
|
2033
|
+
mint,
|
|
2034
|
+
pool: poolPda,
|
|
2035
|
+
inputTree: inputTreePda,
|
|
2036
|
+
merkleTree: outputTreePda,
|
|
2037
|
+
vault: vaultPda,
|
|
2038
|
+
depositorTokenAccount,
|
|
2039
|
+
depositor,
|
|
2040
|
+
recipientTokenAccount,
|
|
2041
|
+
feeRecipientTokenAccount: null,
|
|
2042
|
+
// TODO: Add fee recipient support
|
|
2043
|
+
nullifierAccount0: nullifierPdas[0] ?? null,
|
|
2044
|
+
nullifierAccount1: nullifierPdas[1] ?? null,
|
|
2045
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
2046
|
+
systemProgram: import_web33.SystemProgram.programId
|
|
2047
|
+
},
|
|
2048
|
+
preInstructions,
|
|
2049
|
+
inputTreeIndex
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function toAnchorPublicInputs(builtTx) {
|
|
2053
|
+
return {
|
|
2054
|
+
merkleRoot: builtTx.publicInputs.merkleRoot,
|
|
2055
|
+
nullifiers: builtTx.publicInputs.nullifiers,
|
|
2056
|
+
outputCommitments: builtTx.publicInputs.outputCommitments,
|
|
2057
|
+
publicDeposit: new import_anchor.BN(builtTx.publicInputs.publicDeposit.toString()),
|
|
2058
|
+
publicWithdraw: new import_anchor.BN(builtTx.publicInputs.publicWithdraw.toString()),
|
|
2059
|
+
recipient: builtTx.publicInputs.recipient,
|
|
2060
|
+
mintTokenAddress: builtTx.publicInputs.mintTokenAddress,
|
|
2061
|
+
fee: new import_anchor.BN(builtTx.publicInputs.fee.toString())
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/wallet.ts
|
|
2066
|
+
var COMPUTE_UNITS = 4e5;
|
|
2067
|
+
var COMPUTE_UNIT_PRICE = 1;
|
|
2068
|
+
var HulaWallet = class _HulaWallet {
|
|
2069
|
+
connection;
|
|
2070
|
+
relayerUrl;
|
|
2071
|
+
programId;
|
|
2072
|
+
keys;
|
|
2073
|
+
signer;
|
|
2074
|
+
utxos = [];
|
|
2075
|
+
lastSyncedSlot = "0";
|
|
2076
|
+
initialized = false;
|
|
2077
|
+
constructor(connection, relayerUrl, programId, keys, signer) {
|
|
2078
|
+
this.connection = connection;
|
|
2079
|
+
this.relayerUrl = relayerUrl;
|
|
2080
|
+
this.programId = programId;
|
|
2081
|
+
this.keys = keys;
|
|
2082
|
+
this.signer = signer;
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Create a new wallet with a random spending key
|
|
2086
|
+
*/
|
|
2087
|
+
static async create(config) {
|
|
2088
|
+
await initPoseidon();
|
|
2089
|
+
const connection = new import_web34.Connection(config.rpcUrl, "confirmed");
|
|
2090
|
+
const relayerUrl = config.relayerUrl;
|
|
2091
|
+
const programId = config.programId ?? new import_web34.PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
|
|
2092
|
+
setDefaultRelayerUrl(relayerUrl);
|
|
2093
|
+
const spendingKey = generateSpendingKey();
|
|
2094
|
+
const keys = deriveKeys(spendingKey);
|
|
2095
|
+
const wallet = new _HulaWallet(connection, relayerUrl, programId, keys);
|
|
2096
|
+
wallet.initialized = true;
|
|
2097
|
+
return wallet;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Create a wallet from a Solana keypair
|
|
2101
|
+
*
|
|
2102
|
+
* Derives the spending key deterministically from the keypair's secret key.
|
|
2103
|
+
*/
|
|
2104
|
+
static async fromKeypair(keypair, config) {
|
|
2105
|
+
await initPoseidon();
|
|
2106
|
+
const connection = new import_web34.Connection(config.rpcUrl, "confirmed");
|
|
2107
|
+
const relayerUrl = config.relayerUrl;
|
|
2108
|
+
const programId = config.programId ?? new import_web34.PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
|
|
2109
|
+
setDefaultRelayerUrl(relayerUrl);
|
|
2110
|
+
const spendingKey = deriveSpendingKeyFromSignature(keypair.secretKey);
|
|
2111
|
+
const keys = deriveKeys(spendingKey);
|
|
2112
|
+
const wallet = new _HulaWallet(connection, relayerUrl, programId, keys, keypair);
|
|
2113
|
+
wallet.initialized = true;
|
|
2114
|
+
return wallet;
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Create a wallet from an existing spending key
|
|
2118
|
+
*/
|
|
2119
|
+
static async fromSpendingKey(spendingKey, config) {
|
|
2120
|
+
await initPoseidon();
|
|
2121
|
+
const connection = new import_web34.Connection(config.rpcUrl, "confirmed");
|
|
2122
|
+
const relayerUrl = config.relayerUrl;
|
|
2123
|
+
const programId = config.programId ?? new import_web34.PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
|
|
2124
|
+
setDefaultRelayerUrl(relayerUrl);
|
|
2125
|
+
const keys = deriveKeys(spendingKey);
|
|
2126
|
+
const wallet = new _HulaWallet(connection, relayerUrl, programId, keys);
|
|
2127
|
+
wallet.initialized = true;
|
|
2128
|
+
return wallet;
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Create a wallet from a Solana signature (deterministic derivation)
|
|
2132
|
+
*
|
|
2133
|
+
* This allows recovery as long as the user can sign with their Solana wallet.
|
|
2134
|
+
*/
|
|
2135
|
+
static async fromSignature(signature, config) {
|
|
2136
|
+
await initPoseidon();
|
|
2137
|
+
const connection = new import_web34.Connection(config.rpcUrl, "confirmed");
|
|
2138
|
+
const relayerUrl = config.relayerUrl;
|
|
2139
|
+
const programId = config.programId ?? new import_web34.PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
|
|
2140
|
+
setDefaultRelayerUrl(relayerUrl);
|
|
2141
|
+
const spendingKey = deriveSpendingKeyFromSignature(signature);
|
|
2142
|
+
const keys = deriveKeys(spendingKey);
|
|
2143
|
+
const wallet = new _HulaWallet(connection, relayerUrl, programId, keys);
|
|
2144
|
+
wallet.initialized = true;
|
|
2145
|
+
return wallet;
|
|
2146
|
+
}
|
|
2147
|
+
// ============================================================================
|
|
2148
|
+
// Getters
|
|
2149
|
+
// ============================================================================
|
|
2150
|
+
/**
|
|
2151
|
+
* Get wallet's owner hash (public identifier)
|
|
2152
|
+
*/
|
|
2153
|
+
get owner() {
|
|
2154
|
+
return this.keys.owner;
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Get wallet's owner hash as hex string
|
|
2158
|
+
*/
|
|
2159
|
+
getOwnerHash() {
|
|
2160
|
+
return "0x" + this.keys.owner.toString(16).padStart(64, "0");
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Get wallet's encryption public key as hex string
|
|
2164
|
+
*/
|
|
2165
|
+
getEncryptionPublicKey() {
|
|
2166
|
+
return "0x" + Buffer.from(this.keys.encryptionKeyPair.publicKey).toString("hex");
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Get wallet's encryption public key as bytes
|
|
2170
|
+
*/
|
|
2171
|
+
get encryptionPublicKey() {
|
|
2172
|
+
return this.keys.encryptionKeyPair.publicKey;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Get spending key (CAREFUL - this is sensitive!)
|
|
2176
|
+
*/
|
|
2177
|
+
get spendingKey() {
|
|
2178
|
+
return this.keys.spendingKey;
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Get all wallet keys
|
|
2182
|
+
*/
|
|
2183
|
+
get walletKeys() {
|
|
2184
|
+
return this.keys;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Get the connection
|
|
2188
|
+
*/
|
|
2189
|
+
getConnection() {
|
|
2190
|
+
return this.connection;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Get the signer keypair (if available)
|
|
2194
|
+
*/
|
|
2195
|
+
getSigner() {
|
|
2196
|
+
return this.signer;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Set the signer keypair
|
|
2200
|
+
*/
|
|
2201
|
+
setSigner(signer) {
|
|
2202
|
+
this.signer = signer;
|
|
2203
|
+
}
|
|
2204
|
+
// ============================================================================
|
|
2205
|
+
// UTXO Management
|
|
2206
|
+
// ============================================================================
|
|
2207
|
+
/**
|
|
2208
|
+
* Sync wallet with relayer
|
|
2209
|
+
*
|
|
2210
|
+
* Fetches new encrypted notes and checks for spent UTXOs.
|
|
2211
|
+
*/
|
|
2212
|
+
async sync(onProgress) {
|
|
2213
|
+
if (!this.initialized) {
|
|
2214
|
+
throw new Error("Wallet not initialized");
|
|
2215
|
+
}
|
|
2216
|
+
onProgress?.({ stage: "notes", current: 0, total: 100 });
|
|
2217
|
+
try {
|
|
2218
|
+
const { newUTXOs, spentUTXOs } = await syncUTXOs(
|
|
2219
|
+
this.keys,
|
|
2220
|
+
this.utxos,
|
|
2221
|
+
this.relayerUrl,
|
|
2222
|
+
this.lastSyncedSlot !== "0" ? this.lastSyncedSlot : void 0
|
|
2223
|
+
);
|
|
2224
|
+
this.utxos.push(...newUTXOs);
|
|
2225
|
+
for (const spentId of spentUTXOs) {
|
|
2226
|
+
const utxo = this.utxos.find((u) => u.commitment === spentId);
|
|
2227
|
+
if (utxo) {
|
|
2228
|
+
utxo.spent = true;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
const client = getRelayerClient(this.relayerUrl);
|
|
2232
|
+
const stats = await client.getStats();
|
|
2233
|
+
this.lastSyncedSlot = Date.now().toString();
|
|
2234
|
+
onProgress?.({ stage: "complete", current: 100, total: 100 });
|
|
2235
|
+
return {
|
|
2236
|
+
newUtxos: newUTXOs.length,
|
|
2237
|
+
spentUtxos: spentUTXOs.length,
|
|
2238
|
+
errors: []
|
|
2239
|
+
};
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
const errorMsg = err instanceof Error ? err.message : "Sync failed";
|
|
2242
|
+
onProgress?.({ stage: "complete", current: 100, total: 100 });
|
|
2243
|
+
return {
|
|
2244
|
+
newUtxos: 0,
|
|
2245
|
+
spentUtxos: 0,
|
|
2246
|
+
errors: [errorMsg]
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Get all unspent UTXOs
|
|
2252
|
+
*/
|
|
2253
|
+
getUnspentUTXOs() {
|
|
2254
|
+
return this.utxos.filter((u) => !u.spent).map(deserializeUTXO);
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Get all UTXOs (including spent)
|
|
2258
|
+
*/
|
|
2259
|
+
getAllUTXOs() {
|
|
2260
|
+
return [...this.utxos];
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Get balance for a specific mint
|
|
2264
|
+
*/
|
|
2265
|
+
getBalance(mint) {
|
|
2266
|
+
const mintBigInt = pubkeyToBigInt(mint);
|
|
2267
|
+
const unspent = this.getUnspentUTXOs();
|
|
2268
|
+
return calculateBalance(unspent, mintBigInt);
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Import UTXOs (for wallet recovery or manual import)
|
|
2272
|
+
*/
|
|
2273
|
+
importUTXOs(utxos) {
|
|
2274
|
+
this.utxos.push(...utxos);
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Export UTXOs for backup
|
|
2278
|
+
*/
|
|
2279
|
+
exportUTXOs() {
|
|
2280
|
+
return [...this.utxos];
|
|
2281
|
+
}
|
|
2282
|
+
// ============================================================================
|
|
2283
|
+
// Transaction Building
|
|
2284
|
+
// ============================================================================
|
|
2285
|
+
/**
|
|
2286
|
+
* Deposit tokens into the privacy pool
|
|
2287
|
+
*
|
|
2288
|
+
* @param mint - Token mint address
|
|
2289
|
+
* @param amount - Amount in raw token units
|
|
2290
|
+
* @returns Transaction signature
|
|
2291
|
+
*/
|
|
2292
|
+
async deposit(mint, amount) {
|
|
2293
|
+
if (!this.signer) {
|
|
2294
|
+
throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
|
|
2295
|
+
}
|
|
2296
|
+
const builtTx = await buildTransaction(
|
|
2297
|
+
{
|
|
2298
|
+
mint,
|
|
2299
|
+
depositAmount: amount,
|
|
2300
|
+
outputs: []
|
|
2301
|
+
// Change will be created automatically
|
|
2302
|
+
},
|
|
2303
|
+
this.keys,
|
|
2304
|
+
this.relayerUrl
|
|
2305
|
+
);
|
|
2306
|
+
const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
|
|
2307
|
+
builtTx,
|
|
2308
|
+
this.signer.publicKey,
|
|
2309
|
+
mint,
|
|
2310
|
+
amount,
|
|
2311
|
+
0n
|
|
2312
|
+
);
|
|
2313
|
+
const allPreInstructions = [
|
|
2314
|
+
import_web34.ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
|
|
2315
|
+
import_web34.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
|
|
2316
|
+
...preInstructions
|
|
2317
|
+
];
|
|
2318
|
+
const wallet = new import_anchor2.Wallet(this.signer);
|
|
2319
|
+
const provider = new import_anchor2.AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
|
|
2320
|
+
const program = new import_anchor2.Program(idl_default, provider);
|
|
2321
|
+
const publicInputs = toAnchorPublicInputs(builtTx);
|
|
2322
|
+
const signature = await program.methods.transact(
|
|
2323
|
+
inputTreeIndex,
|
|
2324
|
+
Array.from(builtTx.proof),
|
|
2325
|
+
publicInputs,
|
|
2326
|
+
builtTx.encryptedNotes
|
|
2327
|
+
).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
|
|
2328
|
+
return signature;
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Transfer tokens privately to another user
|
|
2332
|
+
*
|
|
2333
|
+
* @param mint - Token mint address
|
|
2334
|
+
* @param amount - Amount in raw token units
|
|
2335
|
+
* @param recipientOwner - Recipient's owner hash (bigint)
|
|
2336
|
+
* @param recipientEncryptionPubKey - Recipient's encryption public key
|
|
2337
|
+
* @returns Transaction signature
|
|
2338
|
+
*/
|
|
2339
|
+
async transfer(mint, amount, recipientOwner, recipientEncryptionPubKey) {
|
|
2340
|
+
if (!this.signer) {
|
|
2341
|
+
throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
|
|
2342
|
+
}
|
|
2343
|
+
const mintBigInt = pubkeyToBigInt(mint);
|
|
2344
|
+
const unspent = this.getUnspentUTXOs().filter(
|
|
2345
|
+
(u) => u.mintTokenAddress === mintBigInt
|
|
2346
|
+
);
|
|
2347
|
+
const inputUtxos = selectUTXOs(unspent, amount, mintBigInt);
|
|
2348
|
+
const builtTx = await buildTransaction(
|
|
2349
|
+
{
|
|
2350
|
+
mint,
|
|
2351
|
+
inputUtxos,
|
|
2352
|
+
outputs: [
|
|
2353
|
+
{
|
|
2354
|
+
owner: recipientOwner,
|
|
2355
|
+
amount,
|
|
2356
|
+
encryptionPubKey: recipientEncryptionPubKey
|
|
2357
|
+
}
|
|
2358
|
+
]
|
|
2359
|
+
},
|
|
2360
|
+
this.keys,
|
|
2361
|
+
this.relayerUrl
|
|
2362
|
+
);
|
|
2363
|
+
const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
|
|
2364
|
+
builtTx,
|
|
2365
|
+
this.signer.publicKey,
|
|
2366
|
+
mint,
|
|
2367
|
+
0n,
|
|
2368
|
+
0n
|
|
2369
|
+
);
|
|
2370
|
+
const allPreInstructions = [
|
|
2371
|
+
import_web34.ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
|
|
2372
|
+
import_web34.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
|
|
2373
|
+
...preInstructions
|
|
2374
|
+
];
|
|
2375
|
+
const wallet = new import_anchor2.Wallet(this.signer);
|
|
2376
|
+
const provider = new import_anchor2.AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
|
|
2377
|
+
const program = new import_anchor2.Program(idl_default, provider);
|
|
2378
|
+
const publicInputs = toAnchorPublicInputs(builtTx);
|
|
2379
|
+
const signature = await program.methods.transact(
|
|
2380
|
+
inputTreeIndex,
|
|
2381
|
+
Array.from(builtTx.proof),
|
|
2382
|
+
publicInputs,
|
|
2383
|
+
builtTx.encryptedNotes
|
|
2384
|
+
).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
|
|
2385
|
+
for (const utxo of inputUtxos) {
|
|
2386
|
+
const serialized = this.utxos.find((u) => u.commitment === utxo.commitment.toString());
|
|
2387
|
+
if (serialized) {
|
|
2388
|
+
serialized.spent = true;
|
|
2389
|
+
serialized.spentTx = signature;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return signature;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Withdraw tokens from the privacy pool to a public address
|
|
2396
|
+
*
|
|
2397
|
+
* @param mint - Token mint address
|
|
2398
|
+
* @param amount - Amount in raw token units
|
|
2399
|
+
* @param recipient - Public key to receive the tokens
|
|
2400
|
+
* @returns Transaction signature
|
|
2401
|
+
*/
|
|
2402
|
+
async withdraw(mint, amount, recipient) {
|
|
2403
|
+
if (!this.signer) {
|
|
2404
|
+
throw new Error("No signer available. Use fromKeypair() or setSigner() to set a signer.");
|
|
2405
|
+
}
|
|
2406
|
+
const mintBigInt = pubkeyToBigInt(mint);
|
|
2407
|
+
const unspent = this.getUnspentUTXOs().filter(
|
|
2408
|
+
(u) => u.mintTokenAddress === mintBigInt
|
|
2409
|
+
);
|
|
2410
|
+
const inputUtxos = selectUTXOs(unspent, amount, mintBigInt);
|
|
2411
|
+
const builtTx = await buildTransaction(
|
|
2412
|
+
{
|
|
2413
|
+
mint,
|
|
2414
|
+
inputUtxos,
|
|
2415
|
+
withdrawAmount: amount,
|
|
2416
|
+
recipient
|
|
2417
|
+
},
|
|
2418
|
+
this.keys,
|
|
2419
|
+
this.relayerUrl
|
|
2420
|
+
);
|
|
2421
|
+
const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
|
|
2422
|
+
builtTx,
|
|
2423
|
+
this.signer.publicKey,
|
|
2424
|
+
mint,
|
|
2425
|
+
0n,
|
|
2426
|
+
amount,
|
|
2427
|
+
recipient
|
|
2428
|
+
);
|
|
2429
|
+
const allPreInstructions = [
|
|
2430
|
+
import_web34.ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
|
|
2431
|
+
import_web34.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_UNIT_PRICE }),
|
|
2432
|
+
...preInstructions
|
|
2433
|
+
];
|
|
2434
|
+
const wallet = new import_anchor2.Wallet(this.signer);
|
|
2435
|
+
const provider = new import_anchor2.AnchorProvider(this.connection, wallet, { commitment: "confirmed" });
|
|
2436
|
+
const program = new import_anchor2.Program(idl_default, provider);
|
|
2437
|
+
const publicInputs = toAnchorPublicInputs(builtTx);
|
|
2438
|
+
const signature = await program.methods.transact(
|
|
2439
|
+
inputTreeIndex,
|
|
2440
|
+
Array.from(builtTx.proof),
|
|
2441
|
+
publicInputs,
|
|
2442
|
+
builtTx.encryptedNotes
|
|
2443
|
+
).accounts(accounts).preInstructions(allPreInstructions).signers([this.signer]).rpc();
|
|
2444
|
+
for (const utxo of inputUtxos) {
|
|
2445
|
+
const serialized = this.utxos.find((u) => u.commitment === utxo.commitment.toString());
|
|
2446
|
+
if (serialized) {
|
|
2447
|
+
serialized.spent = true;
|
|
2448
|
+
serialized.spentTx = signature;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return signature;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Build a custom transaction
|
|
2455
|
+
*/
|
|
2456
|
+
async buildTransaction(request) {
|
|
2457
|
+
const tx = await buildTransaction(request, this.keys, this.relayerUrl);
|
|
2458
|
+
const { preInstructions } = buildTransactionAccounts(
|
|
2459
|
+
tx,
|
|
2460
|
+
import_web34.PublicKey.default,
|
|
2461
|
+
request.mint,
|
|
2462
|
+
request.depositAmount ?? 0n,
|
|
2463
|
+
request.withdrawAmount ?? 0n,
|
|
2464
|
+
request.recipient
|
|
2465
|
+
);
|
|
2466
|
+
return { transaction: tx, instructions: preInstructions };
|
|
2467
|
+
}
|
|
2468
|
+
// ============================================================================
|
|
2469
|
+
// Transaction Submission
|
|
2470
|
+
// ============================================================================
|
|
2471
|
+
/**
|
|
2472
|
+
* Submit a transaction using an Anchor program
|
|
2473
|
+
*
|
|
2474
|
+
* @param program - Anchor program instance (any type to avoid IDL complexity)
|
|
2475
|
+
* @param payer - Keypair for signing
|
|
2476
|
+
* @param builtTx - Built transaction from buildTransaction()
|
|
2477
|
+
* @param mint - Token mint
|
|
2478
|
+
* @param depositAmount - Amount being deposited (if any)
|
|
2479
|
+
* @param withdrawAmount - Amount being withdrawn (if any)
|
|
2480
|
+
* @param recipient - Recipient for withdrawal (if any)
|
|
2481
|
+
*/
|
|
2482
|
+
async submitTransaction(program, payer, builtTx, mint, depositAmount = 0n, withdrawAmount = 0n, recipient) {
|
|
2483
|
+
const { accounts, preInstructions, inputTreeIndex } = buildTransactionAccounts(
|
|
2484
|
+
builtTx,
|
|
2485
|
+
payer.publicKey,
|
|
2486
|
+
mint,
|
|
2487
|
+
depositAmount,
|
|
2488
|
+
withdrawAmount,
|
|
2489
|
+
recipient
|
|
2490
|
+
);
|
|
2491
|
+
const publicInputs = toAnchorPublicInputs(builtTx);
|
|
2492
|
+
const tx = await program.methods.transact(
|
|
2493
|
+
inputTreeIndex,
|
|
2494
|
+
Array.from(builtTx.proof),
|
|
2495
|
+
publicInputs,
|
|
2496
|
+
builtTx.encryptedNotes
|
|
2497
|
+
).accounts(accounts).preInstructions(preInstructions).signers([payer]).rpc();
|
|
2498
|
+
for (const _utxo of builtTx.outputUtxos) {
|
|
2499
|
+
}
|
|
2500
|
+
return tx;
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
function getKeyDerivationMessage() {
|
|
2504
|
+
return new TextEncoder().encode("Hula Privacy Wallet Key Derivation v1");
|
|
2505
|
+
}
|
|
2506
|
+
async function initHulaSDK() {
|
|
2507
|
+
if (!isPoseidonInitialized()) {
|
|
2508
|
+
await initPoseidon();
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2512
|
+
0 && (module.exports = {
|
|
2513
|
+
DOMAIN_ENCRYPTION,
|
|
2514
|
+
DOMAIN_NULLIFIER,
|
|
2515
|
+
DOMAIN_OWNER,
|
|
2516
|
+
DOMAIN_VIEWING,
|
|
2517
|
+
FIELD_PRIME,
|
|
2518
|
+
HulaWallet,
|
|
2519
|
+
MAX_LEAVES,
|
|
2520
|
+
MERKLE_TREE_DEPTH,
|
|
2521
|
+
MERKLE_TREE_SEED,
|
|
2522
|
+
NULLIFIER_SEED,
|
|
2523
|
+
NUM_INPUT_UTXOS,
|
|
2524
|
+
NUM_OUTPUT_UTXOS,
|
|
2525
|
+
POOL_SEED,
|
|
2526
|
+
PROGRAM_ID,
|
|
2527
|
+
PROOF_SIZE,
|
|
2528
|
+
RelayerClient,
|
|
2529
|
+
TOKEN_2022_PROGRAM_ID,
|
|
2530
|
+
VAULT_SEED,
|
|
2531
|
+
bigIntToBytes,
|
|
2532
|
+
bigIntToBytes32,
|
|
2533
|
+
buildTransaction,
|
|
2534
|
+
buildTransactionAccounts,
|
|
2535
|
+
bytesToBigInt,
|
|
2536
|
+
bytesToHex,
|
|
2537
|
+
calculateBalance,
|
|
2538
|
+
computeCommitment,
|
|
2539
|
+
computeMerklePathFromLeaves,
|
|
2540
|
+
computeMerkleRoot,
|
|
2541
|
+
computeNullifier,
|
|
2542
|
+
computeNullifierFromKeys,
|
|
2543
|
+
computeZeros,
|
|
2544
|
+
createDummyUTXO,
|
|
2545
|
+
createUTXO,
|
|
2546
|
+
decryptNote,
|
|
2547
|
+
deriveKeys,
|
|
2548
|
+
deriveSpendingKeyFromSignature,
|
|
2549
|
+
deserializeEncryptedNote,
|
|
2550
|
+
deserializeUTXO,
|
|
2551
|
+
encryptNote,
|
|
2552
|
+
fetchMerklePath,
|
|
2553
|
+
fetchMerkleRoot,
|
|
2554
|
+
formatCommitment,
|
|
2555
|
+
generateProof,
|
|
2556
|
+
generateProofInMemory,
|
|
2557
|
+
generateSpendingKey,
|
|
2558
|
+
getCircuitPaths,
|
|
2559
|
+
getCurrentTreeIndex,
|
|
2560
|
+
getEmptyTreeRoot,
|
|
2561
|
+
getKeyDerivationMessage,
|
|
2562
|
+
getMerkleTreePDA,
|
|
2563
|
+
getNextLeafIndex,
|
|
2564
|
+
getNullifierPDA,
|
|
2565
|
+
getPoolPDA,
|
|
2566
|
+
getPoseidon,
|
|
2567
|
+
getRelayerClient,
|
|
2568
|
+
getVaultPDA,
|
|
2569
|
+
getZeroAtLevel,
|
|
2570
|
+
hexToBytes,
|
|
2571
|
+
initHulaSDK,
|
|
2572
|
+
initPoseidon,
|
|
2573
|
+
isPoseidonInitialized,
|
|
2574
|
+
parseProof,
|
|
2575
|
+
poseidonHash,
|
|
2576
|
+
pubkeyToBigInt,
|
|
2577
|
+
scanNotesForUTXOs,
|
|
2578
|
+
selectUTXOs,
|
|
2579
|
+
serializeEncryptedNote,
|
|
2580
|
+
serializeUTXO,
|
|
2581
|
+
setCircuitPaths,
|
|
2582
|
+
setDefaultRelayerUrl,
|
|
2583
|
+
syncUTXOs,
|
|
2584
|
+
toAnchorPublicInputs,
|
|
2585
|
+
verifyCircuitFiles,
|
|
2586
|
+
verifyMerklePath
|
|
2587
|
+
});
|