@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,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zcash Shielded Transaction Service
|
|
3
|
+
*
|
|
4
|
+
* High-level service for managing Zcash shielded transactions,
|
|
5
|
+
* providing integration with SIP Protocol privacy levels.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const service = new ZcashShieldedService({
|
|
10
|
+
* rpcConfig: { username: 'user', password: 'pass', testnet: true },
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* // Initialize and create account
|
|
14
|
+
* await service.initialize()
|
|
15
|
+
*
|
|
16
|
+
* // Send shielded transaction
|
|
17
|
+
* const result = await service.sendShielded({
|
|
18
|
+
* to: recipientAddress,
|
|
19
|
+
* amount: 1.5,
|
|
20
|
+
* memo: 'Payment for services',
|
|
21
|
+
* privacyLevel: PrivacyLevel.SHIELDED,
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // Check incoming transactions
|
|
25
|
+
* const received = await service.getReceivedNotes()
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type ZcashConfig,
|
|
31
|
+
type ZcashUnspentNote,
|
|
32
|
+
type ZcashAccountBalance,
|
|
33
|
+
type ZcashOperation,
|
|
34
|
+
type ZcashPrivacyPolicy,
|
|
35
|
+
type ZcashAddressInfo,
|
|
36
|
+
PrivacyLevel,
|
|
37
|
+
} from '@sip-protocol/types'
|
|
38
|
+
import { ZcashRPCClient, ZcashRPCError } from './rpc-client'
|
|
39
|
+
import { ValidationError, IntentError, ErrorCode } from '../errors'
|
|
40
|
+
|
|
41
|
+
// ─── Types ─────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Configuration for ZcashShieldedService
|
|
45
|
+
*/
|
|
46
|
+
export interface ZcashShieldedServiceConfig {
|
|
47
|
+
/** RPC client configuration */
|
|
48
|
+
rpcConfig: ZcashConfig
|
|
49
|
+
/** Default account to use (default: 0) */
|
|
50
|
+
defaultAccount?: number
|
|
51
|
+
/** Default minimum confirmations (default: 1) */
|
|
52
|
+
defaultMinConf?: number
|
|
53
|
+
/** Poll interval for operations in ms (default: 1000) */
|
|
54
|
+
operationPollInterval?: number
|
|
55
|
+
/** Operation timeout in ms (default: 300000 = 5 min) */
|
|
56
|
+
operationTimeout?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shielded send parameters
|
|
61
|
+
*/
|
|
62
|
+
export interface ShieldedSendParams {
|
|
63
|
+
/** Recipient address (shielded or unified) */
|
|
64
|
+
to: string
|
|
65
|
+
/** Amount in ZEC */
|
|
66
|
+
amount: number
|
|
67
|
+
/** Optional memo (max 512 bytes) */
|
|
68
|
+
memo?: string
|
|
69
|
+
/** SIP privacy level */
|
|
70
|
+
privacyLevel?: PrivacyLevel
|
|
71
|
+
/** Source address (uses default if not specified) */
|
|
72
|
+
from?: string
|
|
73
|
+
/** Minimum confirmations for inputs */
|
|
74
|
+
minConf?: number
|
|
75
|
+
/** Custom fee (uses ZIP-317 default if not specified) */
|
|
76
|
+
fee?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Result of a shielded send operation
|
|
81
|
+
*/
|
|
82
|
+
export interface ShieldedSendResult {
|
|
83
|
+
/** Transaction ID */
|
|
84
|
+
txid: string
|
|
85
|
+
/** Operation ID (for tracking) */
|
|
86
|
+
operationId: string
|
|
87
|
+
/** Amount sent (excluding fee) */
|
|
88
|
+
amount: number
|
|
89
|
+
/** Fee paid */
|
|
90
|
+
fee: number
|
|
91
|
+
/** Recipient address */
|
|
92
|
+
to: string
|
|
93
|
+
/** Sender address */
|
|
94
|
+
from: string
|
|
95
|
+
/** Timestamp */
|
|
96
|
+
timestamp: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Received note information
|
|
101
|
+
*/
|
|
102
|
+
export interface ReceivedNote {
|
|
103
|
+
/** Transaction ID */
|
|
104
|
+
txid: string
|
|
105
|
+
/** Amount received */
|
|
106
|
+
amount: number
|
|
107
|
+
/** Memo content (if any) */
|
|
108
|
+
memo?: string
|
|
109
|
+
/** Number of confirmations */
|
|
110
|
+
confirmations: number
|
|
111
|
+
/** Whether spendable */
|
|
112
|
+
spendable: boolean
|
|
113
|
+
/** Pool type (sapling/orchard) */
|
|
114
|
+
pool: 'sapling' | 'orchard'
|
|
115
|
+
/** Receiving address */
|
|
116
|
+
address: string
|
|
117
|
+
/** Whether this is change */
|
|
118
|
+
isChange: boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Shielded balance summary
|
|
123
|
+
*/
|
|
124
|
+
export interface ShieldedBalance {
|
|
125
|
+
/** Total confirmed balance in ZEC */
|
|
126
|
+
confirmed: number
|
|
127
|
+
/** Total unconfirmed balance in ZEC */
|
|
128
|
+
unconfirmed: number
|
|
129
|
+
/** Balance by pool */
|
|
130
|
+
pools: {
|
|
131
|
+
transparent: number
|
|
132
|
+
sapling: number
|
|
133
|
+
orchard: number
|
|
134
|
+
}
|
|
135
|
+
/** Number of spendable notes */
|
|
136
|
+
spendableNotes: number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Viewing key export result
|
|
141
|
+
*/
|
|
142
|
+
export interface ExportedViewingKey {
|
|
143
|
+
/** The viewing key */
|
|
144
|
+
key: string
|
|
145
|
+
/** Associated address */
|
|
146
|
+
address: string
|
|
147
|
+
/** Account number */
|
|
148
|
+
account: number
|
|
149
|
+
/** Creation timestamp */
|
|
150
|
+
exportedAt: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Service Implementation ────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Zcash Shielded Transaction Service
|
|
157
|
+
*
|
|
158
|
+
* Provides high-level operations for Zcash shielded transactions
|
|
159
|
+
* with SIP Protocol integration.
|
|
160
|
+
*/
|
|
161
|
+
export class ZcashShieldedService {
|
|
162
|
+
private readonly client: ZcashRPCClient
|
|
163
|
+
private readonly config: Required<Omit<ZcashShieldedServiceConfig, 'rpcConfig'>>
|
|
164
|
+
private initialized: boolean = false
|
|
165
|
+
private accountAddress: string | null = null
|
|
166
|
+
private account: number = 0
|
|
167
|
+
|
|
168
|
+
constructor(config: ZcashShieldedServiceConfig) {
|
|
169
|
+
this.client = new ZcashRPCClient(config.rpcConfig)
|
|
170
|
+
this.config = {
|
|
171
|
+
defaultAccount: config.defaultAccount ?? 0,
|
|
172
|
+
defaultMinConf: config.defaultMinConf ?? 1,
|
|
173
|
+
operationPollInterval: config.operationPollInterval ?? 1000,
|
|
174
|
+
operationTimeout: config.operationTimeout ?? 300000,
|
|
175
|
+
}
|
|
176
|
+
this.account = this.config.defaultAccount
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Initialization ──────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Initialize the service
|
|
183
|
+
*
|
|
184
|
+
* Creates an account if needed and retrieves the default address.
|
|
185
|
+
*/
|
|
186
|
+
async initialize(): Promise<void> {
|
|
187
|
+
if (this.initialized) return
|
|
188
|
+
|
|
189
|
+
// Verify connection
|
|
190
|
+
await this.client.getBlockCount()
|
|
191
|
+
|
|
192
|
+
// Get or create account address
|
|
193
|
+
try {
|
|
194
|
+
const addressResult = await this.client.getAddressForAccount(this.account, ['sapling', 'orchard'])
|
|
195
|
+
this.accountAddress = addressResult.address
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Account might not exist, create it
|
|
198
|
+
if (error instanceof ZcashRPCError) {
|
|
199
|
+
const newAccount = await this.client.createAccount()
|
|
200
|
+
this.account = newAccount.account
|
|
201
|
+
const addressResult = await this.client.getAddressForAccount(this.account, ['sapling', 'orchard'])
|
|
202
|
+
this.accountAddress = addressResult.address
|
|
203
|
+
} else {
|
|
204
|
+
throw error
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.initialized = true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Ensure the service is initialized
|
|
213
|
+
*/
|
|
214
|
+
private ensureInitialized(): void {
|
|
215
|
+
if (!this.initialized) {
|
|
216
|
+
throw new IntentError(
|
|
217
|
+
'ZcashShieldedService not initialized. Call initialize() first.',
|
|
218
|
+
ErrorCode.INTENT_INVALID_STATE,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Address Operations ──────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get the default shielded address
|
|
227
|
+
*/
|
|
228
|
+
getAddress(): string {
|
|
229
|
+
this.ensureInitialized()
|
|
230
|
+
return this.accountAddress!
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Generate a new diversified address for the account
|
|
235
|
+
*
|
|
236
|
+
* Each address is unlinkable but controlled by the same account.
|
|
237
|
+
*/
|
|
238
|
+
async generateNewAddress(): Promise<string> {
|
|
239
|
+
this.ensureInitialized()
|
|
240
|
+
const result = await this.client.getAddressForAccount(this.account, ['sapling', 'orchard'])
|
|
241
|
+
return result.address
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validate an address
|
|
246
|
+
*/
|
|
247
|
+
async validateAddress(address: string): Promise<ZcashAddressInfo> {
|
|
248
|
+
return this.client.validateAddress(address)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if an address is a shielded address
|
|
253
|
+
*/
|
|
254
|
+
async isShieldedAddress(address: string): Promise<boolean> {
|
|
255
|
+
const info = await this.client.validateAddress(address)
|
|
256
|
+
if (!info.isvalid) return false
|
|
257
|
+
return info.address_type === 'sapling' ||
|
|
258
|
+
info.address_type === 'orchard' ||
|
|
259
|
+
info.address_type === 'unified'
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Balance Operations ──────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get shielded balance summary
|
|
266
|
+
*/
|
|
267
|
+
async getBalance(minConf?: number): Promise<ShieldedBalance> {
|
|
268
|
+
this.ensureInitialized()
|
|
269
|
+
|
|
270
|
+
const accountBalance = await this.client.getAccountBalance(
|
|
271
|
+
this.account,
|
|
272
|
+
minConf ?? this.config.defaultMinConf,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Get unspent notes for spendable count
|
|
276
|
+
const notes = await this.client.listUnspent(minConf ?? this.config.defaultMinConf)
|
|
277
|
+
const spendableNotes = notes.filter((n) => n.spendable).length
|
|
278
|
+
|
|
279
|
+
// Convert zatoshis to ZEC
|
|
280
|
+
const toZec = (zat: number | undefined) => (zat ?? 0) / 100_000_000
|
|
281
|
+
|
|
282
|
+
const transparent = toZec(accountBalance.pools.transparent?.valueZat)
|
|
283
|
+
const sapling = toZec(accountBalance.pools.sapling?.valueZat)
|
|
284
|
+
const orchard = toZec(accountBalance.pools.orchard?.valueZat)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
confirmed: transparent + sapling + orchard,
|
|
288
|
+
unconfirmed: 0, // Would need separate RPC call
|
|
289
|
+
pools: {
|
|
290
|
+
transparent,
|
|
291
|
+
sapling,
|
|
292
|
+
orchard,
|
|
293
|
+
},
|
|
294
|
+
spendableNotes,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Send Operations ─────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Send a shielded transaction
|
|
302
|
+
*
|
|
303
|
+
* @param params - Send parameters
|
|
304
|
+
* @returns Send result with txid
|
|
305
|
+
*/
|
|
306
|
+
async sendShielded(params: ShieldedSendParams): Promise<ShieldedSendResult> {
|
|
307
|
+
this.ensureInitialized()
|
|
308
|
+
|
|
309
|
+
// Validate recipient address
|
|
310
|
+
const recipientInfo = await this.client.validateAddress(params.to)
|
|
311
|
+
if (!recipientInfo.isvalid) {
|
|
312
|
+
throw new ValidationError(
|
|
313
|
+
`Invalid recipient address: ${params.to}`,
|
|
314
|
+
'to',
|
|
315
|
+
undefined,
|
|
316
|
+
ErrorCode.INVALID_ADDRESS,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Validate amount
|
|
321
|
+
if (params.amount <= 0) {
|
|
322
|
+
throw new ValidationError(
|
|
323
|
+
'Amount must be positive',
|
|
324
|
+
'amount',
|
|
325
|
+
{ received: params.amount },
|
|
326
|
+
ErrorCode.INVALID_AMOUNT,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Determine privacy policy based on SIP privacy level
|
|
331
|
+
const privacyPolicy = this.mapPrivacyLevelToPolicy(params.privacyLevel)
|
|
332
|
+
|
|
333
|
+
// Prepare memo (convert to hex if string)
|
|
334
|
+
let memoHex: string | undefined
|
|
335
|
+
if (params.memo) {
|
|
336
|
+
memoHex = Buffer.from(params.memo, 'utf-8').toString('hex')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Determine source address
|
|
340
|
+
const fromAddress = params.from ?? this.accountAddress!
|
|
341
|
+
|
|
342
|
+
// Send transaction
|
|
343
|
+
const operationId = await this.client.sendShielded({
|
|
344
|
+
fromAddress,
|
|
345
|
+
recipients: [
|
|
346
|
+
{
|
|
347
|
+
address: params.to,
|
|
348
|
+
amount: params.amount,
|
|
349
|
+
memo: memoHex,
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
minConf: params.minConf ?? this.config.defaultMinConf,
|
|
353
|
+
fee: params.fee,
|
|
354
|
+
privacyPolicy,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Wait for operation to complete
|
|
358
|
+
const operation = await this.client.waitForOperation(
|
|
359
|
+
operationId,
|
|
360
|
+
this.config.operationPollInterval,
|
|
361
|
+
this.config.operationTimeout,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if (!operation.result?.txid) {
|
|
365
|
+
throw new IntentError(
|
|
366
|
+
'Transaction completed but no txid returned',
|
|
367
|
+
ErrorCode.INTENT_FAILED,
|
|
368
|
+
{ context: { operationId } },
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
txid: operation.result.txid,
|
|
374
|
+
operationId,
|
|
375
|
+
amount: params.amount,
|
|
376
|
+
fee: params.fee ?? 0, // TODO: Get actual fee from operation
|
|
377
|
+
to: params.to,
|
|
378
|
+
from: fromAddress,
|
|
379
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Send shielded transaction with SIP integration
|
|
385
|
+
*
|
|
386
|
+
* Higher-level method that handles privacy level mapping.
|
|
387
|
+
*/
|
|
388
|
+
async sendWithPrivacy(
|
|
389
|
+
to: string,
|
|
390
|
+
amount: number,
|
|
391
|
+
privacyLevel: PrivacyLevel,
|
|
392
|
+
memo?: string,
|
|
393
|
+
): Promise<ShieldedSendResult> {
|
|
394
|
+
// For transparent mode, we could use t-addr but for now require shielded
|
|
395
|
+
if (privacyLevel === PrivacyLevel.TRANSPARENT) {
|
|
396
|
+
throw new ValidationError(
|
|
397
|
+
'Transparent mode not supported for Zcash shielded service. Use standard RPC client.',
|
|
398
|
+
'privacyLevel',
|
|
399
|
+
{ received: privacyLevel },
|
|
400
|
+
ErrorCode.INVALID_PRIVACY_LEVEL,
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return this.sendShielded({
|
|
405
|
+
to,
|
|
406
|
+
amount,
|
|
407
|
+
memo,
|
|
408
|
+
privacyLevel,
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ─── Receive Operations ──────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Get received notes (incoming shielded transactions)
|
|
416
|
+
*
|
|
417
|
+
* @param minConf - Minimum confirmations
|
|
418
|
+
* @param onlySpendable - Only return spendable notes
|
|
419
|
+
*/
|
|
420
|
+
async getReceivedNotes(minConf?: number, onlySpendable: boolean = false): Promise<ReceivedNote[]> {
|
|
421
|
+
this.ensureInitialized()
|
|
422
|
+
|
|
423
|
+
const notes = await this.client.listUnspent(
|
|
424
|
+
minConf ?? this.config.defaultMinConf,
|
|
425
|
+
9999999,
|
|
426
|
+
false,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return notes
|
|
430
|
+
.filter((note) => !onlySpendable || note.spendable)
|
|
431
|
+
.filter((note) => note.pool === 'sapling' || note.pool === 'orchard')
|
|
432
|
+
.map((note) => this.mapNoteToReceived(note))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get pending (unconfirmed) incoming transactions
|
|
437
|
+
*/
|
|
438
|
+
async getPendingNotes(): Promise<ReceivedNote[]> {
|
|
439
|
+
return this.getReceivedNotes(0)
|
|
440
|
+
.then((notes) => notes.filter((n) => n.confirmations === 0))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Wait for incoming note with specific criteria
|
|
445
|
+
*
|
|
446
|
+
* @param predicate - Function to match the expected note
|
|
447
|
+
* @param timeout - Timeout in ms
|
|
448
|
+
* @param pollInterval - Poll interval in ms
|
|
449
|
+
*/
|
|
450
|
+
async waitForNote(
|
|
451
|
+
predicate: (note: ReceivedNote) => boolean,
|
|
452
|
+
timeout: number = 300000,
|
|
453
|
+
pollInterval: number = 5000,
|
|
454
|
+
): Promise<ReceivedNote> {
|
|
455
|
+
const startTime = Date.now()
|
|
456
|
+
|
|
457
|
+
while (Date.now() - startTime < timeout) {
|
|
458
|
+
const notes = await this.getReceivedNotes(0)
|
|
459
|
+
const match = notes.find(predicate)
|
|
460
|
+
|
|
461
|
+
if (match) {
|
|
462
|
+
return match
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await this.delay(pollInterval)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw new IntentError(
|
|
469
|
+
'Timed out waiting for incoming note',
|
|
470
|
+
ErrorCode.NETWORK_TIMEOUT,
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ─── Viewing Key Operations ──────────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Export viewing key for an address
|
|
478
|
+
*
|
|
479
|
+
* The viewing key allows monitoring incoming transactions
|
|
480
|
+
* without spending capability.
|
|
481
|
+
*/
|
|
482
|
+
async exportViewingKey(address?: string): Promise<ExportedViewingKey> {
|
|
483
|
+
this.ensureInitialized()
|
|
484
|
+
const targetAddress = address ?? this.accountAddress!
|
|
485
|
+
|
|
486
|
+
const key = await this.client.exportViewingKey(targetAddress)
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
key,
|
|
490
|
+
address: targetAddress,
|
|
491
|
+
account: this.account,
|
|
492
|
+
exportedAt: Math.floor(Date.now() / 1000),
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Import viewing key for monitoring
|
|
498
|
+
*
|
|
499
|
+
* Allows monitoring transactions to an address without spending.
|
|
500
|
+
*/
|
|
501
|
+
async importViewingKey(
|
|
502
|
+
viewingKey: string,
|
|
503
|
+
rescan: 'yes' | 'no' | 'whenkeyisnew' = 'whenkeyisnew',
|
|
504
|
+
startHeight?: number,
|
|
505
|
+
): Promise<void> {
|
|
506
|
+
await this.client.importViewingKey(viewingKey, rescan, startHeight)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Export viewing key for compliance/audit
|
|
511
|
+
*
|
|
512
|
+
* Specifically for SIP COMPLIANT privacy level.
|
|
513
|
+
*/
|
|
514
|
+
async exportForCompliance(): Promise<{
|
|
515
|
+
viewingKey: ExportedViewingKey
|
|
516
|
+
privacyLevel: PrivacyLevel
|
|
517
|
+
disclaimer: string
|
|
518
|
+
}> {
|
|
519
|
+
const viewingKey = await this.exportViewingKey()
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
viewingKey,
|
|
523
|
+
privacyLevel: PrivacyLevel.COMPLIANT,
|
|
524
|
+
disclaimer:
|
|
525
|
+
'This viewing key provides read-only access to transaction history. ' +
|
|
526
|
+
'It cannot be used to spend funds. Share only with authorized auditors.',
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ─── Operation Tracking ──────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get status of an operation
|
|
534
|
+
*/
|
|
535
|
+
async getOperationStatus(operationId: string): Promise<ZcashOperation | null> {
|
|
536
|
+
const [operation] = await this.client.getOperationStatus([operationId])
|
|
537
|
+
return operation ?? null
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* List all pending operations
|
|
542
|
+
*/
|
|
543
|
+
async listPendingOperations(): Promise<ZcashOperation[]> {
|
|
544
|
+
const executing = await this.client.getOperationStatus()
|
|
545
|
+
return executing.filter((op) => op.status === 'executing' || op.status === 'queued')
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ─── Blockchain Info ─────────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get current block height
|
|
552
|
+
*/
|
|
553
|
+
async getBlockHeight(): Promise<number> {
|
|
554
|
+
return this.client.getBlockCount()
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Check if connected to testnet
|
|
559
|
+
*/
|
|
560
|
+
isTestnet(): boolean {
|
|
561
|
+
return this.client.isTestnet
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Map SIP privacy level to Zcash privacy policy
|
|
568
|
+
*/
|
|
569
|
+
private mapPrivacyLevelToPolicy(level?: PrivacyLevel): ZcashPrivacyPolicy {
|
|
570
|
+
switch (level) {
|
|
571
|
+
case PrivacyLevel.TRANSPARENT:
|
|
572
|
+
return 'NoPrivacy'
|
|
573
|
+
case PrivacyLevel.SHIELDED:
|
|
574
|
+
return 'FullPrivacy'
|
|
575
|
+
case PrivacyLevel.COMPLIANT:
|
|
576
|
+
// Compliant mode uses full privacy but exports viewing key separately
|
|
577
|
+
return 'FullPrivacy'
|
|
578
|
+
default:
|
|
579
|
+
return 'FullPrivacy'
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Map RPC unspent note to ReceivedNote
|
|
585
|
+
*/
|
|
586
|
+
private mapNoteToReceived(note: ZcashUnspentNote): ReceivedNote {
|
|
587
|
+
// Decode memo if present
|
|
588
|
+
let memo: string | undefined
|
|
589
|
+
if (note.memoStr) {
|
|
590
|
+
memo = note.memoStr
|
|
591
|
+
} else if (note.memo && note.memo !== '00' && !note.memo.match(/^f+$/i)) {
|
|
592
|
+
// Try to decode non-empty, non-padding memo
|
|
593
|
+
try {
|
|
594
|
+
memo = Buffer.from(note.memo, 'hex').toString('utf-8').replace(/\0+$/, '')
|
|
595
|
+
if (!memo || memo.length === 0) memo = undefined
|
|
596
|
+
} catch {
|
|
597
|
+
// Invalid UTF-8, leave as undefined
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
txid: note.txid,
|
|
603
|
+
amount: note.amount,
|
|
604
|
+
memo,
|
|
605
|
+
confirmations: note.confirmations,
|
|
606
|
+
spendable: note.spendable,
|
|
607
|
+
pool: note.pool as 'sapling' | 'orchard',
|
|
608
|
+
address: note.address,
|
|
609
|
+
isChange: note.change,
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private delay(ms: number): Promise<void> {
|
|
614
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ─── Getters ─────────────────────────────────────────────────────────────────
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Get underlying RPC client for advanced operations
|
|
621
|
+
*/
|
|
622
|
+
get rpcClient(): ZcashRPCClient {
|
|
623
|
+
return this.client
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Get current account number
|
|
628
|
+
*/
|
|
629
|
+
get currentAccount(): number {
|
|
630
|
+
return this.account
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Create a Zcash shielded service instance
|
|
636
|
+
*/
|
|
637
|
+
export function createZcashShieldedService(
|
|
638
|
+
config: ZcashShieldedServiceConfig,
|
|
639
|
+
): ZcashShieldedService {
|
|
640
|
+
return new ZcashShieldedService(config)
|
|
641
|
+
}
|