@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 ADDED
@@ -0,0 +1,290 @@
1
+ # Hula Privacy SDK
2
+
3
+ Complete toolkit for privacy transactions on Solana using ZK proofs and UTXOs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @hula/sdk
9
+ # or
10
+ bun add @hula/sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { HulaWallet, initHulaSDK, getKeyDerivationMessage } from '@hula/sdk';
17
+
18
+ // Initialize SDK (loads Poseidon hasher)
19
+ await initHulaSDK();
20
+
21
+ // Create wallet from Phantom signature (deterministic derivation)
22
+ const message = getKeyDerivationMessage();
23
+ const signature = await wallet.signMessage(message);
24
+ const hulaWallet = await HulaWallet.fromSignature(signature, {
25
+ rpcUrl: 'https://api.devnet.solana.com',
26
+ relayerUrl: 'http://localhost:3001',
27
+ });
28
+
29
+ // Or create a new wallet with random keys
30
+ const newWallet = await HulaWallet.create({
31
+ rpcUrl: 'https://api.devnet.solana.com',
32
+ relayerUrl: 'http://localhost:3001',
33
+ });
34
+
35
+ // Get wallet public identifier
36
+ console.log('Owner hash:', hulaWallet.ownerHex);
37
+ console.log('Encryption pubkey:', Buffer.from(hulaWallet.encryptionPublicKey).toString('hex'));
38
+ ```
39
+
40
+ ## Syncing UTXOs
41
+
42
+ The wallet syncs with the relayer to discover your UTXOs from encrypted notes:
43
+
44
+ ```typescript
45
+ // Sync wallet to find your UTXOs
46
+ const result = await hulaWallet.sync((progress) => {
47
+ console.log(`Syncing: ${progress.stage} ${progress.current}/${progress.total}`);
48
+ });
49
+
50
+ console.log(`Found ${result.newUtxos} new UTXOs`);
51
+ console.log(`Marked ${result.spentUtxos} UTXOs as spent`);
52
+
53
+ // Check balance
54
+ const balance = hulaWallet.getBalance(mintAddress);
55
+ console.log('Private balance:', balance);
56
+ ```
57
+
58
+ ## Transactions
59
+
60
+ ### Deposit (Public → Private)
61
+
62
+ ```typescript
63
+ const { transaction } = await hulaWallet.deposit(mintAddress, 1_000_000n);
64
+
65
+ // Submit transaction using your preferred method
66
+ // The transaction object contains proof, public inputs, etc.
67
+ ```
68
+
69
+ ### Transfer (Private → Private)
70
+
71
+ ```typescript
72
+ // Get recipient's public info
73
+ const recipientOwner = BigInt('0x...'); // Recipient's owner hash
74
+ const recipientEncPubKey = new Uint8Array([...]); // Recipient's encryption pubkey
75
+
76
+ const { transaction } = await hulaWallet.transfer(
77
+ mintAddress,
78
+ 500_000n,
79
+ recipientOwner,
80
+ recipientEncPubKey
81
+ );
82
+ ```
83
+
84
+ ### Withdraw (Private → Public)
85
+
86
+ ```typescript
87
+ const recipientPubkey = new PublicKey('...');
88
+
89
+ const { transaction } = await hulaWallet.withdraw(
90
+ mintAddress,
91
+ 200_000n,
92
+ recipientPubkey
93
+ );
94
+ ```
95
+
96
+ ## Low-Level API
97
+
98
+ For more control, use the low-level functions directly:
99
+
100
+ ### Key Derivation
101
+
102
+ ```typescript
103
+ import { generateSpendingKey, deriveKeys, initPoseidon } from '@hula/sdk';
104
+
105
+ await initPoseidon();
106
+
107
+ const spendingKey = generateSpendingKey();
108
+ const keys = deriveKeys(spendingKey);
109
+
110
+ console.log('Owner:', keys.owner.toString(16));
111
+ console.log('Viewing key:', keys.viewingKey.toString(16));
112
+ console.log('Encryption pubkey:', Buffer.from(keys.encryptionKeyPair.publicKey).toString('hex'));
113
+ ```
114
+
115
+ ### UTXO Management
116
+
117
+ ```typescript
118
+ import { createUTXO, computeCommitment, computeNullifier } from '@hula/sdk';
119
+
120
+ // Create a UTXO
121
+ const utxo = createUTXO(
122
+ 1_000_000n, // value
123
+ mintBigInt, // mint address as bigint
124
+ keys.owner, // owner hash
125
+ 0, // leaf index
126
+ 0 // tree index
127
+ );
128
+
129
+ // Compute commitment manually
130
+ const commitment = computeCommitment(
131
+ utxo.value,
132
+ utxo.mintTokenAddress,
133
+ utxo.owner,
134
+ utxo.secret
135
+ );
136
+
137
+ // Compute nullifier for spending
138
+ const nullifier = computeNullifier(
139
+ keys.spendingKey,
140
+ utxo.commitment,
141
+ utxo.leafIndex
142
+ );
143
+ ```
144
+
145
+ ### Merkle Tree Operations
146
+
147
+ ```typescript
148
+ import {
149
+ fetchMerklePath,
150
+ fetchMerkleRoot,
151
+ computeMerklePathFromLeaves,
152
+ getCurrentTreeIndex
153
+ } from '@hula/sdk';
154
+
155
+ // Get current tree index
156
+ const treeIndex = await getCurrentTreeIndex('http://localhost:3001');
157
+
158
+ // Fetch merkle root
159
+ const root = await fetchMerkleRoot(treeIndex);
160
+
161
+ // Fetch merkle path for a specific leaf
162
+ const path = await fetchMerklePath(treeIndex, leafIndex);
163
+
164
+ // Or compute locally from leaves
165
+ const leaves = [commitment1, commitment2, ...];
166
+ const localPath = computeMerklePathFromLeaves(leafIndex, leaves);
167
+ ```
168
+
169
+ ### Relayer API
170
+
171
+ ```typescript
172
+ import { RelayerClient, getRelayerClient } from '@hula/sdk';
173
+
174
+ const client = getRelayerClient('http://localhost:3001');
175
+
176
+ // Get pool state
177
+ const pool = await client.getPool();
178
+ console.log('Current tree:', pool.currentTreeIndex);
179
+ console.log('Total commitments:', pool.commitmentCount);
180
+
181
+ // Get leaves for a tree
182
+ const leaves = await client.getAllLeavesForTree(0);
183
+
184
+ // Check if nullifier is spent
185
+ const { spent } = await client.checkNullifier(nullifierHex);
186
+
187
+ // Get encrypted notes
188
+ const notes = await client.getAllNotes();
189
+ ```
190
+
191
+ ### Proof Generation
192
+
193
+ ```typescript
194
+ import { generateProof, setCircuitPaths } from '@hula/sdk';
195
+
196
+ // Set circuit paths (if not in default locations)
197
+ setCircuitPaths(
198
+ '/path/to/transaction.wasm',
199
+ '/path/to/transaction_final.zkey'
200
+ );
201
+
202
+ // Generate proof
203
+ const { proof, publicSignals } = await generateProof(circuitInputs);
204
+ ```
205
+
206
+ ## Configuration
207
+
208
+ ### Circuit Files
209
+
210
+ The SDK looks for circuit files in these locations:
211
+
212
+ 1. `./circuits/build/transaction_js/transaction.wasm`
213
+ 2. `./circuits/build/keys/transaction_final.zkey`
214
+ 3. `./assets/transaction.wasm` and `./assets/transaction_final.zkey`
215
+
216
+ You can also set custom paths:
217
+
218
+ ```typescript
219
+ import { setCircuitPaths } from '@hula/sdk';
220
+
221
+ setCircuitPaths('/custom/path/transaction.wasm', '/custom/path/transaction.zkey');
222
+ ```
223
+
224
+ ### Default Relayer URL
225
+
226
+ ```typescript
227
+ import { setDefaultRelayerUrl } from '@hula/sdk';
228
+
229
+ setDefaultRelayerUrl('https://relayer.hulaprivacy.io');
230
+ ```
231
+
232
+ ## Types
233
+
234
+ All types are exported for TypeScript users:
235
+
236
+ ```typescript
237
+ import type {
238
+ UTXO,
239
+ SerializableUTXO,
240
+ WalletKeys,
241
+ EncryptedNote,
242
+ MerklePath,
243
+ CircuitInputs,
244
+ TransactionRequest,
245
+ BuiltTransaction,
246
+ HulaSDKConfig,
247
+ } from '@hula/sdk';
248
+ ```
249
+
250
+ ## Building
251
+
252
+ ```bash
253
+ # Install dependencies
254
+ bun install
255
+
256
+ # Build
257
+ bun run build
258
+
259
+ # Type check
260
+ bun run typecheck
261
+ ```
262
+
263
+ ## Architecture
264
+
265
+ ```
266
+ sdk/src/
267
+ ├── index.ts # Main exports
268
+ ├── types.ts # Type definitions
269
+ ├── constants.ts # Program IDs, seeds, circuit params
270
+ ├── api.ts # Relayer API client
271
+ ├── crypto.ts # Poseidon, key derivation, encryption
272
+ ├── merkle.ts # Merkle tree operations
273
+ ├── utxo.ts # UTXO management
274
+ ├── proof.ts # ZK proof generation
275
+ ├── transaction.ts # Transaction building
276
+ └── wallet.ts # High-level wallet abstraction
277
+ ```
278
+
279
+ ## Security Considerations
280
+
281
+ 1. **Spending Key**: The spending key is the master secret. Never share it or store it insecurely.
282
+ 2. **Wallet Recovery**: Using `fromSignature()` allows deterministic recovery from a Solana wallet signature.
283
+ 3. **Local Storage**: When storing UTXOs locally, use appropriate encryption.
284
+ 4. **Note Encryption**: Encrypted notes allow recipients to discover UTXOs sent to them.
285
+
286
+ ## License
287
+
288
+ MIT
289
+
290
+
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@hula-privacy/mixer",
3
+ "version": "0.1.0",
4
+ "description": "Hula Privacy Protocol SDK - Complete toolkit for private transactions on Solana",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format cjs,esm --dts",
17
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@coral-xyz/anchor": "^0.30.1",
23
+ "@noble/hashes": "^1.3.0",
24
+ "@solana/spl-token": "^0.4.9",
25
+ "@solana/web3.js": "^1.95.4",
26
+ "circomlibjs": "^0.1.7",
27
+ "snarkjs": "^0.7.5",
28
+ "tweetnacl": "^1.0.3"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "@types/snarkjs": "^0.7.8",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@coral-xyz/anchor": ">=0.29.0",
38
+ "@solana/web3.js": ">=1.90.0"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src"
43
+ ],
44
+ "keywords": [
45
+ "solana",
46
+ "privacy",
47
+ "zk-snark",
48
+ "utxo",
49
+ "hula"
50
+ ],
51
+ "license": "MIT"
52
+ }
package/src/api.ts ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Relayer API Client
3
+ *
4
+ * Fetches data from the relayer with cursor-based pagination
5
+ */
6
+
7
+ import type {
8
+ PaginatedResponse,
9
+ PoolData,
10
+ TreeData,
11
+ LeafData,
12
+ TransactionData,
13
+ NoteData,
14
+ StatsData,
15
+ } from "./types";
16
+
17
+ const DEFAULT_API_URL = "http://localhost:3001";
18
+
19
+ /**
20
+ * API client for the Hula Privacy relayer
21
+ */
22
+ export class RelayerClient {
23
+ private baseUrl: string;
24
+
25
+ constructor(baseUrl: string = DEFAULT_API_URL) {
26
+ this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash
27
+ }
28
+
29
+ private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
30
+ const response = await fetch(`${this.baseUrl}${path}`, {
31
+ ...init,
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ ...init?.headers,
35
+ },
36
+ });
37
+
38
+ if (!response.ok) {
39
+ const errorData = await response.json().catch(() => ({ error: "Unknown error" })) as { error?: string };
40
+ throw new Error(errorData.error || `HTTP ${response.status}`);
41
+ }
42
+
43
+ return response.json() as Promise<T>;
44
+ }
45
+
46
+ // ============================================================================
47
+ // Health & Stats
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Health check
52
+ */
53
+ async health(): Promise<{ status: string; timestamp: string }> {
54
+ return this.fetch("/health");
55
+ }
56
+
57
+ /**
58
+ * Get protocol stats
59
+ */
60
+ async getStats(): Promise<StatsData> {
61
+ return this.fetch("/api/stats");
62
+ }
63
+
64
+ // ============================================================================
65
+ // Pool
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Get pool data
70
+ */
71
+ async getPool(): Promise<PoolData> {
72
+ return this.fetch("/api/pool");
73
+ }
74
+
75
+ // ============================================================================
76
+ // Trees
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Get all merkle trees
81
+ */
82
+ async getTrees(): Promise<{ trees: TreeData[] }> {
83
+ return this.fetch("/api/trees");
84
+ }
85
+
86
+ /**
87
+ * Get tree by index
88
+ */
89
+ async getTree(treeIndex: number): Promise<TreeData> {
90
+ return this.fetch(`/api/trees/${treeIndex}`);
91
+ }
92
+
93
+ // ============================================================================
94
+ // Leaves / Commitments
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Get leaves with cursor pagination
99
+ */
100
+ async getLeaves(
101
+ cursor?: string,
102
+ limit?: number,
103
+ treeIndex?: number
104
+ ): Promise<PaginatedResponse<LeafData>> {
105
+ const params = new URLSearchParams();
106
+ if (cursor) params.set("cursor", cursor);
107
+ if (limit) params.set("limit", String(limit));
108
+ if (treeIndex !== undefined) params.set("treeIndex", String(treeIndex));
109
+
110
+ return this.fetch(`/api/leaves?${params}`);
111
+ }
112
+
113
+ /**
114
+ * Get leaves after a specific index (for syncing)
115
+ */
116
+ async getLeavesAfter(
117
+ treeIndex: number,
118
+ afterLeafIndex: number,
119
+ limit?: number
120
+ ): Promise<{
121
+ items: { treeIndex: number; leafIndex: number; commitment: string; slot: string }[];
122
+ hasMore: boolean;
123
+ lastLeafIndex: number;
124
+ }> {
125
+ const params = new URLSearchParams();
126
+ params.set("treeIndex", String(treeIndex));
127
+ params.set("afterLeafIndex", String(afterLeafIndex));
128
+ if (limit) params.set("limit", String(limit));
129
+
130
+ return this.fetch(`/api/leaves/after?${params}`);
131
+ }
132
+
133
+ /**
134
+ * Get all leaves for a tree (auto-paginated)
135
+ */
136
+ async getAllLeavesForTree(treeIndex: number): Promise<LeafData[]> {
137
+ const allLeaves: LeafData[] = [];
138
+ let cursor: string | undefined;
139
+
140
+ while (true) {
141
+ const response = await this.getLeaves(cursor, 100, treeIndex);
142
+ allLeaves.push(...response.items);
143
+
144
+ if (!response.hasMore || !response.nextCursor) break;
145
+ cursor = response.nextCursor;
146
+ }
147
+
148
+ return allLeaves;
149
+ }
150
+
151
+ /**
152
+ * Get all commitment values for a tree as bigints
153
+ */
154
+ async getCommitmentsForTree(treeIndex: number): Promise<bigint[]> {
155
+ const leaves = await this.getAllLeavesForTree(treeIndex);
156
+ // Sort by leafIndex to ensure correct order
157
+ leaves.sort((a, b) => a.leafIndex - b.leafIndex);
158
+ return leaves.map(l => BigInt(l.commitment));
159
+ }
160
+
161
+ // ============================================================================
162
+ // Transactions
163
+ // ============================================================================
164
+
165
+ /**
166
+ * Get transactions with cursor pagination
167
+ */
168
+ async getTransactions(
169
+ cursor?: string,
170
+ limit?: number,
171
+ mint?: string
172
+ ): Promise<PaginatedResponse<TransactionData>> {
173
+ const params = new URLSearchParams();
174
+ if (cursor) params.set("cursor", cursor);
175
+ if (limit) params.set("limit", String(limit));
176
+ if (mint) params.set("mint", mint);
177
+
178
+ return this.fetch(`/api/transactions?${params}`);
179
+ }
180
+
181
+ /**
182
+ * Get transaction by signature
183
+ */
184
+ async getTransaction(signature: string): Promise<TransactionData> {
185
+ return this.fetch(`/api/transactions/${signature}`);
186
+ }
187
+
188
+ // ============================================================================
189
+ // Nullifiers
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Check if nullifier is spent
194
+ */
195
+ async checkNullifier(nullifier: string): Promise<{
196
+ nullifier: string;
197
+ spent: boolean;
198
+ spentAt?: string;
199
+ }> {
200
+ return this.fetch(`/api/nullifiers/check?nullifier=${encodeURIComponent(nullifier)}`);
201
+ }
202
+
203
+ /**
204
+ * Check multiple nullifiers at once
205
+ */
206
+ async checkNullifiersBatch(
207
+ nullifiers: string[]
208
+ ): Promise<{ results: { nullifier: string; spent: boolean }[] }> {
209
+ return this.fetch("/api/nullifiers/check-batch", {
210
+ method: "POST",
211
+ body: JSON.stringify({ nullifiers }),
212
+ });
213
+ }
214
+
215
+ // ============================================================================
216
+ // Encrypted Notes
217
+ // ============================================================================
218
+
219
+ /**
220
+ * Get encrypted notes with cursor pagination
221
+ */
222
+ async getNotes(
223
+ cursor?: string,
224
+ limit?: number,
225
+ afterSlot?: string
226
+ ): Promise<PaginatedResponse<NoteData>> {
227
+ const params = new URLSearchParams();
228
+ if (cursor) params.set("cursor", cursor);
229
+ if (limit) params.set("limit", String(limit));
230
+ if (afterSlot) params.set("afterSlot", afterSlot);
231
+
232
+ return this.fetch(`/api/notes?${params}`);
233
+ }
234
+
235
+ /**
236
+ * Get all notes (auto-paginated)
237
+ */
238
+ async getAllNotes(afterSlot?: string): Promise<NoteData[]> {
239
+ const allNotes: NoteData[] = [];
240
+ let cursor: string | undefined;
241
+
242
+ while (true) {
243
+ const response = await this.getNotes(cursor, 100, afterSlot);
244
+ allNotes.push(...response.items);
245
+
246
+ if (!response.hasMore || !response.nextCursor) break;
247
+ cursor = response.nextCursor;
248
+ }
249
+
250
+ return allNotes;
251
+ }
252
+ }
253
+
254
+ // Default singleton instance
255
+ let defaultClient: RelayerClient | null = null;
256
+
257
+ /**
258
+ * Get or create the default relayer client
259
+ */
260
+ export function getRelayerClient(url?: string): RelayerClient {
261
+ if (url) {
262
+ return new RelayerClient(url);
263
+ }
264
+ if (!defaultClient) {
265
+ defaultClient = new RelayerClient();
266
+ }
267
+ return defaultClient;
268
+ }
269
+
270
+ /**
271
+ * Set the default relayer URL
272
+ */
273
+ export function setDefaultRelayerUrl(url: string): void {
274
+ defaultClient = new RelayerClient(url);
275
+ }
276
+