@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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Constants for Hula Privacy Protocol
3
+ */
4
+
5
+ import { PublicKey } from "@solana/web3.js";
6
+
7
+ // ============================================================================
8
+ // Program Constants
9
+ // ============================================================================
10
+
11
+ /** Hula Privacy Program ID */
12
+ export const PROGRAM_ID = new PublicKey("tnJ9AAxNau3BRBTe7NzXpv8DeYyiogvGd8YPnCiuarA");
13
+
14
+ /** Token 2022 Program ID */
15
+ export const TOKEN_2022_PROGRAM_ID = new PublicKey(
16
+ "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
17
+ );
18
+
19
+ // ============================================================================
20
+ // PDA Seeds
21
+ // ============================================================================
22
+
23
+ export const POOL_SEED = Buffer.from("hula_pool");
24
+ export const VAULT_SEED = Buffer.from("vault");
25
+ export const MERKLE_TREE_SEED = Buffer.from("merkle_tree");
26
+ export const NULLIFIER_SEED = Buffer.from("nullifier");
27
+
28
+ // ============================================================================
29
+ // Circuit Parameters
30
+ // ============================================================================
31
+
32
+ /** Number of input UTXOs in circuit */
33
+ export const NUM_INPUT_UTXOS = 2;
34
+
35
+ /** Number of output UTXOs in circuit */
36
+ export const NUM_OUTPUT_UTXOS = 2;
37
+
38
+ /** Depth of merkle tree */
39
+ export const MERKLE_TREE_DEPTH = 10;
40
+
41
+ /** Maximum leaves per tree (2^DEPTH) */
42
+ export const MAX_LEAVES = 1 << MERKLE_TREE_DEPTH;
43
+
44
+ /** Size of proof in bytes */
45
+ export const PROOF_SIZE = 256;
46
+
47
+ // ============================================================================
48
+ // Domain Separators (matching circuits/keys.circom)
49
+ // ============================================================================
50
+
51
+ export const DOMAIN_OWNER = 0n;
52
+ export const DOMAIN_VIEWING = 1n;
53
+ export const DOMAIN_NULLIFIER = 2n;
54
+ export const DOMAIN_ENCRYPTION = 3n;
55
+
56
+ // ============================================================================
57
+ // Crypto Constants
58
+ // ============================================================================
59
+
60
+ /** BN254 field prime */
61
+ export const FIELD_PRIME = BigInt(
62
+ "21888242871839275222246405745257275088548364400416034343698204186575808495617"
63
+ );
64
+
65
+ // ============================================================================
66
+ // PDA Derivation Functions
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Derive Pool PDA
71
+ */
72
+ export function getPoolPDA(): [PublicKey, number] {
73
+ return PublicKey.findProgramAddressSync([POOL_SEED], PROGRAM_ID);
74
+ }
75
+
76
+ /**
77
+ * Derive Merkle Tree PDA for a specific tree index
78
+ */
79
+ export function getMerkleTreePDA(treeIndex: number = 0): [PublicKey, number] {
80
+ const treeIndexBuffer = Buffer.alloc(4);
81
+ treeIndexBuffer.writeUInt32LE(treeIndex, 0);
82
+ return PublicKey.findProgramAddressSync(
83
+ [MERKLE_TREE_SEED, treeIndexBuffer],
84
+ PROGRAM_ID
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Derive Vault PDA for a specific mint
90
+ */
91
+ export function getVaultPDA(mint: PublicKey): [PublicKey, number] {
92
+ return PublicKey.findProgramAddressSync(
93
+ [VAULT_SEED, mint.toBytes()],
94
+ PROGRAM_ID
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Derive Nullifier PDA for a nullifier hash
100
+ */
101
+ export function getNullifierPDA(nullifier: Uint8Array): [PublicKey, number] {
102
+ return PublicKey.findProgramAddressSync(
103
+ [NULLIFIER_SEED, nullifier],
104
+ PROGRAM_ID
105
+ );
106
+ }
107
+
108
+
package/src/crypto.ts ADDED
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Cryptographic utilities for Hula Privacy Protocol
3
+ *
4
+ * Includes Poseidon hashing, key derivation, and encryption
5
+ */
6
+
7
+ import { buildPoseidon, type Poseidon } from "circomlibjs";
8
+ import * as nacl from "tweetnacl";
9
+ import { sha256 } from "@noble/hashes/sha256";
10
+ import {
11
+ DOMAIN_OWNER,
12
+ DOMAIN_VIEWING,
13
+ DOMAIN_NULLIFIER,
14
+ DOMAIN_ENCRYPTION,
15
+ FIELD_PRIME,
16
+ } from "./constants";
17
+ import type { WalletKeys, EncryptedNote } from "./types";
18
+
19
+ // ============================================================================
20
+ // Poseidon Hasher
21
+ // ============================================================================
22
+
23
+ let poseidonInstance: Poseidon | null = null;
24
+
25
+ /**
26
+ * Initialize Poseidon hasher (must be called once before using hash functions)
27
+ */
28
+ export async function initPoseidon(): Promise<void> {
29
+ if (!poseidonInstance) {
30
+ poseidonInstance = await buildPoseidon();
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Check if Poseidon is initialized
36
+ */
37
+ export function isPoseidonInitialized(): boolean {
38
+ return poseidonInstance !== null;
39
+ }
40
+
41
+ /**
42
+ * Get Poseidon instance (throws if not initialized)
43
+ */
44
+ export function getPoseidon(): Poseidon {
45
+ if (!poseidonInstance) {
46
+ throw new Error("Poseidon not initialized. Call initPoseidon() first.");
47
+ }
48
+ return poseidonInstance;
49
+ }
50
+
51
+ /**
52
+ * Poseidon hash of inputs
53
+ */
54
+ export function poseidonHash(inputs: bigint[]): bigint {
55
+ const poseidon = getPoseidon();
56
+ const hash = poseidon(inputs.map(x => poseidon.F.e(x)));
57
+ return poseidon.F.toObject(hash);
58
+ }
59
+
60
+ // ============================================================================
61
+ // Byte Conversion Utilities
62
+ // ============================================================================
63
+
64
+ /**
65
+ * Convert bytes to bigint (big-endian)
66
+ */
67
+ export function bytesToBigInt(bytes: Uint8Array): bigint {
68
+ let result = 0n;
69
+ for (let i = 0; i < bytes.length; i++) {
70
+ result = (result << 8n) + BigInt(bytes[i]);
71
+ }
72
+ return result;
73
+ }
74
+
75
+ /**
76
+ * Convert bigint to bytes (big-endian)
77
+ */
78
+ export function bigIntToBytes(value: bigint, length: number): Uint8Array {
79
+ const bytes = new Uint8Array(length);
80
+ let v = value;
81
+ for (let i = length - 1; i >= 0; i--) {
82
+ bytes[i] = Number(v & 0xffn);
83
+ v >>= 8n;
84
+ }
85
+ return bytes;
86
+ }
87
+
88
+ /**
89
+ * Convert bigint to 32-byte array (big-endian)
90
+ */
91
+ export function bigIntToBytes32(value: bigint): Uint8Array {
92
+ return bigIntToBytes(value, 32);
93
+ }
94
+
95
+ /**
96
+ * Convert hex string to bytes
97
+ */
98
+ export function hexToBytes(hex: string): Uint8Array {
99
+ const cleaned = hex.startsWith("0x") ? hex.slice(2) : hex;
100
+ const bytes = new Uint8Array(cleaned.length / 2);
101
+ for (let i = 0; i < bytes.length; i++) {
102
+ bytes[i] = parseInt(cleaned.substr(i * 2, 2), 16);
103
+ }
104
+ return bytes;
105
+ }
106
+
107
+ /**
108
+ * Convert bytes to hex string
109
+ */
110
+ export function bytesToHex(bytes: Uint8Array): string {
111
+ return Array.from(bytes)
112
+ .map(b => b.toString(16).padStart(2, "0"))
113
+ .join("");
114
+ }
115
+
116
+ /**
117
+ * Convert PublicKey to bigint
118
+ */
119
+ export function pubkeyToBigInt(pubkey: { toBytes(): Uint8Array }): bigint {
120
+ return bytesToBigInt(pubkey.toBytes());
121
+ }
122
+
123
+ // ============================================================================
124
+ // Key Generation & Derivation
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Generate a new random spending key
129
+ */
130
+ export function generateSpendingKey(): bigint {
131
+ const randomBytes = nacl.randomBytes(32);
132
+ return bytesToBigInt(randomBytes) % (2n ** 253n);
133
+ }
134
+
135
+ /**
136
+ * Derive all wallet keys from spending key
137
+ */
138
+ export function deriveKeys(spendingKey: bigint): WalletKeys {
139
+ const owner = poseidonHash([spendingKey, DOMAIN_OWNER]);
140
+ const viewingKey = poseidonHash([spendingKey, DOMAIN_VIEWING]);
141
+ const nullifierKey = poseidonHash([spendingKey, DOMAIN_NULLIFIER]);
142
+ const encryptionSeed = poseidonHash([spendingKey, DOMAIN_ENCRYPTION]);
143
+
144
+ // Derive X25519 keypair from encryption seed
145
+ const seedBytes = bigIntToBytes(encryptionSeed, 32);
146
+ const hashedSeed = sha256(seedBytes);
147
+ const encryptionKeyPair = nacl.box.keyPair.fromSecretKey(hashedSeed);
148
+
149
+ return {
150
+ spendingKey,
151
+ owner,
152
+ viewingKey,
153
+ nullifierKey,
154
+ encryptionKeyPair,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Derive spending key from wallet signature
160
+ *
161
+ * This allows deterministic key derivation from a Solana wallet signature,
162
+ * enabling wallet recovery as long as the user has access to their wallet.
163
+ */
164
+ export function deriveSpendingKeyFromSignature(signature: Uint8Array): bigint {
165
+ if (signature.length < 64) {
166
+ throw new Error("Signature must be at least 64 bytes");
167
+ }
168
+
169
+ // Use SHA-256 of the signature to derive the spending key
170
+ const hash = sha256(signature);
171
+ return bytesToBigInt(hash) % FIELD_PRIME;
172
+ }
173
+
174
+ // ============================================================================
175
+ // Note Encryption
176
+ // ============================================================================
177
+
178
+ /**
179
+ * Encrypt a note for a recipient
180
+ *
181
+ * The note contains the UTXO data needed for the recipient to claim it.
182
+ */
183
+ export function encryptNote(
184
+ noteData: {
185
+ value: bigint;
186
+ mintTokenAddress: bigint;
187
+ secret: bigint;
188
+ leafIndex: number;
189
+ },
190
+ recipientEncryptionPubKey: Uint8Array
191
+ ): EncryptedNote {
192
+ const payload = {
193
+ value: noteData.value.toString(),
194
+ mintTokenAddress: noteData.mintTokenAddress.toString(),
195
+ secret: noteData.secret.toString(),
196
+ leafIndex: noteData.leafIndex,
197
+ };
198
+
199
+ const message = new TextEncoder().encode(JSON.stringify(payload));
200
+ const nonce = nacl.randomBytes(24);
201
+ const ephemeralKeyPair = nacl.box.keyPair();
202
+
203
+ const ciphertext = nacl.box(
204
+ message,
205
+ nonce,
206
+ recipientEncryptionPubKey,
207
+ ephemeralKeyPair.secretKey
208
+ );
209
+
210
+ return {
211
+ ephemeralPubKey: ephemeralKeyPair.publicKey,
212
+ ciphertext,
213
+ nonce,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Decrypt an encrypted note
219
+ *
220
+ * Returns null if decryption fails (not intended for this recipient)
221
+ */
222
+ export function decryptNote(
223
+ encryptedNote: EncryptedNote,
224
+ encryptionSecretKey: Uint8Array
225
+ ): { value: bigint; mintTokenAddress: bigint; secret: bigint; leafIndex: number } | null {
226
+ try {
227
+ const decrypted = nacl.box.open(
228
+ encryptedNote.ciphertext,
229
+ encryptedNote.nonce,
230
+ encryptedNote.ephemeralPubKey,
231
+ encryptionSecretKey
232
+ );
233
+
234
+ if (!decrypted) return null;
235
+
236
+ const noteData = JSON.parse(new TextDecoder().decode(decrypted));
237
+ return {
238
+ value: BigInt(noteData.value),
239
+ mintTokenAddress: BigInt(noteData.mintTokenAddress),
240
+ secret: BigInt(noteData.secret),
241
+ leafIndex: noteData.leafIndex,
242
+ };
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Serialize encrypted note for on-chain storage
250
+ */
251
+ export function serializeEncryptedNote(note: EncryptedNote): Uint8Array {
252
+ const totalLength = 32 + 24 + 2 + note.ciphertext.length;
253
+ const buffer = new Uint8Array(totalLength);
254
+
255
+ buffer.set(note.ephemeralPubKey, 0);
256
+ buffer.set(note.nonce, 32);
257
+
258
+ // Ciphertext length (2 bytes, big-endian)
259
+ buffer[56] = (note.ciphertext.length >> 8) & 0xff;
260
+ buffer[57] = note.ciphertext.length & 0xff;
261
+
262
+ buffer.set(note.ciphertext, 58);
263
+
264
+ return buffer;
265
+ }
266
+
267
+ /**
268
+ * Deserialize encrypted note from on-chain data
269
+ */
270
+ export function deserializeEncryptedNote(data: Uint8Array): EncryptedNote {
271
+ const ephemeralPubKey = data.slice(0, 32);
272
+ const nonce = data.slice(32, 56);
273
+ const ciphertextLength = (data[56] << 8) | data[57];
274
+ const ciphertext = data.slice(58, 58 + ciphertextLength);
275
+
276
+ return { ephemeralPubKey, nonce, ciphertext };
277
+ }
278
+
279
+ // ============================================================================
280
+ // Formatting Utilities
281
+ // ============================================================================
282
+
283
+ /**
284
+ * Format a commitment/nullifier for display (truncated hex)
285
+ */
286
+ export function formatCommitment(value: bigint | Uint8Array, length: number = 16): string {
287
+ if (value instanceof Uint8Array) {
288
+ return `0x${bytesToHex(value).slice(0, length)}...`;
289
+ }
290
+ return `0x${value.toString(16).padStart(64, "0").slice(0, length)}...`;
291
+ }
292
+
293
+
package/src/index.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Hula Privacy SDK
3
+ *
4
+ * Complete toolkit for privacy transactions on Solana.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { HulaWallet, initHulaSDK } from '@hula/sdk';
9
+ *
10
+ * // Initialize SDK
11
+ * await initHulaSDK();
12
+ *
13
+ * // Create wallet from Phantom signature
14
+ * const signature = await phantom.signMessage(getKeyDerivationMessage());
15
+ * const wallet = await HulaWallet.fromSignature(signature, {
16
+ * rpcUrl: 'https://api.devnet.solana.com',
17
+ * relayerUrl: 'https://relayer.hulaprivacy.io',
18
+ * });
19
+ *
20
+ * // Sync wallet to find your UTXOs
21
+ * await wallet.sync();
22
+ *
23
+ * // Check balance
24
+ * const balance = wallet.getBalance(mintAddress);
25
+ * console.log('Private balance:', balance);
26
+ *
27
+ * // Deposit tokens
28
+ * const depositTx = await wallet.deposit(mintAddress, 1000000n);
29
+ *
30
+ * // Transfer privately
31
+ * const transferTx = await wallet.transfer(
32
+ * mintAddress,
33
+ * 500000n,
34
+ * recipientOwnerHash,
35
+ * recipientEncryptionPubKey
36
+ * );
37
+ *
38
+ * // Withdraw to public address
39
+ * const withdrawTx = await wallet.withdraw(
40
+ * mintAddress,
41
+ * 200000n,
42
+ * recipientPublicKey
43
+ * );
44
+ * ```
45
+ */
46
+
47
+ // ============================================================================
48
+ // Main Exports
49
+ // ============================================================================
50
+
51
+ // Wallet
52
+ export { HulaWallet, initHulaSDK, getKeyDerivationMessage } from "./wallet";
53
+
54
+ // Relayer API
55
+ export { RelayerClient, getRelayerClient, setDefaultRelayerUrl } from "./api";
56
+
57
+ // Crypto
58
+ export {
59
+ initPoseidon,
60
+ isPoseidonInitialized,
61
+ getPoseidon,
62
+ poseidonHash,
63
+ deriveKeys,
64
+ generateSpendingKey,
65
+ deriveSpendingKeyFromSignature,
66
+ encryptNote,
67
+ decryptNote,
68
+ serializeEncryptedNote,
69
+ deserializeEncryptedNote,
70
+ bytesToBigInt,
71
+ bigIntToBytes,
72
+ bigIntToBytes32,
73
+ bytesToHex,
74
+ hexToBytes,
75
+ pubkeyToBigInt,
76
+ formatCommitment,
77
+ } from "./crypto";
78
+
79
+ // UTXO
80
+ export {
81
+ computeCommitment,
82
+ computeNullifier,
83
+ computeNullifierFromKeys,
84
+ createUTXO,
85
+ createDummyUTXO,
86
+ serializeUTXO,
87
+ deserializeUTXO,
88
+ scanNotesForUTXOs,
89
+ syncUTXOs,
90
+ selectUTXOs,
91
+ calculateBalance,
92
+ } from "./utxo";
93
+
94
+ // Merkle
95
+ export {
96
+ computeZeros,
97
+ getZeroAtLevel,
98
+ getEmptyTreeRoot,
99
+ computeMerklePathFromLeaves,
100
+ computeMerkleRoot,
101
+ verifyMerklePath,
102
+ fetchMerklePath,
103
+ fetchMerkleRoot,
104
+ getNextLeafIndex,
105
+ getCurrentTreeIndex,
106
+ } from "./merkle";
107
+
108
+ // Proof
109
+ export {
110
+ setCircuitPaths,
111
+ getCircuitPaths,
112
+ verifyCircuitFiles,
113
+ parseProof,
114
+ generateProof,
115
+ generateProofInMemory,
116
+ } from "./proof";
117
+
118
+ // Transaction
119
+ export {
120
+ buildTransaction,
121
+ buildTransactionAccounts,
122
+ toAnchorPublicInputs,
123
+ } from "./transaction";
124
+
125
+ // Constants
126
+ export {
127
+ PROGRAM_ID,
128
+ TOKEN_2022_PROGRAM_ID,
129
+ POOL_SEED,
130
+ VAULT_SEED,
131
+ MERKLE_TREE_SEED,
132
+ NULLIFIER_SEED,
133
+ NUM_INPUT_UTXOS,
134
+ NUM_OUTPUT_UTXOS,
135
+ MERKLE_TREE_DEPTH,
136
+ MAX_LEAVES,
137
+ PROOF_SIZE,
138
+ DOMAIN_OWNER,
139
+ DOMAIN_VIEWING,
140
+ DOMAIN_NULLIFIER,
141
+ DOMAIN_ENCRYPTION,
142
+ FIELD_PRIME,
143
+ getPoolPDA,
144
+ getMerkleTreePDA,
145
+ getVaultPDA,
146
+ getNullifierPDA,
147
+ } from "./constants";
148
+
149
+ // ============================================================================
150
+ // Type Exports
151
+ // ============================================================================
152
+
153
+ export type {
154
+ // Core types
155
+ UTXO,
156
+ SerializableUTXO,
157
+ WalletKeys,
158
+ EncryptedNote,
159
+
160
+ // Merkle types
161
+ MerklePath,
162
+ LeafData,
163
+
164
+ // Transaction types
165
+ CircuitInputs,
166
+ PublicInputs,
167
+ OutputSpec,
168
+ TransactionRequest,
169
+ BuiltTransaction,
170
+
171
+ // API types
172
+ PaginatedResponse,
173
+ PoolData,
174
+ TreeData,
175
+ TransactionData,
176
+ NoteData,
177
+ StatsData,
178
+
179
+ // Config types
180
+ HulaSDKConfig,
181
+ SyncProgress,
182
+ SyncResult,
183
+ } from "./types";
184
+
185
+