@sip-protocol/sdk 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/LICENSE +21 -0
- package/dist/index.d.mts +3640 -0
- package/dist/index.d.ts +3640 -0
- package/dist/index.js +5725 -0
- package/dist/index.mjs +5606 -0
- package/package.json +61 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/near-intents.ts +475 -0
- package/src/adapters/oneclick-client.ts +367 -0
- package/src/commitment.ts +470 -0
- package/src/crypto.ts +93 -0
- package/src/errors.ts +471 -0
- package/src/index.ts +369 -0
- package/src/intent.ts +488 -0
- package/src/privacy.ts +382 -0
- package/src/proofs/index.ts +52 -0
- package/src/proofs/interface.ts +228 -0
- package/src/proofs/mock.ts +258 -0
- package/src/proofs/noir.ts +233 -0
- package/src/sip.ts +299 -0
- package/src/solver/index.ts +25 -0
- package/src/solver/mock-solver.ts +278 -0
- package/src/stealth.ts +414 -0
- package/src/validation.ts +401 -0
- package/src/wallet/base-adapter.ts +407 -0
- package/src/wallet/errors.ts +106 -0
- package/src/wallet/ethereum/adapter.ts +655 -0
- package/src/wallet/ethereum/index.ts +48 -0
- package/src/wallet/ethereum/mock.ts +505 -0
- package/src/wallet/ethereum/types.ts +364 -0
- package/src/wallet/index.ts +116 -0
- package/src/wallet/registry.ts +207 -0
- package/src/wallet/solana/adapter.ts +533 -0
- package/src/wallet/solana/index.ts +40 -0
- package/src/wallet/solana/mock.ts +522 -0
- package/src/wallet/solana/types.ts +253 -0
- package/src/zcash/index.ts +53 -0
- package/src/zcash/rpc-client.ts +623 -0
- package/src/zcash/shielded-service.ts +641 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zcash RPC Client
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for interacting with zcashd node via JSON-RPC.
|
|
5
|
+
* Supports both mainnet and testnet with automatic retry logic.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const client = new ZcashRPCClient({
|
|
10
|
+
* username: 'rpcuser',
|
|
11
|
+
* password: 'rpcpassword',
|
|
12
|
+
* testnet: true,
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* // Create new account and get address
|
|
16
|
+
* const { account } = await client.createAccount()
|
|
17
|
+
* const { address } = await client.getAddressForAccount(account)
|
|
18
|
+
*
|
|
19
|
+
* // Send shielded transaction
|
|
20
|
+
* const opId = await client.sendShielded({
|
|
21
|
+
* fromAddress: address,
|
|
22
|
+
* recipients: [{ address: recipientAddr, amount: 0.1 }],
|
|
23
|
+
* })
|
|
24
|
+
*
|
|
25
|
+
* // Wait for completion
|
|
26
|
+
* const result = await client.waitForOperation(opId)
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
type ZcashConfig,
|
|
32
|
+
type ZcashAddressInfo,
|
|
33
|
+
type ZcashNewAccount,
|
|
34
|
+
type ZcashAccountAddress,
|
|
35
|
+
type ZcashAccountBalance,
|
|
36
|
+
type ZcashReceiverType,
|
|
37
|
+
type ZcashUnspentNote,
|
|
38
|
+
type ZcashShieldedSendParams,
|
|
39
|
+
type ZcashOperation,
|
|
40
|
+
type ZcashBlockHeader,
|
|
41
|
+
type ZcashBlock,
|
|
42
|
+
type ZcashRPCRequest,
|
|
43
|
+
type ZcashRPCResponse,
|
|
44
|
+
type ZcashRPCError as ZcashRPCErrorType,
|
|
45
|
+
type ZcashBlockchainInfo,
|
|
46
|
+
type ZcashNetworkInfo,
|
|
47
|
+
ZcashErrorCode,
|
|
48
|
+
} from '@sip-protocol/types'
|
|
49
|
+
import { NetworkError, ErrorCode } from '../errors'
|
|
50
|
+
|
|
51
|
+
// ─── Default Configuration ─────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const DEFAULT_CONFIG: Required<Omit<ZcashConfig, 'username' | 'password'>> = {
|
|
54
|
+
host: '127.0.0.1',
|
|
55
|
+
port: 8232,
|
|
56
|
+
testnet: false,
|
|
57
|
+
timeout: 30000,
|
|
58
|
+
retries: 3,
|
|
59
|
+
retryDelay: 1000,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Error Classes ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Error thrown when Zcash RPC call fails
|
|
66
|
+
*/
|
|
67
|
+
export class ZcashRPCError extends Error {
|
|
68
|
+
constructor(
|
|
69
|
+
message: string,
|
|
70
|
+
public readonly code: number,
|
|
71
|
+
public readonly data?: unknown,
|
|
72
|
+
) {
|
|
73
|
+
super(message)
|
|
74
|
+
this.name = 'ZcashRPCError'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if error is due to insufficient funds
|
|
79
|
+
*/
|
|
80
|
+
isInsufficientFunds(): boolean {
|
|
81
|
+
return this.code === ZcashErrorCode.WALLET_INSUFFICIENT_FUNDS
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if error is due to invalid address
|
|
86
|
+
*/
|
|
87
|
+
isInvalidAddress(): boolean {
|
|
88
|
+
return this.code === ZcashErrorCode.INVALID_ADDRESS_OR_KEY
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if error is due to wallet being locked
|
|
93
|
+
*/
|
|
94
|
+
isWalletLocked(): boolean {
|
|
95
|
+
return this.code === ZcashErrorCode.WALLET_UNLOCK_NEEDED
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── RPC Client ────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Zcash RPC Client
|
|
103
|
+
*
|
|
104
|
+
* Provides type-safe access to zcashd JSON-RPC API with automatic
|
|
105
|
+
* retry logic and proper error handling.
|
|
106
|
+
*
|
|
107
|
+
* @security IMPORTANT: Always use HTTPS in production environments.
|
|
108
|
+
* This client uses HTTP Basic Authentication which transmits credentials
|
|
109
|
+
* in base64-encoded cleartext. Without TLS/HTTPS, credentials and all
|
|
110
|
+
* RPC data are vulnerable to network sniffing and man-in-the-middle attacks.
|
|
111
|
+
*
|
|
112
|
+
* Production configuration should use:
|
|
113
|
+
* - HTTPS endpoint (e.g., https://your-node.com:8232)
|
|
114
|
+
* - Valid TLS certificates
|
|
115
|
+
* - Secure credential storage
|
|
116
|
+
* - Network-level access controls
|
|
117
|
+
*/
|
|
118
|
+
export class ZcashRPCClient {
|
|
119
|
+
private readonly config: Required<ZcashConfig>
|
|
120
|
+
private readonly baseUrl: string
|
|
121
|
+
private requestId: number = 0
|
|
122
|
+
|
|
123
|
+
constructor(config: ZcashConfig) {
|
|
124
|
+
// Use testnet port if testnet is enabled and no custom port provided
|
|
125
|
+
const defaultPort = config.testnet ? 18232 : DEFAULT_CONFIG.port
|
|
126
|
+
|
|
127
|
+
this.config = {
|
|
128
|
+
host: config.host ?? DEFAULT_CONFIG.host,
|
|
129
|
+
port: config.port ?? defaultPort,
|
|
130
|
+
username: config.username,
|
|
131
|
+
password: config.password,
|
|
132
|
+
testnet: config.testnet ?? DEFAULT_CONFIG.testnet,
|
|
133
|
+
timeout: config.timeout ?? DEFAULT_CONFIG.timeout,
|
|
134
|
+
retries: config.retries ?? DEFAULT_CONFIG.retries,
|
|
135
|
+
retryDelay: config.retryDelay ?? DEFAULT_CONFIG.retryDelay,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.baseUrl = `http://${this.config.host}:${this.config.port}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Address Operations ────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Validate a Zcash address
|
|
145
|
+
*
|
|
146
|
+
* @param address - Address to validate (t-addr, z-addr, or unified)
|
|
147
|
+
* @returns Address validation info
|
|
148
|
+
*/
|
|
149
|
+
async validateAddress(address: string): Promise<ZcashAddressInfo> {
|
|
150
|
+
return this.call<ZcashAddressInfo>('z_validateaddress', [address])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a new HD account
|
|
155
|
+
*
|
|
156
|
+
* @returns New account number
|
|
157
|
+
*/
|
|
158
|
+
async createAccount(): Promise<ZcashNewAccount> {
|
|
159
|
+
return this.call<ZcashNewAccount>('z_getnewaccount', [])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get or derive an address for an account
|
|
164
|
+
*
|
|
165
|
+
* @param account - Account number
|
|
166
|
+
* @param receiverTypes - Optional receiver types (default: best shielded + p2pkh)
|
|
167
|
+
* @param diversifierIndex - Optional specific diversifier index
|
|
168
|
+
* @returns Account address info
|
|
169
|
+
*/
|
|
170
|
+
async getAddressForAccount(
|
|
171
|
+
account: number,
|
|
172
|
+
receiverTypes?: ZcashReceiverType[],
|
|
173
|
+
diversifierIndex?: number,
|
|
174
|
+
): Promise<ZcashAccountAddress> {
|
|
175
|
+
const params: unknown[] = [account]
|
|
176
|
+
if (receiverTypes !== undefined) {
|
|
177
|
+
params.push(receiverTypes)
|
|
178
|
+
if (diversifierIndex !== undefined) {
|
|
179
|
+
params.push(diversifierIndex)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return this.call<ZcashAccountAddress>('z_getaddressforaccount', params)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Generate a new shielded address (DEPRECATED)
|
|
187
|
+
*
|
|
188
|
+
* @deprecated Use createAccount() and getAddressForAccount() instead
|
|
189
|
+
* @param type - Address type ('sapling' or 'sprout')
|
|
190
|
+
* @returns New shielded address
|
|
191
|
+
*/
|
|
192
|
+
async generateShieldedAddress(type: 'sapling' | 'sprout' = 'sapling'): Promise<string> {
|
|
193
|
+
console.warn(
|
|
194
|
+
'generateShieldedAddress() is deprecated and will be removed in v0.2.0. ' +
|
|
195
|
+
'Use createAccount() and getAddressForAccount() instead.'
|
|
196
|
+
)
|
|
197
|
+
return this.call<string>('z_getnewaddress', [type])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* List all shielded addresses in the wallet
|
|
202
|
+
*
|
|
203
|
+
* @returns Array of shielded addresses
|
|
204
|
+
*/
|
|
205
|
+
async listAddresses(): Promise<string[]> {
|
|
206
|
+
return this.call<string[]>('z_listaddresses', [])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Balance Operations ────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get balance for an account
|
|
213
|
+
*
|
|
214
|
+
* @param account - Account number
|
|
215
|
+
* @param minConf - Minimum confirmations (default: 1)
|
|
216
|
+
* @returns Account balance by pool
|
|
217
|
+
*/
|
|
218
|
+
async getAccountBalance(account: number, minConf: number = 1): Promise<ZcashAccountBalance> {
|
|
219
|
+
return this.call<ZcashAccountBalance>('z_getbalanceforaccount', [account, minConf])
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get balance for an address (DEPRECATED)
|
|
224
|
+
*
|
|
225
|
+
* @deprecated Use getAccountBalance() instead
|
|
226
|
+
* @param address - Address to check
|
|
227
|
+
* @param minConf - Minimum confirmations
|
|
228
|
+
* @returns Balance in ZEC
|
|
229
|
+
*/
|
|
230
|
+
async getBalance(address: string, minConf: number = 1): Promise<number> {
|
|
231
|
+
return this.call<number>('z_getbalance', [address, minConf])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get total wallet balance
|
|
236
|
+
*
|
|
237
|
+
* @param minConf - Minimum confirmations
|
|
238
|
+
* @returns Total balances (transparent, private, total)
|
|
239
|
+
*/
|
|
240
|
+
async getTotalBalance(minConf: number = 1): Promise<{
|
|
241
|
+
transparent: string
|
|
242
|
+
private: string
|
|
243
|
+
total: string
|
|
244
|
+
}> {
|
|
245
|
+
return this.call('z_gettotalbalance', [minConf])
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─── UTXO Operations ───────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* List unspent shielded notes
|
|
252
|
+
*
|
|
253
|
+
* @param minConf - Minimum confirmations (default: 1)
|
|
254
|
+
* @param maxConf - Maximum confirmations (default: 9999999)
|
|
255
|
+
* @param includeWatchonly - Include watchonly addresses
|
|
256
|
+
* @param addresses - Filter by addresses
|
|
257
|
+
* @returns Array of unspent notes
|
|
258
|
+
*/
|
|
259
|
+
async listUnspent(
|
|
260
|
+
minConf: number = 1,
|
|
261
|
+
maxConf: number = 9999999,
|
|
262
|
+
includeWatchonly: boolean = false,
|
|
263
|
+
addresses?: string[],
|
|
264
|
+
): Promise<ZcashUnspentNote[]> {
|
|
265
|
+
const params: unknown[] = [minConf, maxConf, includeWatchonly]
|
|
266
|
+
if (addresses) {
|
|
267
|
+
params.push(addresses)
|
|
268
|
+
}
|
|
269
|
+
return this.call<ZcashUnspentNote[]>('z_listunspent', params)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── Transaction Operations ────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Send a shielded transaction
|
|
276
|
+
*
|
|
277
|
+
* @param params - Send parameters
|
|
278
|
+
* @returns Operation ID for tracking
|
|
279
|
+
*/
|
|
280
|
+
async sendShielded(params: ZcashShieldedSendParams): Promise<string> {
|
|
281
|
+
const amounts = params.recipients.map((r) => ({
|
|
282
|
+
address: r.address,
|
|
283
|
+
amount: r.amount,
|
|
284
|
+
...(r.memo && { memo: r.memo }),
|
|
285
|
+
}))
|
|
286
|
+
|
|
287
|
+
const rpcParams: unknown[] = [params.fromAddress, amounts]
|
|
288
|
+
|
|
289
|
+
if (params.minConf !== undefined) {
|
|
290
|
+
rpcParams.push(params.minConf)
|
|
291
|
+
if (params.fee !== undefined) {
|
|
292
|
+
rpcParams.push(params.fee)
|
|
293
|
+
if (params.privacyPolicy !== undefined) {
|
|
294
|
+
rpcParams.push(params.privacyPolicy)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this.call<string>('z_sendmany', rpcParams)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Shield coinbase UTXOs to a shielded address
|
|
304
|
+
*
|
|
305
|
+
* @param fromAddress - Transparent address with coinbase
|
|
306
|
+
* @param toAddress - Shielded destination
|
|
307
|
+
* @param fee - Optional fee
|
|
308
|
+
* @param limit - Max UTXOs to shield
|
|
309
|
+
* @returns Operation ID
|
|
310
|
+
*/
|
|
311
|
+
async shieldCoinbase(
|
|
312
|
+
fromAddress: string,
|
|
313
|
+
toAddress: string,
|
|
314
|
+
fee?: number,
|
|
315
|
+
limit?: number,
|
|
316
|
+
): Promise<{ operationid: string; shieldingUTXOs: number; shieldingValue: number }> {
|
|
317
|
+
const params: unknown[] = [fromAddress, toAddress]
|
|
318
|
+
if (fee !== undefined) params.push(fee)
|
|
319
|
+
if (limit !== undefined) params.push(limit)
|
|
320
|
+
return this.call('z_shieldcoinbase', params)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ─── Operation Management ──────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get status of async operations
|
|
327
|
+
*
|
|
328
|
+
* @param operationIds - Optional specific operation IDs
|
|
329
|
+
* @returns Array of operation statuses
|
|
330
|
+
*/
|
|
331
|
+
async getOperationStatus(operationIds?: string[]): Promise<ZcashOperation[]> {
|
|
332
|
+
return this.call<ZcashOperation[]>('z_getoperationstatus', operationIds ? [operationIds] : [])
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get and remove completed operation results
|
|
337
|
+
*
|
|
338
|
+
* @param operationIds - Optional specific operation IDs
|
|
339
|
+
* @returns Array of operation results
|
|
340
|
+
*/
|
|
341
|
+
async getOperationResult(operationIds?: string[]): Promise<ZcashOperation[]> {
|
|
342
|
+
return this.call<ZcashOperation[]>('z_getoperationresult', operationIds ? [operationIds] : [])
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* List all operation IDs
|
|
347
|
+
*
|
|
348
|
+
* @param status - Optional filter by status
|
|
349
|
+
* @returns Array of operation IDs
|
|
350
|
+
*/
|
|
351
|
+
async listOperationIds(status?: string): Promise<string[]> {
|
|
352
|
+
return this.call<string[]>('z_listoperationids', status ? [status] : [])
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Wait for an operation to complete
|
|
357
|
+
*
|
|
358
|
+
* @param operationId - Operation ID to wait for
|
|
359
|
+
* @param pollInterval - Poll interval in ms (default: 1000)
|
|
360
|
+
* @param timeout - Max wait time in ms (default: 300000 = 5 min)
|
|
361
|
+
* @returns Completed operation
|
|
362
|
+
* @throws ZcashRPCError if operation fails or times out
|
|
363
|
+
*/
|
|
364
|
+
async waitForOperation(
|
|
365
|
+
operationId: string,
|
|
366
|
+
pollInterval: number = 1000,
|
|
367
|
+
timeout: number = 300000,
|
|
368
|
+
): Promise<ZcashOperation> {
|
|
369
|
+
const startTime = Date.now()
|
|
370
|
+
|
|
371
|
+
while (Date.now() - startTime < timeout) {
|
|
372
|
+
const [operation] = await this.getOperationStatus([operationId])
|
|
373
|
+
|
|
374
|
+
if (!operation) {
|
|
375
|
+
throw new ZcashRPCError(`Operation ${operationId} not found`, -1)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (operation.status === 'success') {
|
|
379
|
+
return operation
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (operation.status === 'failed') {
|
|
383
|
+
throw new ZcashRPCError(
|
|
384
|
+
operation.error?.message ?? 'Operation failed',
|
|
385
|
+
operation.error?.code ?? -1,
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (operation.status === 'cancelled') {
|
|
390
|
+
throw new ZcashRPCError('Operation was cancelled', -1)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Still executing or queued, wait and retry
|
|
394
|
+
await this.delay(pollInterval)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
throw new ZcashRPCError(`Operation ${operationId} timed out after ${timeout}ms`, -1)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── Blockchain Operations ─────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get current block count
|
|
404
|
+
*
|
|
405
|
+
* @returns Current block height
|
|
406
|
+
*/
|
|
407
|
+
async getBlockCount(): Promise<number> {
|
|
408
|
+
return this.call<number>('getblockcount', [])
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get block hash at height
|
|
413
|
+
*
|
|
414
|
+
* @param height - Block height
|
|
415
|
+
* @returns Block hash
|
|
416
|
+
*/
|
|
417
|
+
async getBlockHash(height: number): Promise<string> {
|
|
418
|
+
return this.call<string>('getblockhash', [height])
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get block header
|
|
423
|
+
*
|
|
424
|
+
* @param hashOrHeight - Block hash or height
|
|
425
|
+
* @returns Block header
|
|
426
|
+
*/
|
|
427
|
+
async getBlockHeader(hashOrHeight: string | number): Promise<ZcashBlockHeader> {
|
|
428
|
+
const hash =
|
|
429
|
+
typeof hashOrHeight === 'number' ? await this.getBlockHash(hashOrHeight) : hashOrHeight
|
|
430
|
+
return this.call<ZcashBlockHeader>('getblockheader', [hash, true])
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get full block data
|
|
435
|
+
*
|
|
436
|
+
* @param hashOrHeight - Block hash or height
|
|
437
|
+
* @returns Block data
|
|
438
|
+
*/
|
|
439
|
+
async getBlock(hashOrHeight: string | number): Promise<ZcashBlock> {
|
|
440
|
+
const hash =
|
|
441
|
+
typeof hashOrHeight === 'number' ? await this.getBlockHash(hashOrHeight) : hashOrHeight
|
|
442
|
+
return this.call<ZcashBlock>('getblock', [hash, 1])
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get blockchain info
|
|
447
|
+
*
|
|
448
|
+
* @returns Blockchain information
|
|
449
|
+
*/
|
|
450
|
+
async getBlockchainInfo(): Promise<ZcashBlockchainInfo> {
|
|
451
|
+
return this.call<ZcashBlockchainInfo>('getblockchaininfo', [])
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get network info
|
|
456
|
+
*
|
|
457
|
+
* @returns Network information
|
|
458
|
+
*/
|
|
459
|
+
async getNetworkInfo(): Promise<ZcashNetworkInfo> {
|
|
460
|
+
return this.call<ZcashNetworkInfo>('getnetworkinfo', [])
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ─── Key Management ────────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Export viewing key for address
|
|
467
|
+
*
|
|
468
|
+
* @param address - Shielded address
|
|
469
|
+
* @returns Viewing key
|
|
470
|
+
*/
|
|
471
|
+
async exportViewingKey(address: string): Promise<string> {
|
|
472
|
+
return this.call<string>('z_exportviewingkey', [address])
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Import viewing key
|
|
477
|
+
*
|
|
478
|
+
* @param viewingKey - The viewing key to import
|
|
479
|
+
* @param rescan - Rescan the wallet (default: whenkeyisnew)
|
|
480
|
+
* @param startHeight - Start height for rescan
|
|
481
|
+
*/
|
|
482
|
+
async importViewingKey(
|
|
483
|
+
viewingKey: string,
|
|
484
|
+
rescan: 'yes' | 'no' | 'whenkeyisnew' = 'whenkeyisnew',
|
|
485
|
+
startHeight?: number,
|
|
486
|
+
): Promise<void> {
|
|
487
|
+
const params: unknown[] = [viewingKey, rescan]
|
|
488
|
+
if (startHeight !== undefined) params.push(startHeight)
|
|
489
|
+
await this.call<null>('z_importviewingkey', params)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ─── Low-Level RPC ─────────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Make a raw RPC call
|
|
496
|
+
*
|
|
497
|
+
* @param method - RPC method name
|
|
498
|
+
* @param params - Method parameters
|
|
499
|
+
* @returns RPC response result
|
|
500
|
+
*/
|
|
501
|
+
async call<T>(method: string, params: unknown[] = []): Promise<T> {
|
|
502
|
+
const request: ZcashRPCRequest = {
|
|
503
|
+
jsonrpc: '1.0',
|
|
504
|
+
id: ++this.requestId,
|
|
505
|
+
method,
|
|
506
|
+
params,
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
let lastError: Error | null = null
|
|
510
|
+
|
|
511
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
512
|
+
try {
|
|
513
|
+
const response = await this.executeRequest<T>(request)
|
|
514
|
+
|
|
515
|
+
if (response.error) {
|
|
516
|
+
throw new ZcashRPCError(response.error.message, response.error.code, response.error.data)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return response.result as T
|
|
520
|
+
} catch (error) {
|
|
521
|
+
lastError = error as Error
|
|
522
|
+
|
|
523
|
+
// Don't retry on RPC errors (only on network errors)
|
|
524
|
+
if (error instanceof ZcashRPCError) {
|
|
525
|
+
throw error
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Wait before retry (except on last attempt)
|
|
529
|
+
if (attempt < this.config.retries) {
|
|
530
|
+
await this.delay(this.config.retryDelay * (attempt + 1))
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
throw new NetworkError(
|
|
536
|
+
`Zcash RPC call failed after ${this.config.retries + 1} attempts: ${lastError?.message}`,
|
|
537
|
+
ErrorCode.NETWORK_FAILED,
|
|
538
|
+
{ cause: lastError ?? undefined },
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Execute HTTP request to RPC endpoint
|
|
544
|
+
*/
|
|
545
|
+
private async executeRequest<T>(request: ZcashRPCRequest): Promise<ZcashRPCResponse<T>> {
|
|
546
|
+
const controller = new AbortController()
|
|
547
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const response = await fetch(this.baseUrl, {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: {
|
|
553
|
+
'Content-Type': 'application/json',
|
|
554
|
+
Authorization: `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`,
|
|
555
|
+
},
|
|
556
|
+
body: JSON.stringify(request),
|
|
557
|
+
signal: controller.signal,
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
if (!response.ok) {
|
|
561
|
+
throw new Error(`HTTP error: ${response.status} ${response.statusText}`)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return (await response.json()) as ZcashRPCResponse<T>
|
|
565
|
+
} finally {
|
|
566
|
+
clearTimeout(timeoutId)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private delay(ms: number): Promise<void> {
|
|
571
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ─── Getters ───────────────────────────────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Check if client is configured for testnet
|
|
578
|
+
*/
|
|
579
|
+
get isTestnet(): boolean {
|
|
580
|
+
return this.config.testnet
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Get the RPC endpoint URL
|
|
585
|
+
*/
|
|
586
|
+
get endpoint(): string {
|
|
587
|
+
return this.baseUrl
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Create a Zcash RPC client
|
|
593
|
+
*
|
|
594
|
+
* @param config - Client configuration
|
|
595
|
+
* @returns ZcashRPCClient instance
|
|
596
|
+
*
|
|
597
|
+
* @security IMPORTANT: Always use HTTPS in production environments.
|
|
598
|
+
* HTTP Basic Auth transmits credentials in cleartext without TLS/HTTPS.
|
|
599
|
+
* Configure your zcashd node with TLS certificates and use https:// URLs.
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```typescript
|
|
603
|
+
* // ✅ Production (HTTPS)
|
|
604
|
+
* const client = createZcashClient({
|
|
605
|
+
* host: 'https://your-node.com',
|
|
606
|
+
* port: 8232,
|
|
607
|
+
* username: process.env.ZCASH_RPC_USER,
|
|
608
|
+
* password: process.env.ZCASH_RPC_PASS,
|
|
609
|
+
* })
|
|
610
|
+
*
|
|
611
|
+
* // ⚠️ Development only (HTTP)
|
|
612
|
+
* const testClient = createZcashClient({
|
|
613
|
+
* host: '127.0.0.1',
|
|
614
|
+
* port: 18232,
|
|
615
|
+
* username: 'test',
|
|
616
|
+
* password: 'test',
|
|
617
|
+
* testnet: true,
|
|
618
|
+
* })
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
export function createZcashClient(config: ZcashConfig): ZcashRPCClient {
|
|
622
|
+
return new ZcashRPCClient(config)
|
|
623
|
+
}
|