@sip-protocol/sdk 0.3.2 → 0.5.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/dist/browser.d.mts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2881 -295
- package/dist/browser.mjs +62 -2
- package/dist/chunk-AOZIY3GU.mjs +12995 -0
- package/dist/chunk-BCLIX5T2.mjs +12940 -0
- package/dist/chunk-DMHBKRWV.mjs +14712 -0
- package/dist/chunk-FKXPHKYD.mjs +12955 -0
- package/dist/chunk-HGU6HZRC.mjs +231 -0
- package/dist/chunk-J4Q4NJ2U.mjs +13544 -0
- package/dist/chunk-OPQ2GQIO.mjs +13013 -0
- package/dist/chunk-W2B7T6WU.mjs +14714 -0
- package/dist/index-5jAdWMA-.d.ts +8973 -0
- package/dist/index-B9Vkpaao.d.mts +8973 -0
- package/dist/index-BcWNakUD.d.ts +7990 -0
- package/dist/index-BsKY3Hr0.d.mts +7990 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2852 -266
- package/dist/index.mjs +62 -2
- package/dist/proofs/noir.mjs +1 -1
- package/package.json +2 -1
- package/src/adapters/near-intents.ts +8 -0
- package/src/bitcoin/index.ts +51 -0
- package/src/bitcoin/silent-payments.ts +865 -0
- package/src/bitcoin/taproot.ts +590 -0
- package/src/compliance/compliance-manager.ts +87 -0
- package/src/compliance/conditional-threshold.ts +379 -0
- package/src/compliance/conditional.ts +382 -0
- package/src/compliance/derivation.ts +489 -0
- package/src/compliance/index.ts +50 -8
- package/src/compliance/pdf.ts +365 -0
- package/src/compliance/reports.ts +644 -0
- package/src/compliance/threshold.ts +529 -0
- package/src/compliance/types.ts +223 -0
- package/src/cosmos/ibc-stealth.ts +825 -0
- package/src/cosmos/index.ts +83 -0
- package/src/cosmos/stealth.ts +487 -0
- package/src/errors.ts +8 -0
- package/src/index.ts +80 -1
- package/src/move/aptos.ts +369 -0
- package/src/move/index.ts +35 -0
- package/src/move/sui.ts +367 -0
- package/src/oracle/types.ts +8 -0
- package/src/settlement/backends/direct-chain.ts +8 -0
- package/src/stealth.ts +3 -3
- package/src/validation.ts +42 -1
- package/src/wallet/aptos/adapter.ts +422 -0
- package/src/wallet/aptos/index.ts +10 -0
- package/src/wallet/aptos/mock.ts +410 -0
- package/src/wallet/aptos/types.ts +278 -0
- package/src/wallet/bitcoin/adapter.ts +470 -0
- package/src/wallet/bitcoin/index.ts +38 -0
- package/src/wallet/bitcoin/mock.ts +516 -0
- package/src/wallet/bitcoin/types.ts +274 -0
- package/src/wallet/cosmos/adapter.ts +484 -0
- package/src/wallet/cosmos/index.ts +63 -0
- package/src/wallet/cosmos/mock.ts +596 -0
- package/src/wallet/cosmos/types.ts +462 -0
- package/src/wallet/index.ts +127 -0
- package/src/wallet/sui/adapter.ts +471 -0
- package/src/wallet/sui/index.ts +10 -0
- package/src/wallet/sui/mock.ts +439 -0
- 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
|
+
}
|