@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/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
|
+
|