@hula-privacy/mixer 0.1.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/README.md +290 -0
- package/package.json +52 -0
- package/src/api.ts +276 -0
- package/src/constants.ts +108 -0
- package/src/crypto.ts +293 -0
- package/src/index.ts +185 -0
- package/src/merkle.ts +220 -0
- package/src/proof.ts +251 -0
- package/src/transaction.ts +464 -0
- package/src/types.ts +331 -0
- package/src/utxo.ts +358 -0
- package/src/wallet.ts +475 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Hula Privacy SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PublicKey, Keypair, Connection } from "@solana/web3.js";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Cryptographic Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* UTXO (Unspent Transaction Output) in the privacy pool
|
|
13
|
+
*/
|
|
14
|
+
export interface UTXO {
|
|
15
|
+
/** Amount in raw token units */
|
|
16
|
+
value: bigint;
|
|
17
|
+
/** Token mint address as bigint */
|
|
18
|
+
mintTokenAddress: bigint;
|
|
19
|
+
/** Owner hash (derived from spending key) */
|
|
20
|
+
owner: bigint;
|
|
21
|
+
/** Random blinding factor */
|
|
22
|
+
secret: bigint;
|
|
23
|
+
/** Poseidon hash commitment */
|
|
24
|
+
commitment: bigint;
|
|
25
|
+
/** Index in the merkle tree */
|
|
26
|
+
leafIndex: number;
|
|
27
|
+
/** Which tree this UTXO is in */
|
|
28
|
+
treeIndex: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serializable UTXO for storage/transmission
|
|
33
|
+
*/
|
|
34
|
+
export interface SerializableUTXO {
|
|
35
|
+
value: string;
|
|
36
|
+
mintTokenAddress: string;
|
|
37
|
+
owner: string;
|
|
38
|
+
secret: string;
|
|
39
|
+
commitment: string;
|
|
40
|
+
leafIndex: number;
|
|
41
|
+
treeIndex: number;
|
|
42
|
+
/** Token mint public key */
|
|
43
|
+
mint: string;
|
|
44
|
+
/** Whether this UTXO has been spent */
|
|
45
|
+
spent: boolean;
|
|
46
|
+
/** Transaction that spent this UTXO */
|
|
47
|
+
spentTx?: string;
|
|
48
|
+
/** Transaction that created this UTXO */
|
|
49
|
+
createdTx: string;
|
|
50
|
+
/** Timestamp when created */
|
|
51
|
+
createdAt: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wallet keys derived from spending key
|
|
56
|
+
*/
|
|
57
|
+
export interface WalletKeys {
|
|
58
|
+
/** Master spending key - KEEP SECRET */
|
|
59
|
+
spendingKey: bigint;
|
|
60
|
+
/** Owner hash - public identifier for receiving funds */
|
|
61
|
+
owner: bigint;
|
|
62
|
+
/** Viewing key - for decrypting notes */
|
|
63
|
+
viewingKey: bigint;
|
|
64
|
+
/** Nullifier key - for computing nullifiers */
|
|
65
|
+
nullifierKey: bigint;
|
|
66
|
+
/** X25519 keypair for note encryption */
|
|
67
|
+
encryptionKeyPair: {
|
|
68
|
+
publicKey: Uint8Array;
|
|
69
|
+
secretKey: Uint8Array;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Encrypted note attached to transactions
|
|
75
|
+
*/
|
|
76
|
+
export interface EncryptedNote {
|
|
77
|
+
/** Ephemeral public key for ECDH */
|
|
78
|
+
ephemeralPubKey: Uint8Array;
|
|
79
|
+
/** Encrypted ciphertext */
|
|
80
|
+
ciphertext: Uint8Array;
|
|
81
|
+
/** Nonce for decryption */
|
|
82
|
+
nonce: Uint8Array;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Merkle Tree Types
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Merkle path for proving inclusion
|
|
91
|
+
*/
|
|
92
|
+
export interface MerklePath {
|
|
93
|
+
/** Sibling hashes at each level */
|
|
94
|
+
pathElements: bigint[];
|
|
95
|
+
/** Direction at each level (0 = left, 1 = right) */
|
|
96
|
+
pathIndices: number[];
|
|
97
|
+
/** Root of the tree */
|
|
98
|
+
root: bigint;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Leaf data from the relayer
|
|
103
|
+
*/
|
|
104
|
+
export interface LeafData {
|
|
105
|
+
id: string;
|
|
106
|
+
treeIndex: number;
|
|
107
|
+
leafIndex: number;
|
|
108
|
+
commitment: string;
|
|
109
|
+
slot: string;
|
|
110
|
+
createdAt: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Transaction Types
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Circuit inputs for ZK proof generation
|
|
119
|
+
*/
|
|
120
|
+
export interface CircuitInputs {
|
|
121
|
+
// Public inputs
|
|
122
|
+
merkleRoot: string;
|
|
123
|
+
nullifiers: string[];
|
|
124
|
+
outputCommitments: string[];
|
|
125
|
+
publicDeposit: string;
|
|
126
|
+
publicWithdraw: string;
|
|
127
|
+
recipient: string;
|
|
128
|
+
mintTokenAddress: string;
|
|
129
|
+
fee: string;
|
|
130
|
+
|
|
131
|
+
// Private inputs for input UTXOs
|
|
132
|
+
inputSpendingKeys: string[];
|
|
133
|
+
inputValues: string[];
|
|
134
|
+
inputSecrets: string[];
|
|
135
|
+
inputLeafIndices: string[];
|
|
136
|
+
inputPathElements: string[][];
|
|
137
|
+
inputPathIndices: number[][];
|
|
138
|
+
|
|
139
|
+
// Private inputs for output UTXOs
|
|
140
|
+
outputValues: string[];
|
|
141
|
+
outputOwners: string[];
|
|
142
|
+
outputSecrets: string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Public inputs for the on-chain verifier
|
|
147
|
+
*/
|
|
148
|
+
export interface PublicInputs {
|
|
149
|
+
merkleRoot: number[];
|
|
150
|
+
nullifiers: number[][];
|
|
151
|
+
outputCommitments: number[][];
|
|
152
|
+
publicDeposit: bigint;
|
|
153
|
+
publicWithdraw: bigint;
|
|
154
|
+
recipient: PublicKey;
|
|
155
|
+
mintTokenAddress: PublicKey;
|
|
156
|
+
fee: bigint;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Output UTXO specification
|
|
161
|
+
*/
|
|
162
|
+
export interface OutputSpec {
|
|
163
|
+
/** Owner hash (bigint) or "self" for own wallet */
|
|
164
|
+
owner: bigint | "self";
|
|
165
|
+
/** Amount in raw token units */
|
|
166
|
+
amount: bigint;
|
|
167
|
+
/** Encryption public key for encrypted note (optional) */
|
|
168
|
+
encryptionPubKey?: Uint8Array;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Transaction request for building a privacy transaction
|
|
173
|
+
*/
|
|
174
|
+
export interface TransactionRequest {
|
|
175
|
+
/** Amount to deposit from public wallet */
|
|
176
|
+
depositAmount?: bigint;
|
|
177
|
+
/** Input UTXOs to spend */
|
|
178
|
+
inputUtxos?: UTXO[];
|
|
179
|
+
/** Output UTXOs to create */
|
|
180
|
+
outputs?: OutputSpec[];
|
|
181
|
+
/** Amount to withdraw to public wallet */
|
|
182
|
+
withdrawAmount?: bigint;
|
|
183
|
+
/** Recipient public key for withdrawal */
|
|
184
|
+
recipient?: PublicKey;
|
|
185
|
+
/** Relayer fee */
|
|
186
|
+
fee?: bigint;
|
|
187
|
+
/** Token mint address */
|
|
188
|
+
mint: PublicKey;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Built transaction ready for submission
|
|
193
|
+
*/
|
|
194
|
+
export interface BuiltTransaction {
|
|
195
|
+
/** Proof bytes (256 bytes) */
|
|
196
|
+
proof: Uint8Array;
|
|
197
|
+
/** Public inputs for the circuit */
|
|
198
|
+
publicInputs: PublicInputs;
|
|
199
|
+
/** Encrypted notes for outputs */
|
|
200
|
+
encryptedNotes: Buffer[];
|
|
201
|
+
/** Created output UTXOs (for tracking) */
|
|
202
|
+
outputUtxos: UTXO[];
|
|
203
|
+
/** Nullifiers being spent */
|
|
204
|
+
nullifiers: bigint[];
|
|
205
|
+
/** Input tree index */
|
|
206
|
+
inputTreeIndex: number;
|
|
207
|
+
/** Output tree index (current tree) */
|
|
208
|
+
outputTreeIndex: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Relayer API Types
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Paginated response from relayer
|
|
217
|
+
*/
|
|
218
|
+
export interface PaginatedResponse<T> {
|
|
219
|
+
items: T[];
|
|
220
|
+
nextCursor: string | null;
|
|
221
|
+
hasMore: boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Pool data from relayer
|
|
226
|
+
*/
|
|
227
|
+
export interface PoolData {
|
|
228
|
+
authority: string;
|
|
229
|
+
commitmentCount: string;
|
|
230
|
+
merkleRoot: string;
|
|
231
|
+
paused: boolean;
|
|
232
|
+
currentTreeIndex: number;
|
|
233
|
+
lastSlot: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Tree data from relayer
|
|
238
|
+
*/
|
|
239
|
+
export interface TreeData {
|
|
240
|
+
id: string;
|
|
241
|
+
treeIndex: number;
|
|
242
|
+
nextIndex: number;
|
|
243
|
+
root: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Transaction data from relayer
|
|
248
|
+
*/
|
|
249
|
+
export interface TransactionData {
|
|
250
|
+
id: string;
|
|
251
|
+
signature: string;
|
|
252
|
+
slot: string;
|
|
253
|
+
blockTime?: string;
|
|
254
|
+
payer: string;
|
|
255
|
+
mint: string;
|
|
256
|
+
outputTreeIndex: number;
|
|
257
|
+
publicDeposit: string;
|
|
258
|
+
publicWithdraw: string;
|
|
259
|
+
fee: string;
|
|
260
|
+
success: boolean;
|
|
261
|
+
nullifiers: string[];
|
|
262
|
+
encryptedNotes: { noteIndex: number; data: string }[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Encrypted note data from relayer
|
|
267
|
+
*/
|
|
268
|
+
export interface NoteData {
|
|
269
|
+
id: string;
|
|
270
|
+
noteIndex: number;
|
|
271
|
+
encryptedData: string;
|
|
272
|
+
txSignature: string;
|
|
273
|
+
slot: string;
|
|
274
|
+
mint: string;
|
|
275
|
+
treeIndex: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Stats from relayer
|
|
280
|
+
*/
|
|
281
|
+
export interface StatsData {
|
|
282
|
+
pool: {
|
|
283
|
+
commitmentCount: string;
|
|
284
|
+
currentTreeIndex: number;
|
|
285
|
+
paused: boolean;
|
|
286
|
+
} | null;
|
|
287
|
+
treeCount: number;
|
|
288
|
+
leafCount: number;
|
|
289
|
+
transactionCount: number;
|
|
290
|
+
nullifierCount: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// SDK Config Types
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* SDK configuration
|
|
299
|
+
*/
|
|
300
|
+
export interface HulaSDKConfig {
|
|
301
|
+
/** Solana RPC endpoint */
|
|
302
|
+
rpcUrl: string;
|
|
303
|
+
/** Relayer API endpoint */
|
|
304
|
+
relayerUrl: string;
|
|
305
|
+
/** Path to circuit WASM file */
|
|
306
|
+
circuitWasmPath?: string;
|
|
307
|
+
/** Path to circuit zkey file */
|
|
308
|
+
circuitZkeyPath?: string;
|
|
309
|
+
/** Network: localnet | devnet | mainnet */
|
|
310
|
+
network?: "localnet" | "devnet" | "mainnet";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Wallet sync progress
|
|
315
|
+
*/
|
|
316
|
+
export interface SyncProgress {
|
|
317
|
+
stage: "notes" | "nullifiers" | "complete";
|
|
318
|
+
current: number;
|
|
319
|
+
total: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Wallet sync result
|
|
324
|
+
*/
|
|
325
|
+
export interface SyncResult {
|
|
326
|
+
newUtxos: number;
|
|
327
|
+
spentUtxos: number;
|
|
328
|
+
errors: string[];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
package/src/utxo.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTXO (Unspent Transaction Output) management
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for creating, scanning, and managing UTXOs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getRelayerClient } from "./api";
|
|
8
|
+
import {
|
|
9
|
+
poseidonHash,
|
|
10
|
+
decryptNote,
|
|
11
|
+
deserializeEncryptedNote,
|
|
12
|
+
generateSpendingKey,
|
|
13
|
+
hexToBytes,
|
|
14
|
+
} from "./crypto";
|
|
15
|
+
import { DOMAIN_NULLIFIER } from "./constants";
|
|
16
|
+
import type { UTXO, WalletKeys, SerializableUTXO, NoteData } from "./types";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Commitment & Nullifier Computation
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compute UTXO commitment
|
|
24
|
+
* commitment = Poseidon(value, mintTokenAddress, owner, secret)
|
|
25
|
+
*/
|
|
26
|
+
export function computeCommitment(
|
|
27
|
+
value: bigint,
|
|
28
|
+
mintTokenAddress: bigint,
|
|
29
|
+
owner: bigint,
|
|
30
|
+
secret: bigint
|
|
31
|
+
): bigint {
|
|
32
|
+
return poseidonHash([value, mintTokenAddress, owner, secret]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute nullifier for spending a UTXO
|
|
37
|
+
* nullifier = Poseidon(nullifierKey, commitment, leafIndex)
|
|
38
|
+
*/
|
|
39
|
+
export function computeNullifier(
|
|
40
|
+
spendingKey: bigint,
|
|
41
|
+
commitment: bigint,
|
|
42
|
+
leafIndex: number
|
|
43
|
+
): bigint {
|
|
44
|
+
const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
|
|
45
|
+
return poseidonHash([nullifierKey, commitment, BigInt(leafIndex)]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute nullifier from wallet keys
|
|
50
|
+
*/
|
|
51
|
+
export function computeNullifierFromKeys(
|
|
52
|
+
keys: WalletKeys,
|
|
53
|
+
commitment: bigint,
|
|
54
|
+
leafIndex: number
|
|
55
|
+
): bigint {
|
|
56
|
+
return poseidonHash([keys.nullifierKey, commitment, BigInt(leafIndex)]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// UTXO Creation
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a new UTXO with random secret
|
|
65
|
+
*/
|
|
66
|
+
export function createUTXO(
|
|
67
|
+
value: bigint,
|
|
68
|
+
mintTokenAddress: bigint,
|
|
69
|
+
owner: bigint,
|
|
70
|
+
leafIndex: number,
|
|
71
|
+
treeIndex: number = 0
|
|
72
|
+
): UTXO {
|
|
73
|
+
const secret = generateSpendingKey(); // Random blinding factor
|
|
74
|
+
const commitment = computeCommitment(value, mintTokenAddress, owner, secret);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
value,
|
|
78
|
+
mintTokenAddress,
|
|
79
|
+
owner,
|
|
80
|
+
secret,
|
|
81
|
+
commitment,
|
|
82
|
+
leafIndex,
|
|
83
|
+
treeIndex,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a dummy (zero) UTXO for padding
|
|
89
|
+
*/
|
|
90
|
+
export function createDummyUTXO(): UTXO {
|
|
91
|
+
return {
|
|
92
|
+
value: 0n,
|
|
93
|
+
mintTokenAddress: 0n,
|
|
94
|
+
owner: 0n,
|
|
95
|
+
secret: 0n,
|
|
96
|
+
commitment: 0n,
|
|
97
|
+
leafIndex: 0,
|
|
98
|
+
treeIndex: 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// UTXO Serialization
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Serialize UTXO for storage
|
|
108
|
+
*/
|
|
109
|
+
export function serializeUTXO(
|
|
110
|
+
utxo: UTXO,
|
|
111
|
+
mint: string,
|
|
112
|
+
createdTx: string,
|
|
113
|
+
spent: boolean = false,
|
|
114
|
+
spentTx?: string
|
|
115
|
+
): SerializableUTXO {
|
|
116
|
+
return {
|
|
117
|
+
value: utxo.value.toString(),
|
|
118
|
+
mintTokenAddress: utxo.mintTokenAddress.toString(),
|
|
119
|
+
owner: utxo.owner.toString(),
|
|
120
|
+
secret: utxo.secret.toString(),
|
|
121
|
+
commitment: utxo.commitment.toString(),
|
|
122
|
+
leafIndex: utxo.leafIndex,
|
|
123
|
+
treeIndex: utxo.treeIndex,
|
|
124
|
+
mint,
|
|
125
|
+
spent,
|
|
126
|
+
spentTx,
|
|
127
|
+
createdTx,
|
|
128
|
+
createdAt: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Deserialize UTXO from storage
|
|
134
|
+
*/
|
|
135
|
+
export function deserializeUTXO(data: SerializableUTXO): UTXO {
|
|
136
|
+
return {
|
|
137
|
+
value: BigInt(data.value),
|
|
138
|
+
mintTokenAddress: BigInt(data.mintTokenAddress),
|
|
139
|
+
owner: BigInt(data.owner),
|
|
140
|
+
secret: BigInt(data.secret),
|
|
141
|
+
commitment: BigInt(data.commitment),
|
|
142
|
+
leafIndex: data.leafIndex,
|
|
143
|
+
treeIndex: data.treeIndex,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// UTXO Scanning
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Scan encrypted notes for UTXOs belonging to this wallet
|
|
153
|
+
*
|
|
154
|
+
* This attempts to decrypt each note with the wallet's encryption key.
|
|
155
|
+
* If successful, the note belongs to this wallet.
|
|
156
|
+
*/
|
|
157
|
+
export function scanNotesForUTXOs(
|
|
158
|
+
notes: Array<{
|
|
159
|
+
encryptedData: string;
|
|
160
|
+
treeIndex: number;
|
|
161
|
+
mint: string;
|
|
162
|
+
txSignature: string;
|
|
163
|
+
}>,
|
|
164
|
+
walletKeys: WalletKeys,
|
|
165
|
+
commitments: Map<number, Map<number, bigint>> // treeIndex -> leafIndex -> commitment
|
|
166
|
+
): SerializableUTXO[] {
|
|
167
|
+
const foundUTXOs: SerializableUTXO[] = [];
|
|
168
|
+
|
|
169
|
+
for (const note of notes) {
|
|
170
|
+
try {
|
|
171
|
+
// Parse encrypted note from hex
|
|
172
|
+
const noteBytes = hexToBytes(note.encryptedData);
|
|
173
|
+
const encryptedNote = deserializeEncryptedNote(noteBytes);
|
|
174
|
+
|
|
175
|
+
// Try to decrypt
|
|
176
|
+
const decrypted = decryptNote(encryptedNote, walletKeys.encryptionKeyPair.secretKey);
|
|
177
|
+
if (!decrypted) continue;
|
|
178
|
+
|
|
179
|
+
// Successfully decrypted - this is our UTXO
|
|
180
|
+
const commitment = computeCommitment(
|
|
181
|
+
decrypted.value,
|
|
182
|
+
decrypted.mintTokenAddress,
|
|
183
|
+
walletKeys.owner,
|
|
184
|
+
decrypted.secret
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Verify commitment exists on-chain (optional but recommended)
|
|
188
|
+
const treeCommitments = commitments.get(note.treeIndex);
|
|
189
|
+
if (treeCommitments) {
|
|
190
|
+
const onChainCommitment = treeCommitments.get(decrypted.leafIndex);
|
|
191
|
+
if (onChainCommitment !== undefined && onChainCommitment !== commitment) {
|
|
192
|
+
console.warn(`Commitment mismatch for leaf ${decrypted.leafIndex} in tree ${note.treeIndex}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
foundUTXOs.push({
|
|
198
|
+
value: decrypted.value.toString(),
|
|
199
|
+
mintTokenAddress: decrypted.mintTokenAddress.toString(),
|
|
200
|
+
owner: walletKeys.owner.toString(),
|
|
201
|
+
secret: decrypted.secret.toString(),
|
|
202
|
+
commitment: commitment.toString(),
|
|
203
|
+
leafIndex: decrypted.leafIndex,
|
|
204
|
+
treeIndex: note.treeIndex,
|
|
205
|
+
mint: note.mint,
|
|
206
|
+
spent: false,
|
|
207
|
+
createdTx: note.txSignature,
|
|
208
|
+
createdAt: new Date().toISOString(),
|
|
209
|
+
});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
// Failed to decrypt - not our note, skip
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return foundUTXOs;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Sync wallet UTXOs from relayer
|
|
220
|
+
*
|
|
221
|
+
* This fetches all encrypted notes and attempts to decrypt them.
|
|
222
|
+
* Also checks which UTXOs have been spent.
|
|
223
|
+
*/
|
|
224
|
+
export async function syncUTXOs(
|
|
225
|
+
walletKeys: WalletKeys,
|
|
226
|
+
existingUTXOs: SerializableUTXO[],
|
|
227
|
+
relayerUrl?: string,
|
|
228
|
+
afterSlot?: string
|
|
229
|
+
): Promise<{
|
|
230
|
+
newUTXOs: SerializableUTXO[];
|
|
231
|
+
spentUTXOs: string[]; // IDs of UTXOs that are now spent
|
|
232
|
+
}> {
|
|
233
|
+
const client = getRelayerClient(relayerUrl);
|
|
234
|
+
|
|
235
|
+
// Fetch all notes since last sync
|
|
236
|
+
const notes = await client.getAllNotes(afterSlot);
|
|
237
|
+
|
|
238
|
+
// Build commitment map for verification (optional)
|
|
239
|
+
const pool = await client.getPool();
|
|
240
|
+
const commitments = new Map<number, Map<number, bigint>>();
|
|
241
|
+
|
|
242
|
+
// For each tree, fetch commitments
|
|
243
|
+
for (let treeIndex = 0; treeIndex <= pool.currentTreeIndex; treeIndex++) {
|
|
244
|
+
const leaves = await client.getCommitmentsForTree(treeIndex);
|
|
245
|
+
const treeMap = new Map<number, bigint>();
|
|
246
|
+
leaves.forEach((commitment, index) => {
|
|
247
|
+
treeMap.set(index, commitment);
|
|
248
|
+
});
|
|
249
|
+
commitments.set(treeIndex, treeMap);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Scan notes for new UTXOs
|
|
253
|
+
const newUTXOs = scanNotesForUTXOs(
|
|
254
|
+
notes.map(n => ({
|
|
255
|
+
encryptedData: n.encryptedData,
|
|
256
|
+
treeIndex: n.treeIndex,
|
|
257
|
+
mint: n.mint,
|
|
258
|
+
txSignature: n.txSignature,
|
|
259
|
+
})),
|
|
260
|
+
walletKeys,
|
|
261
|
+
commitments
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Check which existing UTXOs have been spent
|
|
265
|
+
const unspentUTXOs = existingUTXOs.filter(u => !u.spent);
|
|
266
|
+
const nullifiersToCheck: string[] = [];
|
|
267
|
+
const utxoByNullifier = new Map<string, string>();
|
|
268
|
+
|
|
269
|
+
for (const utxo of unspentUTXOs) {
|
|
270
|
+
const commitment = BigInt(utxo.commitment);
|
|
271
|
+
const nullifier = computeNullifier(
|
|
272
|
+
walletKeys.spendingKey,
|
|
273
|
+
commitment,
|
|
274
|
+
utxo.leafIndex
|
|
275
|
+
);
|
|
276
|
+
const nullifierStr = nullifier.toString();
|
|
277
|
+
nullifiersToCheck.push(nullifierStr);
|
|
278
|
+
utxoByNullifier.set(nullifierStr, utxo.commitment); // Use commitment as ID
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const spentUTXOs: string[] = [];
|
|
282
|
+
|
|
283
|
+
if (nullifiersToCheck.length > 0) {
|
|
284
|
+
const batchSize = 50;
|
|
285
|
+
for (let i = 0; i < nullifiersToCheck.length; i += batchSize) {
|
|
286
|
+
const batch = nullifiersToCheck.slice(i, i + batchSize);
|
|
287
|
+
const result = await client.checkNullifiersBatch(batch);
|
|
288
|
+
|
|
289
|
+
for (const r of result.results) {
|
|
290
|
+
if (r.spent) {
|
|
291
|
+
const utxoId = utxoByNullifier.get(r.nullifier);
|
|
292
|
+
if (utxoId) {
|
|
293
|
+
spentUTXOs.push(utxoId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { newUTXOs, spentUTXOs };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// UTXO Selection
|
|
305
|
+
// ============================================================================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Select UTXOs for spending (greedy algorithm)
|
|
309
|
+
*
|
|
310
|
+
* Selects the largest UTXOs first until the target amount is reached.
|
|
311
|
+
*/
|
|
312
|
+
export function selectUTXOs(
|
|
313
|
+
utxos: UTXO[],
|
|
314
|
+
targetAmount: bigint,
|
|
315
|
+
mint?: bigint
|
|
316
|
+
): UTXO[] {
|
|
317
|
+
// Filter by mint if specified
|
|
318
|
+
const filtered = mint !== undefined
|
|
319
|
+
? utxos.filter(u => u.mintTokenAddress === mint)
|
|
320
|
+
: utxos;
|
|
321
|
+
|
|
322
|
+
// Sort by value descending
|
|
323
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
324
|
+
if (a.value > b.value) return -1;
|
|
325
|
+
if (a.value < b.value) return 1;
|
|
326
|
+
return 0;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const selected: UTXO[] = [];
|
|
330
|
+
let total = 0n;
|
|
331
|
+
|
|
332
|
+
for (const utxo of sorted) {
|
|
333
|
+
if (total >= targetAmount) break;
|
|
334
|
+
selected.push(utxo);
|
|
335
|
+
total += utxo.value;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (total < targetAmount) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Insufficient balance. Need ${targetAmount}, have ${total}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return selected;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Calculate total balance of UTXOs
|
|
349
|
+
*/
|
|
350
|
+
export function calculateBalance(utxos: UTXO[], mint?: bigint): bigint {
|
|
351
|
+
const filtered = mint !== undefined
|
|
352
|
+
? utxos.filter(u => u.mintTokenAddress === mint)
|
|
353
|
+
: utxos;
|
|
354
|
+
|
|
355
|
+
return filtered.reduce((sum, u) => sum + u.value, 0n);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|