@sip-protocol/sdk 0.3.1 → 0.4.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.
Files changed (49) hide show
  1. package/dist/browser.d.mts +2 -2
  2. package/dist/browser.d.ts +2 -2
  3. package/dist/browser.js +1028 -146
  4. package/dist/browser.mjs +49 -1
  5. package/dist/chunk-AOZIY3GU.mjs +12995 -0
  6. package/dist/chunk-BCLIX5T2.mjs +12940 -0
  7. package/dist/chunk-EU4UEWWG.mjs +12164 -0
  8. package/dist/chunk-FKXPHKYD.mjs +12955 -0
  9. package/dist/chunk-OPQ2GQIO.mjs +13013 -0
  10. package/dist/index-BcWNakUD.d.ts +7990 -0
  11. package/dist/index-BsKY3Hr0.d.mts +7990 -0
  12. package/dist/index.d.mts +2 -2
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +999 -117
  15. package/dist/index.mjs +49 -1
  16. package/package.json +2 -1
  17. package/src/adapters/near-intents.ts +8 -0
  18. package/src/bitcoin/index.ts +51 -0
  19. package/src/bitcoin/silent-payments.ts +865 -0
  20. package/src/bitcoin/taproot.ts +590 -0
  21. package/src/cosmos/ibc-stealth.ts +825 -0
  22. package/src/cosmos/index.ts +83 -0
  23. package/src/cosmos/stealth.ts +487 -0
  24. package/src/index.ts +51 -0
  25. package/src/move/aptos.ts +369 -0
  26. package/src/move/index.ts +35 -0
  27. package/src/move/sui.ts +367 -0
  28. package/src/oracle/types.ts +8 -0
  29. package/src/settlement/backends/direct-chain.ts +8 -0
  30. package/src/settlement/backends/near-intents.ts +11 -0
  31. package/src/stealth.ts +3 -3
  32. package/src/validation.ts +42 -1
  33. package/src/wallet/aptos/adapter.ts +422 -0
  34. package/src/wallet/aptos/index.ts +10 -0
  35. package/src/wallet/aptos/mock.ts +410 -0
  36. package/src/wallet/aptos/types.ts +278 -0
  37. package/src/wallet/bitcoin/adapter.ts +470 -0
  38. package/src/wallet/bitcoin/index.ts +38 -0
  39. package/src/wallet/bitcoin/mock.ts +516 -0
  40. package/src/wallet/bitcoin/types.ts +274 -0
  41. package/src/wallet/cosmos/adapter.ts +484 -0
  42. package/src/wallet/cosmos/index.ts +63 -0
  43. package/src/wallet/cosmos/mock.ts +596 -0
  44. package/src/wallet/cosmos/types.ts +462 -0
  45. package/src/wallet/index.ts +127 -0
  46. package/src/wallet/sui/adapter.ts +471 -0
  47. package/src/wallet/sui/index.ts +10 -0
  48. package/src/wallet/sui/mock.ts +439 -0
  49. package/src/wallet/sui/types.ts +245 -0
@@ -0,0 +1,825 @@
1
+ /**
2
+ * Cosmos IBC Stealth Transfers
3
+ *
4
+ * Implements privacy-preserving cross-chain transfers using IBC (Inter-Blockchain
5
+ * Communication) protocol with stealth addresses.
6
+ *
7
+ * Key Features:
8
+ * - Cross-chain stealth transfers between Cosmos chains
9
+ * - Ephemeral key transmission via IBC memo field
10
+ * - View tag for efficient scanning
11
+ * - Support for multiple Cosmos chains (Hub, Osmosis, Injective, Celestia)
12
+ *
13
+ * IBC Memo Format:
14
+ * ```json
15
+ * {
16
+ * "sip": {
17
+ * "version": 1,
18
+ * "ephemeralKey": "0x...",
19
+ * "viewTag": 123
20
+ * }
21
+ * }
22
+ * ```
23
+ *
24
+ * @see https://ibc.cosmos.network/main/ibc/overview
25
+ * @see https://github.com/cosmos/ibc
26
+ */
27
+
28
+ import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
29
+ import { secp256k1 } from '@noble/curves/secp256k1'
30
+ import { sha256 } from '@noble/hashes/sha256'
31
+ import {
32
+ CosmosStealthService,
33
+ type CosmosChainId,
34
+ type CosmosStealthResult,
35
+ CHAIN_PREFIXES,
36
+ } from './stealth'
37
+ import type { HexString, StealthMetaAddress } from '@sip-protocol/types'
38
+ import { ValidationError } from '../errors'
39
+
40
+ /**
41
+ * IBC channel configuration for chain pairs
42
+ */
43
+ export interface IBCChannel {
44
+ /** Source channel ID (e.g., "channel-0") */
45
+ sourceChannel: string
46
+ /** Destination channel ID (e.g., "channel-141") */
47
+ destChannel: string
48
+ /** Port ID (typically "transfer" for token transfers) */
49
+ portId: string
50
+ }
51
+
52
+ /**
53
+ * Known IBC channels between Cosmos chains
54
+ *
55
+ * These are production channel IDs. For testnet/devnet, different channels may be used.
56
+ * Channel mappings are bidirectional (source→dest and dest→source).
57
+ *
58
+ * @see https://www.mintscan.io/cosmos/relayers for channel explorer
59
+ */
60
+ export const IBC_CHANNELS: Record<string, IBCChannel> = {
61
+ // Cosmos Hub ↔ Osmosis
62
+ 'cosmos-osmosis': {
63
+ sourceChannel: 'channel-141',
64
+ destChannel: 'channel-0',
65
+ portId: 'transfer',
66
+ },
67
+ 'osmosis-cosmos': {
68
+ sourceChannel: 'channel-0',
69
+ destChannel: 'channel-141',
70
+ portId: 'transfer',
71
+ },
72
+
73
+ // Cosmos Hub ↔ Injective
74
+ 'cosmos-injective': {
75
+ sourceChannel: 'channel-220',
76
+ destChannel: 'channel-1',
77
+ portId: 'transfer',
78
+ },
79
+ 'injective-cosmos': {
80
+ sourceChannel: 'channel-1',
81
+ destChannel: 'channel-220',
82
+ portId: 'transfer',
83
+ },
84
+
85
+ // Cosmos Hub ↔ Celestia
86
+ 'cosmos-celestia': {
87
+ sourceChannel: 'channel-674',
88
+ destChannel: 'channel-0',
89
+ portId: 'transfer',
90
+ },
91
+ 'celestia-cosmos': {
92
+ sourceChannel: 'channel-0',
93
+ destChannel: 'channel-674',
94
+ portId: 'transfer',
95
+ },
96
+
97
+ // Osmosis ↔ Injective
98
+ 'osmosis-injective': {
99
+ sourceChannel: 'channel-122',
100
+ destChannel: 'channel-8',
101
+ portId: 'transfer',
102
+ },
103
+ 'injective-osmosis': {
104
+ sourceChannel: 'channel-8',
105
+ destChannel: 'channel-122',
106
+ portId: 'transfer',
107
+ },
108
+
109
+ // Osmosis ↔ Celestia
110
+ 'osmosis-celestia': {
111
+ sourceChannel: 'channel-6994',
112
+ destChannel: 'channel-2',
113
+ portId: 'transfer',
114
+ },
115
+ 'celestia-osmosis': {
116
+ sourceChannel: 'channel-2',
117
+ destChannel: 'channel-6994',
118
+ portId: 'transfer',
119
+ },
120
+
121
+ // Injective ↔ Celestia
122
+ 'injective-celestia': {
123
+ sourceChannel: 'channel-152',
124
+ destChannel: 'channel-7',
125
+ portId: 'transfer',
126
+ },
127
+ 'celestia-injective': {
128
+ sourceChannel: 'channel-7',
129
+ destChannel: 'channel-152',
130
+ portId: 'transfer',
131
+ },
132
+ }
133
+
134
+ /**
135
+ * Parameters for creating a stealth IBC transfer
136
+ */
137
+ export interface StealthIBCTransferParams {
138
+ /** Source chain (where tokens are transferred from) */
139
+ sourceChain: CosmosChainId
140
+ /** Destination chain (where tokens are transferred to) */
141
+ destChain: CosmosChainId
142
+ /** Recipient's stealth meta-address (sip:cosmos:...) or StealthMetaAddress */
143
+ recipientMetaAddress: string | StealthMetaAddress
144
+ /** Amount to transfer (in base units) */
145
+ amount: bigint
146
+ /** Token denomination (e.g., "uatom", "uosmo", "inj") */
147
+ denom: string
148
+ /** Optional memo text (will be merged with SIP metadata) */
149
+ memo?: string
150
+ /** Optional timeout height */
151
+ timeoutHeight?: {
152
+ revisionNumber: bigint
153
+ revisionHeight: bigint
154
+ }
155
+ /** Optional timeout timestamp (Unix timestamp in nanoseconds) */
156
+ timeoutTimestamp?: bigint
157
+ }
158
+
159
+ /**
160
+ * Result of stealth IBC transfer creation
161
+ */
162
+ export interface StealthIBCTransfer {
163
+ /** Source chain */
164
+ sourceChain: CosmosChainId
165
+ /** Destination chain */
166
+ destChain: CosmosChainId
167
+ /** Generated stealth address on destination chain (bech32) */
168
+ stealthAddress: string
169
+ /** Stealth public key (for verification) */
170
+ stealthPublicKey: HexString
171
+ /** Ephemeral public key (for recipient to derive private key) */
172
+ ephemeralPublicKey: HexString
173
+ /** View tag for efficient scanning (0-255) */
174
+ viewTag: number
175
+ /** Amount being transferred */
176
+ amount: bigint
177
+ /** Token denomination */
178
+ denom: string
179
+ /** IBC channel configuration */
180
+ ibcChannel: IBCChannel
181
+ /** IBC memo containing SIP metadata */
182
+ memo: string
183
+ }
184
+
185
+ /**
186
+ * IBC MsgTransfer message (Cosmos SDK standard)
187
+ *
188
+ * This is the standard IBC transfer message format.
189
+ * @see https://github.com/cosmos/ibc-go/blob/main/proto/ibc/applications/transfer/v1/tx.proto
190
+ */
191
+ export interface IBCMsgTransfer {
192
+ /** Message type URL */
193
+ typeUrl: '/ibc.applications.transfer.v1.MsgTransfer'
194
+ /** Message value */
195
+ value: {
196
+ /** Source port (typically "transfer") */
197
+ sourcePort: string
198
+ /** Source channel ID */
199
+ sourceChannel: string
200
+ /** Token being transferred */
201
+ token: {
202
+ /** Token denomination */
203
+ denom: string
204
+ /** Amount (as string) */
205
+ amount: string
206
+ }
207
+ /** Sender address (on source chain) */
208
+ sender: string
209
+ /** Receiver address (stealth address on dest chain) */
210
+ receiver: string
211
+ /** Optional timeout height */
212
+ timeoutHeight?: {
213
+ revisionNumber: string
214
+ revisionHeight: string
215
+ }
216
+ /** Optional timeout timestamp (Unix nanos as string) */
217
+ timeoutTimestamp?: string
218
+ /** Memo containing SIP metadata */
219
+ memo: string
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Incoming IBC transfer to scan for stealth payments
225
+ */
226
+ export interface IncomingIBCTransfer {
227
+ /** Transfer ID or hash */
228
+ id: string
229
+ /** Sender address on source chain */
230
+ sender: string
231
+ /** Receiver address on dest chain (might be our stealth address) */
232
+ receiver: string
233
+ /** Amount received */
234
+ amount: bigint
235
+ /** Token denomination */
236
+ denom: string
237
+ /** IBC memo (contains SIP metadata) */
238
+ memo: string
239
+ /** Source chain */
240
+ sourceChain: CosmosChainId
241
+ /** Destination chain */
242
+ destChain: CosmosChainId
243
+ /** Block height */
244
+ height: bigint
245
+ /** Timestamp */
246
+ timestamp: bigint
247
+ }
248
+
249
+ /**
250
+ * Detected stealth transfer received by recipient
251
+ */
252
+ export interface ReceivedStealthTransfer {
253
+ /** Transfer ID */
254
+ id: string
255
+ /** Source chain */
256
+ sourceChain: CosmosChainId
257
+ /** Destination chain */
258
+ destChain: CosmosChainId
259
+ /** Stealth address that received funds */
260
+ stealthAddress: string
261
+ /** Amount received */
262
+ amount: bigint
263
+ /** Token denomination */
264
+ denom: string
265
+ /** Ephemeral public key (from memo) */
266
+ ephemeralPublicKey: HexString
267
+ /** View tag (from memo) */
268
+ viewTag: number
269
+ /** Derived private key to claim funds */
270
+ privateKey: HexString
271
+ /** Block height */
272
+ height: bigint
273
+ /** Timestamp */
274
+ timestamp: bigint
275
+ }
276
+
277
+ /**
278
+ * SIP metadata encoded in IBC memo
279
+ */
280
+ interface SIPMemoMetadata {
281
+ sip: {
282
+ /** Protocol version */
283
+ version: number
284
+ /** Ephemeral public key (hex with 0x prefix) */
285
+ ephemeralKey: HexString
286
+ /** View tag for efficient scanning */
287
+ viewTag: number
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Cosmos IBC Stealth Service
293
+ *
294
+ * Provides IBC transfer functionality with stealth address privacy.
295
+ */
296
+ export class CosmosIBCStealthService {
297
+ private stealthService: CosmosStealthService
298
+
299
+ constructor() {
300
+ this.stealthService = new CosmosStealthService()
301
+ }
302
+
303
+ /**
304
+ * Create a stealth IBC transfer
305
+ *
306
+ * Generates a stealth address on the destination chain and creates
307
+ * an IBC transfer with SIP metadata in the memo field.
308
+ *
309
+ * @param params - Transfer parameters
310
+ * @returns Stealth IBC transfer details
311
+ * @throws {ValidationError} If parameters are invalid
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * const service = new CosmosIBCStealthService()
316
+ * const transfer = service.createStealthIBCTransfer({
317
+ * sourceChain: 'cosmos',
318
+ * destChain: 'osmosis',
319
+ * recipientMetaAddress: 'sip:cosmos:0x02abc...123:0x03def...456',
320
+ * amount: 1000000n, // 1 ATOM (6 decimals)
321
+ * denom: 'uatom'
322
+ * })
323
+ * console.log(transfer.stealthAddress) // "osmo1..."
324
+ * ```
325
+ */
326
+ createStealthIBCTransfer(params: StealthIBCTransferParams): StealthIBCTransfer {
327
+ // Validate chains
328
+ this.validateChain(params.sourceChain)
329
+ this.validateChain(params.destChain)
330
+
331
+ if (params.sourceChain === params.destChain) {
332
+ throw new ValidationError(
333
+ 'source and destination chains must be different for IBC transfers',
334
+ 'sourceChain/destChain'
335
+ )
336
+ }
337
+
338
+ // Validate amount
339
+ if (params.amount <= 0n) {
340
+ throw new ValidationError('amount must be positive', 'amount')
341
+ }
342
+
343
+ // Validate denom
344
+ if (!params.denom || params.denom.length === 0) {
345
+ throw new ValidationError('denom cannot be empty', 'denom')
346
+ }
347
+
348
+ // Parse recipient meta-address
349
+ const metaAddress = this.parseMetaAddress(params.recipientMetaAddress)
350
+
351
+ // Generate stealth address on destination chain
352
+ const stealthResult = this.stealthService.generateStealthAddressFromMeta(
353
+ metaAddress,
354
+ params.destChain
355
+ )
356
+
357
+ // Get IBC channel
358
+ const ibcChannel = this.getIBCChannel(params.sourceChain, params.destChain)
359
+
360
+ // Create SIP memo
361
+ const sipMemo = this.encodeSIPMemo(
362
+ stealthResult.ephemeralPublicKey,
363
+ stealthResult.viewTag,
364
+ params.memo
365
+ )
366
+
367
+ return {
368
+ sourceChain: params.sourceChain,
369
+ destChain: params.destChain,
370
+ stealthAddress: stealthResult.stealthAddress,
371
+ stealthPublicKey: stealthResult.stealthPublicKey,
372
+ ephemeralPublicKey: stealthResult.ephemeralPublicKey,
373
+ viewTag: stealthResult.viewTag,
374
+ amount: params.amount,
375
+ denom: params.denom,
376
+ ibcChannel,
377
+ memo: sipMemo,
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Build IBC MsgTransfer for a stealth transfer
383
+ *
384
+ * Creates a Cosmos SDK MsgTransfer message that can be signed and broadcast.
385
+ *
386
+ * @param transfer - Stealth IBC transfer details
387
+ * @param senderAddress - Sender's address on source chain
388
+ * @param timeoutHeight - Optional timeout height
389
+ * @param timeoutTimestamp - Optional timeout timestamp (Unix nanos)
390
+ * @returns IBC MsgTransfer message
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * const service = new CosmosIBCStealthService()
395
+ * const transfer = service.createStealthIBCTransfer({...})
396
+ * const msg = service.buildIBCMsgTransfer(
397
+ * transfer,
398
+ * 'cosmos1sender...',
399
+ * undefined,
400
+ * BigInt(Date.now() + 600000) * 1_000_000n // 10 min timeout
401
+ * )
402
+ * // Sign and broadcast msg
403
+ * ```
404
+ */
405
+ buildIBCMsgTransfer(
406
+ transfer: StealthIBCTransfer,
407
+ senderAddress: string,
408
+ timeoutHeight?: { revisionNumber: bigint; revisionHeight: bigint },
409
+ timeoutTimestamp?: bigint
410
+ ): IBCMsgTransfer {
411
+ // Validate sender address
412
+ if (!this.stealthService.isValidCosmosAddress(senderAddress, transfer.sourceChain)) {
413
+ throw new ValidationError(
414
+ `invalid sender address for chain ${transfer.sourceChain}`,
415
+ 'senderAddress'
416
+ )
417
+ }
418
+
419
+ const msg: IBCMsgTransfer = {
420
+ typeUrl: '/ibc.applications.transfer.v1.MsgTransfer',
421
+ value: {
422
+ sourcePort: transfer.ibcChannel.portId,
423
+ sourceChannel: transfer.ibcChannel.sourceChannel,
424
+ token: {
425
+ denom: transfer.denom,
426
+ amount: transfer.amount.toString(),
427
+ },
428
+ sender: senderAddress,
429
+ receiver: transfer.stealthAddress,
430
+ memo: transfer.memo,
431
+ },
432
+ }
433
+
434
+ // Add optional timeout height
435
+ if (timeoutHeight) {
436
+ msg.value.timeoutHeight = {
437
+ revisionNumber: timeoutHeight.revisionNumber.toString(),
438
+ revisionHeight: timeoutHeight.revisionHeight.toString(),
439
+ }
440
+ }
441
+
442
+ // Add optional timeout timestamp
443
+ if (timeoutTimestamp) {
444
+ msg.value.timeoutTimestamp = timeoutTimestamp.toString()
445
+ }
446
+
447
+ return msg
448
+ }
449
+
450
+ /**
451
+ * Scan incoming IBC transfers for stealth payments
452
+ *
453
+ * Checks if any incoming transfers are addressed to stealth addresses
454
+ * belonging to the recipient. For each match, derives the private key
455
+ * needed to claim the funds.
456
+ *
457
+ * @param viewingKey - Recipient's viewing private key
458
+ * @param spendingPubKey - Recipient's spending public key
459
+ * @param spendingPrivateKey - Recipient's spending private key
460
+ * @param transfers - List of incoming IBC transfers to scan
461
+ * @returns List of detected stealth transfers with derived keys
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * const service = new CosmosIBCStealthService()
466
+ * const received = service.scanIBCTransfers(
467
+ * viewingPrivKey,
468
+ * spendingPubKey,
469
+ * spendingPrivKey,
470
+ * incomingTransfers
471
+ * )
472
+ *
473
+ * for (const transfer of received) {
474
+ * console.log(`Received ${transfer.amount} ${transfer.denom}`)
475
+ * // Use transfer.privateKey to claim funds from transfer.stealthAddress
476
+ * }
477
+ * ```
478
+ */
479
+ scanIBCTransfers(
480
+ viewingKey: Uint8Array,
481
+ spendingPubKey: Uint8Array,
482
+ spendingPrivateKey: HexString,
483
+ transfers: IncomingIBCTransfer[]
484
+ ): ReceivedStealthTransfer[] {
485
+ const received: ReceivedStealthTransfer[] = []
486
+
487
+ for (const transfer of transfers) {
488
+ try {
489
+ // Parse SIP metadata from memo
490
+ const sipData = this.decodeSIPMemo(transfer.memo)
491
+ if (!sipData) {
492
+ continue // Not a SIP transfer
493
+ }
494
+
495
+ // Parse ephemeral public key from memo
496
+ const ephemeralPubKey = hexToBytes(sipData.sip.ephemeralKey.slice(2))
497
+ const viewingPrivKey = hexToBytes(`0x${bytesToHex(viewingKey)}`.slice(2))
498
+ const spendingPrivKey = hexToBytes(spendingPrivateKey.slice(2))
499
+
500
+ // Compute stealth private key using EIP-5564 algorithm:
501
+ // 1. Compute shared secret: S = spendingPriv * ephemeralPub
502
+ const sharedSecretPoint = secp256k1.getSharedSecret(spendingPrivKey, ephemeralPubKey)
503
+ const sharedSecretHash = sha256(sharedSecretPoint)
504
+
505
+ // 2. Derive stealth private key: stealthPriv = viewingPriv + hash(S) mod n
506
+ const viewingPrivBigInt = bytesToBigInt(viewingPrivKey)
507
+ const hashBigInt = bytesToBigInt(sharedSecretHash)
508
+ const stealthPrivBigInt = (viewingPrivBigInt + hashBigInt) % secp256k1.CURVE.n
509
+
510
+ // Convert to bytes and then to compressed public key
511
+ const stealthPrivKey = bigIntToBytes(stealthPrivBigInt, 32)
512
+ const stealthPubKey = secp256k1.getPublicKey(stealthPrivKey, true)
513
+
514
+ // Convert to Cosmos address
515
+ const derivedAddress = this.stealthService.stealthKeyToCosmosAddress(
516
+ stealthPubKey,
517
+ CHAIN_PREFIXES[transfer.destChain]
518
+ )
519
+
520
+ // Check if this stealth address matches the receiver
521
+ if (derivedAddress === transfer.receiver) {
522
+ // Match! This transfer is for us
523
+ received.push({
524
+ id: transfer.id,
525
+ sourceChain: transfer.sourceChain,
526
+ destChain: transfer.destChain,
527
+ stealthAddress: transfer.receiver,
528
+ amount: transfer.amount,
529
+ denom: transfer.denom,
530
+ ephemeralPublicKey: sipData.sip.ephemeralKey,
531
+ viewTag: sipData.sip.viewTag,
532
+ privateKey: `0x${bytesToHex(stealthPrivKey)}` as HexString,
533
+ height: transfer.height,
534
+ timestamp: transfer.timestamp,
535
+ })
536
+ }
537
+ } catch (error) {
538
+ // Skip this transfer if we can't parse or derive
539
+ continue
540
+ }
541
+ }
542
+
543
+ return received
544
+ }
545
+
546
+ /**
547
+ * Get IBC channel for a chain pair
548
+ *
549
+ * Looks up the IBC channel configuration for transferring tokens
550
+ * from source chain to destination chain.
551
+ *
552
+ * @param sourceChain - Source chain
553
+ * @param destChain - Destination chain
554
+ * @returns IBC channel configuration
555
+ * @throws {ValidationError} If channel is not configured
556
+ *
557
+ * @example
558
+ * ```typescript
559
+ * const service = new CosmosIBCStealthService()
560
+ * const channel = service.getIBCChannel('cosmos', 'osmosis')
561
+ * console.log(channel.sourceChannel) // "channel-141"
562
+ * ```
563
+ */
564
+ getIBCChannel(sourceChain: CosmosChainId, destChain: CosmosChainId): IBCChannel {
565
+ const key = `${sourceChain}-${destChain}`
566
+ const channel = IBC_CHANNELS[key]
567
+
568
+ if (!channel) {
569
+ throw new ValidationError(
570
+ `no IBC channel configured for ${sourceChain} → ${destChain}`,
571
+ 'sourceChain/destChain'
572
+ )
573
+ }
574
+
575
+ return channel
576
+ }
577
+
578
+ /**
579
+ * Encode SIP metadata into IBC memo
580
+ *
581
+ * Creates a JSON memo containing the ephemeral key and view tag.
582
+ * If custom memo text is provided, it's merged with SIP metadata.
583
+ *
584
+ * @param ephemeralKey - Ephemeral public key
585
+ * @param viewTag - View tag
586
+ * @param customMemo - Optional custom memo text
587
+ * @returns JSON-encoded memo string
588
+ */
589
+ private encodeSIPMemo(
590
+ ephemeralKey: HexString,
591
+ viewTag: number,
592
+ customMemo?: string
593
+ ): string {
594
+ const sipData: SIPMemoMetadata = {
595
+ sip: {
596
+ version: 1,
597
+ ephemeralKey,
598
+ viewTag,
599
+ },
600
+ }
601
+
602
+ if (customMemo) {
603
+ return JSON.stringify({
604
+ ...sipData,
605
+ note: customMemo,
606
+ })
607
+ }
608
+
609
+ return JSON.stringify(sipData)
610
+ }
611
+
612
+ /**
613
+ * Decode SIP metadata from IBC memo
614
+ *
615
+ * Parses the JSON memo and extracts SIP metadata.
616
+ *
617
+ * @param memo - IBC memo string
618
+ * @returns Parsed SIP metadata, or null if not a valid SIP memo
619
+ */
620
+ private decodeSIPMemo(memo: string): SIPMemoMetadata | null {
621
+ try {
622
+ const parsed = JSON.parse(memo)
623
+
624
+ // Validate SIP structure
625
+ if (
626
+ !parsed.sip ||
627
+ typeof parsed.sip.version !== 'number' ||
628
+ typeof parsed.sip.ephemeralKey !== 'string' ||
629
+ typeof parsed.sip.viewTag !== 'number'
630
+ ) {
631
+ return null
632
+ }
633
+
634
+ // Validate ephemeral key format
635
+ if (!parsed.sip.ephemeralKey.startsWith('0x') || parsed.sip.ephemeralKey.length !== 68) {
636
+ return null
637
+ }
638
+
639
+ // Validate view tag range
640
+ if (parsed.sip.viewTag < 0 || parsed.sip.viewTag > 255) {
641
+ return null
642
+ }
643
+
644
+ return parsed as SIPMemoMetadata
645
+ } catch {
646
+ return null
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Parse recipient meta-address (string or object)
652
+ *
653
+ * @param metaAddress - Meta-address as string or StealthMetaAddress object
654
+ * @returns StealthMetaAddress object
655
+ * @throws {ValidationError} If format is invalid
656
+ */
657
+ private parseMetaAddress(
658
+ metaAddress: string | StealthMetaAddress
659
+ ): StealthMetaAddress {
660
+ if (typeof metaAddress === 'object') {
661
+ // Validate object has required fields
662
+ if (!metaAddress.spendingKey || !metaAddress.viewingKey || !metaAddress.chain) {
663
+ throw new ValidationError(
664
+ 'meta-address must have spendingKey, viewingKey, and chain',
665
+ 'recipientMetaAddress'
666
+ )
667
+ }
668
+ return metaAddress
669
+ }
670
+
671
+ // Parse string format: sip:chain:spendingKey:viewingKey
672
+ const parts = metaAddress.split(':')
673
+ if (parts.length < 4 || parts[0] !== 'sip') {
674
+ throw new ValidationError(
675
+ 'invalid meta-address format, expected: sip:chain:spendingKey:viewingKey',
676
+ 'recipientMetaAddress'
677
+ )
678
+ }
679
+
680
+ const [, chain, spendingKey, viewingKey] = parts
681
+ return {
682
+ chain: chain as any,
683
+ spendingKey: spendingKey as HexString,
684
+ viewingKey: viewingKey as HexString,
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Validate Cosmos chain ID
690
+ */
691
+ private validateChain(chain: CosmosChainId): void {
692
+ if (!(chain in CHAIN_PREFIXES)) {
693
+ throw new ValidationError(
694
+ `invalid Cosmos chain '${chain}', must be one of: ${Object.keys(CHAIN_PREFIXES).join(', ')}`,
695
+ 'chain'
696
+ )
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Check if a string is a valid SIP memo
702
+ *
703
+ * @param memo - Memo string to check
704
+ * @returns true if memo contains valid SIP metadata
705
+ */
706
+ isSIPMemo(memo: string): boolean {
707
+ return this.decodeSIPMemo(memo) !== null
708
+ }
709
+
710
+ /**
711
+ * Extract custom note from SIP memo (if present)
712
+ *
713
+ * @param memo - IBC memo string
714
+ * @returns Custom note text, or undefined if not present
715
+ */
716
+ extractCustomNote(memo: string): string | undefined {
717
+ try {
718
+ const parsed = JSON.parse(memo)
719
+ return parsed.note
720
+ } catch {
721
+ return undefined
722
+ }
723
+ }
724
+ }
725
+
726
+ // ─── Standalone Functions ──────────────────────────────────────────────────
727
+
728
+ /**
729
+ * Create a stealth IBC transfer
730
+ *
731
+ * Convenience function that creates a CosmosIBCStealthService instance.
732
+ *
733
+ * @param params - Transfer parameters
734
+ * @returns Stealth IBC transfer details
735
+ */
736
+ export function createStealthIBCTransfer(
737
+ params: StealthIBCTransferParams
738
+ ): StealthIBCTransfer {
739
+ const service = new CosmosIBCStealthService()
740
+ return service.createStealthIBCTransfer(params)
741
+ }
742
+
743
+ /**
744
+ * Build IBC MsgTransfer for a stealth transfer
745
+ *
746
+ * Convenience function that creates a CosmosIBCStealthService instance.
747
+ *
748
+ * @param transfer - Stealth IBC transfer details
749
+ * @param senderAddress - Sender's address
750
+ * @param timeoutHeight - Optional timeout height
751
+ * @param timeoutTimestamp - Optional timeout timestamp
752
+ * @returns IBC MsgTransfer message
753
+ */
754
+ export function buildIBCMsgTransfer(
755
+ transfer: StealthIBCTransfer,
756
+ senderAddress: string,
757
+ timeoutHeight?: { revisionNumber: bigint; revisionHeight: bigint },
758
+ timeoutTimestamp?: bigint
759
+ ): IBCMsgTransfer {
760
+ const service = new CosmosIBCStealthService()
761
+ return service.buildIBCMsgTransfer(transfer, senderAddress, timeoutHeight, timeoutTimestamp)
762
+ }
763
+
764
+ /**
765
+ * Scan incoming IBC transfers for stealth payments
766
+ *
767
+ * Convenience function that creates a CosmosIBCStealthService instance.
768
+ *
769
+ * @param viewingKey - Recipient's viewing private key
770
+ * @param spendingPubKey - Recipient's spending public key
771
+ * @param spendingPrivateKey - Recipient's spending private key
772
+ * @param transfers - List of incoming IBC transfers
773
+ * @returns List of detected stealth transfers
774
+ */
775
+ export function scanIBCTransfers(
776
+ viewingKey: Uint8Array,
777
+ spendingPubKey: Uint8Array,
778
+ spendingPrivateKey: HexString,
779
+ transfers: IncomingIBCTransfer[]
780
+ ): ReceivedStealthTransfer[] {
781
+ const service = new CosmosIBCStealthService()
782
+ return service.scanIBCTransfers(viewingKey, spendingPubKey, spendingPrivateKey, transfers)
783
+ }
784
+
785
+ /**
786
+ * Get IBC channel for a chain pair
787
+ *
788
+ * Convenience function that creates a CosmosIBCStealthService instance.
789
+ *
790
+ * @param sourceChain - Source chain
791
+ * @param destChain - Destination chain
792
+ * @returns IBC channel configuration
793
+ */
794
+ export function getIBCChannel(
795
+ sourceChain: CosmosChainId,
796
+ destChain: CosmosChainId
797
+ ): IBCChannel {
798
+ const service = new CosmosIBCStealthService()
799
+ return service.getIBCChannel(sourceChain, destChain)
800
+ }
801
+
802
+ // ─── Utility Functions ──────────────────────────────────────────────────────
803
+
804
+ /**
805
+ * Convert bytes to bigint (big-endian)
806
+ */
807
+ function bytesToBigInt(bytes: Uint8Array): bigint {
808
+ let result = 0n
809
+ for (const byte of bytes) {
810
+ result = (result << 8n) + BigInt(byte)
811
+ }
812
+ return result
813
+ }
814
+
815
+ /**
816
+ * Convert bigint to bytes (big-endian)
817
+ */
818
+ function bigIntToBytes(value: bigint, length: number): Uint8Array {
819
+ const bytes = new Uint8Array(length)
820
+ for (let i = length - 1; i >= 0; i--) {
821
+ bytes[i] = Number(value & 0xffn)
822
+ value >>= 8n
823
+ }
824
+ return bytes
825
+ }