@sip-protocol/sdk 0.9.0 → 0.10.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{TransportWebUSB-YQMAGJAJ.mjs → TransportWebUSB-2KITI5HD.mjs} +24 -12
  3. package/dist/browser.d.mts +4 -4
  4. package/dist/browser.d.ts +4 -4
  5. package/dist/browser.js +1346 -838
  6. package/dist/browser.mjs +13 -3
  7. package/dist/{chunk-64AYA5F5.mjs → chunk-G3TBBG2K.mjs} +221 -146
  8. package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
  9. package/dist/{chunk-6EU6WQFK.mjs → chunk-PT2DNA7E.mjs} +257 -235
  10. package/dist/{constants-LHAAUC2T.mjs → constants-DCJYTIU3.mjs} +5 -1
  11. package/dist/{dist-2OGQ7FED.mjs → dist-PYEXZNFD.mjs} +609 -221
  12. package/dist/{index-DeE1ZzA4.d.mts → index-B1d8pihL.d.mts} +117 -33
  13. package/dist/{index-DXh2IGkz.d.ts → index-UQhQJZbM.d.ts} +117 -33
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1339 -831
  17. package/dist/index.mjs +13 -3
  18. package/dist/{interface-Bf7w1PLW.d.mts → interface-CQi0-WfS.d.mts} +2 -2
  19. package/dist/{interface-Bf7w1PLW.d.ts → interface-CQi0-WfS.d.ts} +2 -2
  20. package/dist/{noir-kzbLVTei.d.mts → noir-CwPIyBLj.d.mts} +1 -1
  21. package/dist/{noir-kzbLVTei.d.ts → noir-CwPIyBLj.d.ts} +1 -1
  22. package/dist/proofs/halo2.d.mts +1 -1
  23. package/dist/proofs/halo2.d.ts +1 -1
  24. package/dist/proofs/kimchi.d.mts +1 -1
  25. package/dist/proofs/kimchi.d.ts +1 -1
  26. package/dist/proofs/noir.d.mts +1 -1
  27. package/dist/proofs/noir.d.ts +1 -1
  28. package/dist/{solana-U3MEGU7W.mjs → solana-ZWNIQTSU.mjs} +6 -6
  29. package/package.json +32 -32
  30. package/src/adapters/gelato-relay.ts +386 -0
  31. package/src/adapters/index.ts +28 -0
  32. package/src/adapters/oneinch.ts +126 -0
  33. package/src/chains/ethereum/privacy-adapter.ts +8 -5
  34. package/src/chains/ethereum/stealth.ts +17 -14
  35. package/src/chains/near/privacy-adapter.ts +8 -5
  36. package/src/chains/near/resolver.ts +22 -8
  37. package/src/chains/near/stealth.ts +9 -9
  38. package/src/chains/solana/constants.ts +13 -1
  39. package/src/chains/solana/ephemeral-keys.ts +3 -257
  40. package/src/chains/solana/index.ts +2 -3
  41. package/src/chains/solana/providers/helius-enhanced.ts +6 -6
  42. package/src/chains/solana/providers/webhook.ts +2 -2
  43. package/src/chains/solana/scan.ts +9 -8
  44. package/src/chains/solana/stealth-scanner.ts +3 -3
  45. package/src/chains/solana/types.ts +18 -4
  46. package/src/cosmos/ibc-stealth.ts +6 -6
  47. package/src/index.ts +6 -0
  48. package/src/move/aptos.ts +15 -9
  49. package/src/move/sui.ts +15 -9
  50. package/src/nft/private-nft.ts +10 -6
  51. package/src/privacy-backends/shadowwire.ts +13 -0
  52. package/src/stealth/ed25519.ts +173 -12
  53. package/src/stealth/index.ts +47 -4
  54. package/src/stealth/secp256k1.ts +144 -7
  55. package/src/stealth.ts +7 -0
  56. package/src/wallet/ethereum/privacy-adapter.ts +1 -1
  57. package/src/wallet/hardware/ledger-privacy.ts +2 -2
  58. package/src/wallet/near/adapter.ts +2 -2
  59. package/src/wallet/near/meteor-wallet.ts +2 -2
  60. package/src/wallet/near/my-near-wallet.ts +2 -2
  61. package/src/wallet/near/wallet-selector.ts +2 -2
  62. package/src/wallet/solana/privacy-adapter.ts +9 -9
  63. package/dist/chunk-5EKF243P.mjs +0 -33809
  64. package/dist/chunk-YWGJ77A2.mjs +0 -33806
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Gelato Relay Adapter for SIP Protocol
3
+ *
4
+ * Enables gasless claim and withdrawal from stealth addresses via Gelato Relay.
5
+ * Recipients can claim funds without holding ETH for gas — critical for stealth
6
+ * addresses which are freshly generated and have zero balance.
7
+ *
8
+ * ## Two Modes
9
+ *
10
+ * - **sponsoredCall**: SIP pays gas from Gas Tank (requires API key)
11
+ * - **callWithSyncFee**: Fee deducted from withdrawal amount (requires SIPRelayer)
12
+ *
13
+ * ```
14
+ * ┌──────────────────────────────────────────────────────────┐
15
+ * │ GELATO RELAY + SIP PRIVACY FLOW │
16
+ * │ │
17
+ * │ Sponsored Mode: │
18
+ * │ 1. Recipient calls sponsoredClaim() with proof │
19
+ * │ 2. Gelato relays tx to SIPPrivacy.withdrawDeposit() │
20
+ * │ 3. SIP Gas Tank pays the gas fee │
21
+ * │ 4. Funds arrive at recipient stealth address │
22
+ * │ │
23
+ * │ SyncFee Mode: │
24
+ * │ 1. Recipient calls syncFeeClaim() with proof + maxFee │
25
+ * │ 2. Gelato relays tx to SIPRelayer contract │
26
+ * │ 3. SIPRelayer calls SIPPrivacy.withdrawDeposit() │
27
+ * │ 4. SIPRelayer deducts gas fee from withdrawn amount │
28
+ * │ 5. Remainder arrives at recipient stealth address │
29
+ * │ │
30
+ * │ Result: Zero-gas claims from stealth addresses │
31
+ * └──────────────────────────────────────────────────────────┘
32
+ * ```
33
+ *
34
+ * ## ABI Encoding
35
+ *
36
+ * Uses manual ABI encoding with @noble/hashes keccak256 for function selectors.
37
+ * No ethers.js or viem dependency required.
38
+ *
39
+ * @see https://docs.gelato.network/web3-services/relay
40
+ */
41
+
42
+ import { keccak_256 } from '@noble/hashes/sha3'
43
+ import { bytesToHex } from '@noble/hashes/utils'
44
+
45
+ // ═══════════════════════════════════════════
46
+ // Types
47
+ // ═══════════════════════════════════════════
48
+
49
+ export interface GelatoRelayConfig {
50
+ /** Gelato Gas Tank API key (required for sponsoredCall) */
51
+ apiKey?: string
52
+ /** Target chain ID (e.g. 11155111 for Sepolia) */
53
+ chainId: number
54
+ /** SIPPrivacy contract address */
55
+ sipPrivacyAddress: string
56
+ /** SIPRelayer contract address (required for callWithSyncFee) */
57
+ sipRelayerAddress?: string
58
+ }
59
+
60
+ export interface RelayClaimParams {
61
+ /** Deposit transfer ID */
62
+ transferId: bigint
63
+ /** Nullifier hash (bytes32 hex) */
64
+ nullifier: string
65
+ /** ZK proof (bytes hex) */
66
+ proof: string
67
+ /** Recipient address (20-byte hex) */
68
+ recipient: string
69
+ }
70
+
71
+ export interface SyncFeeClaimParams extends RelayClaimParams {
72
+ /** Token to pay Gelato fee in */
73
+ feeToken: string
74
+ /** Maximum fee willing to pay */
75
+ maxFee: bigint
76
+ /** ERC20 token address (omit or zero address for ETH) */
77
+ token?: string
78
+ }
79
+
80
+ export interface RelayResult {
81
+ /** Gelato task ID for tracking */
82
+ taskId: string
83
+ /** Which relay mode was used */
84
+ mode: 'sponsored' | 'syncFee'
85
+ }
86
+
87
+ export type TaskStatus =
88
+ | 'CheckPending'
89
+ | 'ExecPending'
90
+ | 'ExecSuccess'
91
+ | 'ExecReverted'
92
+ | 'Cancelled'
93
+
94
+ export interface TaskStatusResult {
95
+ /** Gelato task ID */
96
+ taskId: string
97
+ /** Current task state */
98
+ taskState: TaskStatus
99
+ /** Transaction hash (available after execution) */
100
+ transactionHash?: string
101
+ /** Block number (available after execution) */
102
+ blockNumber?: number
103
+ }
104
+
105
+ // ═══════════════════════════════════════════
106
+ // Constants
107
+ // ═══════════════════════════════════════════
108
+
109
+ const GELATO_RELAY_URL = 'https://relay.gelato.digital'
110
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
111
+
112
+ // ═══════════════════════════════════════════
113
+ // ABI Encoding Helpers (no ethers/viem)
114
+ // ═══════════════════════════════════════════
115
+
116
+ /**
117
+ * Compute 4-byte function selector from Solidity signature.
118
+ * selector = keccak256(signature)[0:4]
119
+ */
120
+ export function functionSelector(signature: string): string {
121
+ const hash = keccak_256(new TextEncoder().encode(signature))
122
+ return '0x' + bytesToHex(hash).slice(0, 8)
123
+ }
124
+
125
+ /** Left-pad a bigint to 32 bytes (64 hex chars) */
126
+ function padUint256(value: bigint): string {
127
+ if (value < 0n) throw new Error(`uint256 cannot be negative: ${value}`)
128
+ if (value >= 2n ** 256n) throw new Error(`uint256 overflow: ${value}`)
129
+ return value.toString(16).padStart(64, '0')
130
+ }
131
+
132
+ /** Left-pad an address to 32 bytes (64 hex chars) */
133
+ function padAddress(addr: string): string {
134
+ const clean = addr.startsWith('0x') ? addr.slice(2) : addr
135
+ if (clean.length !== 40) {
136
+ throw new Error(`Invalid address length: expected 40 hex chars, got ${clean.length}`)
137
+ }
138
+ return clean.toLowerCase().padStart(64, '0')
139
+ }
140
+
141
+ /** Right-pad a bytes32 value to 32 bytes (64 hex chars) */
142
+ function padBytes32(value: string): string {
143
+ const clean = value.startsWith('0x') ? value.slice(2) : value
144
+ if (clean.length > 64) {
145
+ throw new Error(`bytes32 value too long: ${clean.length} hex chars (max 64)`)
146
+ }
147
+ return clean.padEnd(64, '0')
148
+ }
149
+
150
+ /**
151
+ * ABI-encode a dynamic `bytes` value.
152
+ * Returns: length (32 bytes) + data (padded to 32-byte boundary)
153
+ */
154
+ function encodeBytes(value: string): string {
155
+ const clean = value.startsWith('0x') ? value.slice(2) : value
156
+ if (clean.length % 2 !== 0) {
157
+ throw new Error(`bytes value must have even hex length, got ${clean.length}`)
158
+ }
159
+ const length = clean.length / 2
160
+ const paddedData = clean.length % 64 === 0
161
+ ? clean
162
+ : clean + '0'.repeat(64 - (clean.length % 64))
163
+ return padUint256(BigInt(length)) + paddedData
164
+ }
165
+
166
+ // ═══════════════════════════════════════════
167
+ // GelatoRelayAdapter
168
+ // ═══════════════════════════════════════════
169
+
170
+ export class GelatoRelayAdapter {
171
+ private config: GelatoRelayConfig
172
+
173
+ constructor(config: GelatoRelayConfig) {
174
+ if (!config.sipPrivacyAddress) {
175
+ throw new Error('sipPrivacyAddress is required')
176
+ }
177
+ if (!config.chainId) {
178
+ throw new Error('chainId is required')
179
+ }
180
+ this.config = config
181
+ }
182
+
183
+ /**
184
+ * Gasless withdrawal via sponsoredCall (SIP pays gas from Gas Tank).
185
+ * Calls SIPPrivacy.withdrawDeposit() directly.
186
+ */
187
+ async sponsoredClaim(params: RelayClaimParams): Promise<RelayResult> {
188
+ if (!this.config.apiKey) {
189
+ throw new Error('API key required for sponsoredCall')
190
+ }
191
+
192
+ const data = this.encodeWithdrawDeposit(params)
193
+
194
+ const response = await fetch(`${GELATO_RELAY_URL}/relays/v2/sponsored-call`, {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify({
198
+ chainId: this.config.chainId,
199
+ target: this.config.sipPrivacyAddress,
200
+ data,
201
+ sponsorApiKey: this.config.apiKey,
202
+ }),
203
+ })
204
+
205
+ if (!response.ok) {
206
+ const text = await response.text().catch(() => response.statusText)
207
+ throw new Error(`Gelato relay error: ${response.status} ${text}`)
208
+ }
209
+
210
+ const result = await response.json()
211
+ return { taskId: result.taskId, mode: 'sponsored' }
212
+ }
213
+
214
+ /**
215
+ * Gasless withdrawal via callWithSyncFee (fee deducted from withdrawn amount).
216
+ * Routes through SIPRelayer contract which handles fee deduction.
217
+ *
218
+ * - ETH withdrawals: calls relayedWithdrawETH()
219
+ * - ERC20 withdrawals: calls relayedWithdrawToken() (when params.token is set)
220
+ */
221
+ async syncFeeClaim(params: SyncFeeClaimParams): Promise<RelayResult> {
222
+ if (!this.config.sipRelayerAddress) {
223
+ throw new Error('SIPRelayer address required for callWithSyncFee')
224
+ }
225
+
226
+ const isToken = params.token && params.token !== ZERO_ADDRESS
227
+ const data = isToken
228
+ ? this.encodeRelayedWithdrawToken(params)
229
+ : this.encodeRelayedWithdrawETH(params)
230
+
231
+ const response = await fetch(`${GELATO_RELAY_URL}/relays/v2/call-with-sync-fee`, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({
235
+ chainId: this.config.chainId,
236
+ target: this.config.sipRelayerAddress,
237
+ data,
238
+ feeToken: params.feeToken,
239
+ isRelayContext: true,
240
+ }),
241
+ })
242
+
243
+ if (!response.ok) {
244
+ const text = await response.text().catch(() => response.statusText)
245
+ throw new Error(`Gelato relay error: ${response.status} ${text}`)
246
+ }
247
+
248
+ const result = await response.json()
249
+ return { taskId: result.taskId, mode: 'syncFee' }
250
+ }
251
+
252
+ /**
253
+ * Check relay task status.
254
+ * Poll this after submitting a relay request to track execution.
255
+ */
256
+ async getTaskStatus(taskId: string): Promise<TaskStatusResult> {
257
+ if (!taskId) {
258
+ throw new Error('taskId is required')
259
+ }
260
+ if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) {
261
+ throw new Error(`Invalid taskId format: ${taskId}`)
262
+ }
263
+
264
+ const response = await fetch(`${GELATO_RELAY_URL}/tasks/status/${taskId}`)
265
+
266
+ if (!response.ok) {
267
+ throw new Error(`Gelato status error: ${response.status}`)
268
+ }
269
+
270
+ const result = await response.json()
271
+ return {
272
+ taskId: result.task.taskId,
273
+ taskState: result.task.taskState,
274
+ transactionHash: result.task.transactionHash,
275
+ blockNumber: result.task.blockNumber,
276
+ }
277
+ }
278
+
279
+ // ═══════════════════════════════════════════
280
+ // ABI Encoding (no ethers/viem dependency)
281
+ // ═══════════════════════════════════════════
282
+
283
+ /**
284
+ * Encode withdrawDeposit(uint256,bytes32,bytes,address)
285
+ *
286
+ * ABI layout:
287
+ * [selector 4B]
288
+ * [0x00] transferId — uint256
289
+ * [0x20] nullifier — bytes32
290
+ * [0x40] proof offset — uint256 (points to 0x80)
291
+ * [0x60] recipient — address
292
+ * [0x80] proof length — uint256
293
+ * [0xa0] proof data — bytes (padded)
294
+ */
295
+ private encodeWithdrawDeposit(params: RelayClaimParams): string {
296
+ const selector = functionSelector('withdrawDeposit(uint256,bytes32,bytes,address)')
297
+ const proofOffset = padUint256(128n) // 4 slots * 32 = 128
298
+
299
+ return selector
300
+ + padUint256(params.transferId)
301
+ + padBytes32(params.nullifier)
302
+ + proofOffset
303
+ + padAddress(params.recipient)
304
+ + encodeBytes(params.proof)
305
+ }
306
+
307
+ /**
308
+ * Encode relayedWithdrawETH(uint256,bytes32,bytes,address,uint256)
309
+ *
310
+ * ABI layout:
311
+ * [selector 4B]
312
+ * [0x00] transferId — uint256
313
+ * [0x20] nullifier — bytes32
314
+ * [0x40] proof offset — uint256 (points to 0xa0)
315
+ * [0x60] recipient — address
316
+ * [0x80] maxFee — uint256
317
+ * [0xa0] proof length — uint256
318
+ * [0xc0] proof data — bytes (padded)
319
+ */
320
+ private encodeRelayedWithdrawETH(params: SyncFeeClaimParams): string {
321
+ const selector = functionSelector('relayedWithdrawETH(uint256,bytes32,bytes,address,uint256)')
322
+ const proofOffset = padUint256(160n) // 5 slots * 32 = 160
323
+
324
+ return selector
325
+ + padUint256(params.transferId)
326
+ + padBytes32(params.nullifier)
327
+ + proofOffset
328
+ + padAddress(params.recipient)
329
+ + padUint256(params.maxFee)
330
+ + encodeBytes(params.proof)
331
+ }
332
+
333
+ /**
334
+ * Encode relayedWithdrawToken(uint256,bytes32,bytes,address,address,uint256)
335
+ *
336
+ * ABI layout:
337
+ * [selector 4B]
338
+ * [0x00] transferId — uint256
339
+ * [0x20] nullifier — bytes32
340
+ * [0x40] proof offset — uint256 (points to 0xc0)
341
+ * [0x60] recipient — address
342
+ * [0x80] token — address
343
+ * [0xa0] maxFee — uint256
344
+ * [0xc0] proof length — uint256
345
+ * [0xe0] proof data — bytes (padded)
346
+ */
347
+ private encodeRelayedWithdrawToken(params: SyncFeeClaimParams): string {
348
+ const selector = functionSelector('relayedWithdrawToken(uint256,bytes32,bytes,address,address,uint256)')
349
+ const proofOffset = padUint256(192n) // 6 slots * 32 = 192
350
+
351
+ return selector
352
+ + padUint256(params.transferId)
353
+ + padBytes32(params.nullifier)
354
+ + proofOffset
355
+ + padAddress(params.recipient)
356
+ + padAddress(params.token!)
357
+ + padUint256(params.maxFee)
358
+ + encodeBytes(params.proof)
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Factory function for creating a GelatoRelayAdapter.
364
+ *
365
+ * @example
366
+ * ```typescript
367
+ * // Sponsored mode (SIP pays gas)
368
+ * const relay = createGelatoRelayAdapter({
369
+ * apiKey: 'gelato-api-key',
370
+ * chainId: 11155111,
371
+ * sipPrivacyAddress: '0x1FED...',
372
+ * })
373
+ * const result = await relay.sponsoredClaim({ transferId, nullifier, proof, recipient })
374
+ *
375
+ * // SyncFee mode (fee from withdrawal)
376
+ * const relay = createGelatoRelayAdapter({
377
+ * chainId: 11155111,
378
+ * sipPrivacyAddress: '0x1FED...',
379
+ * sipRelayerAddress: '0xABC...',
380
+ * })
381
+ * const result = await relay.syncFeeClaim({ transferId, nullifier, proof, recipient, feeToken, maxFee })
382
+ * ```
383
+ */
384
+ export function createGelatoRelayAdapter(config: GelatoRelayConfig): GelatoRelayAdapter {
385
+ return new GelatoRelayAdapter(config)
386
+ }
@@ -11,6 +11,9 @@
11
11
  * ### Solana DEX
12
12
  * - **JupiterAdapter** — Jupiter aggregator for private Solana swaps
13
13
  *
14
+ * ### EVM Relay
15
+ * - **GelatoRelayAdapter** — Gasless claims from stealth addresses via Gelato Relay
16
+ *
14
17
  * @example
15
18
  * ```typescript
16
19
  * import { JupiterAdapter, NEARIntentsAdapter } from '@sip-protocol/sdk'
@@ -58,3 +61,28 @@ export type {
58
61
  JupiterSwapResult,
59
62
  JupiterPrivateSwapResult,
60
63
  } from './jupiter'
64
+
65
+ // 1inch Aggregator (EVM)
66
+ export { OneInchAdapter } from './oneinch'
67
+
68
+ export type {
69
+ OneInchQuote,
70
+ OneInchSwapData,
71
+ OneInchSwapParams,
72
+ } from './oneinch'
73
+
74
+ // Gelato Relay (EVM gasless)
75
+ export {
76
+ GelatoRelayAdapter,
77
+ createGelatoRelayAdapter,
78
+ functionSelector,
79
+ } from './gelato-relay'
80
+
81
+ export type {
82
+ GelatoRelayConfig,
83
+ RelayClaimParams,
84
+ SyncFeeClaimParams,
85
+ RelayResult,
86
+ TaskStatus,
87
+ TaskStatusResult,
88
+ } from './gelato-relay'
@@ -0,0 +1,126 @@
1
+ /**
2
+ * 1inch Aggregator Adapter for SIP Protocol
3
+ *
4
+ * Generates swap calldata for privacy-preserving EVM swaps via 1inch aggregator.
5
+ * SIP adds stealth addresses as the swap recipient, breaking the on-chain identity link.
6
+ *
7
+ * ## Privacy Model
8
+ *
9
+ * 1inch swaps are routed through 200+ DEX pools for best price. SIP enhances privacy by:
10
+ * - Setting `destReceiver` to a stealth address (recipient unlinkable)
11
+ * - SIPSwapRouter validates calldata on-chain before forwarding to 1inch
12
+ *
13
+ * ```
14
+ * ┌──────────────────────────────────────────────────────────────┐
15
+ * │ 1INCH + SIP PRIVACY FLOW │
16
+ * │ │
17
+ * │ 1. SDK: Generate stealth address for output │
18
+ * │ 2. SDK: Call 1inch API (destReceiver = stealth) │
19
+ * │ 3. SDK: Submit calldata to SIPSwapRouter │
20
+ * │ 4. Contract: Validate calldata, deduct fee, forward │
21
+ * │ 5. 1inch Router → DEX pools → stealth address │
22
+ * │ │
23
+ * │ Result: Best-price swap, recipient unlinkable │
24
+ * └──────────────────────────────────────────────────────────────┘
25
+ * ```
26
+ *
27
+ * @see https://portal.1inch.dev
28
+ */
29
+
30
+ export interface OneInchQuote {
31
+ toAmount: string
32
+ estimatedGas: string
33
+ protocols: Array<{ name: string; part: number }>
34
+ }
35
+
36
+ export interface OneInchSwapData {
37
+ tx: {
38
+ to: string
39
+ data: string
40
+ value: string
41
+ gas: number
42
+ }
43
+ toAmount: string
44
+ }
45
+
46
+ export interface OneInchSwapParams {
47
+ src: string
48
+ dst: string
49
+ amount: string
50
+ from: string
51
+ destReceiver: string
52
+ slippage: number
53
+ disableEstimate?: boolean
54
+ }
55
+
56
+ const CHAIN_IDS: Record<string, number> = {
57
+ ethereum: 1,
58
+ arbitrum: 42161,
59
+ optimism: 10,
60
+ base: 8453,
61
+ polygon: 137,
62
+ }
63
+
64
+ const ONEINCH_ROUTER = '0x111111125421cA6dc452d289314280a0f8842A65'
65
+ const API_BASE = 'https://api.1inch.dev/swap/v6.0'
66
+
67
+ export class OneInchAdapter {
68
+ private apiKey: string
69
+ private chainId: number
70
+
71
+ constructor(apiKey: string, chain: string | number) {
72
+ this.apiKey = apiKey
73
+ this.chainId = typeof chain === 'number' ? chain : CHAIN_IDS[chain]
74
+ if (!this.chainId) throw new Error(`Unsupported chain: ${chain}`)
75
+ }
76
+
77
+ get routerAddress(): string {
78
+ return ONEINCH_ROUTER
79
+ }
80
+
81
+ async getQuote(params: {
82
+ src: string
83
+ dst: string
84
+ amount: string
85
+ }): Promise<OneInchQuote> {
86
+ const url = new URL(`${API_BASE}/${this.chainId}/quote`)
87
+ url.searchParams.set('src', params.src)
88
+ url.searchParams.set('dst', params.dst)
89
+ url.searchParams.set('amount', params.amount)
90
+
91
+ const response = await fetch(url.toString(), {
92
+ headers: { Authorization: `Bearer ${this.apiKey}` },
93
+ })
94
+
95
+ if (!response.ok) {
96
+ throw new Error(`1inch API error: ${response.status} ${response.statusText}`)
97
+ }
98
+
99
+ return response.json()
100
+ }
101
+
102
+ async getSwapCalldata(params: OneInchSwapParams): Promise<OneInchSwapData> {
103
+ const url = new URL(`${API_BASE}/${this.chainId}/swap`)
104
+ url.searchParams.set('src', params.src)
105
+ url.searchParams.set('dst', params.dst)
106
+ url.searchParams.set('amount', params.amount)
107
+ url.searchParams.set('from', params.from)
108
+ url.searchParams.set('destReceiver', params.destReceiver)
109
+ url.searchParams.set('slippage', params.slippage.toString())
110
+ url.searchParams.set('disableEstimate', (params.disableEstimate ?? true).toString())
111
+
112
+ const response = await fetch(url.toString(), {
113
+ headers: { Authorization: `Bearer ${this.apiKey}` },
114
+ })
115
+
116
+ if (!response.ok) {
117
+ throw new Error(`1inch API error: ${response.status} ${response.statusText}`)
118
+ }
119
+
120
+ return response.json()
121
+ }
122
+
123
+ static supportedChains(): string[] {
124
+ return Object.keys(CHAIN_IDS)
125
+ }
126
+ }
@@ -363,20 +363,23 @@ export class EthereumPrivacyAdapter {
363
363
  /**
364
364
  * Check if a stealth address belongs to a recipient
365
365
  *
366
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
367
+ * private key plus their spending PUBLIC key (no spending private key needed).
368
+ *
366
369
  * @param stealthAddress - Stealth address object
367
- * @param spendingPrivateKey - Spending private key (hex)
368
370
  * @param viewingPrivateKey - Viewing private key (hex)
371
+ * @param spendingPublicKey - Spending public key (hex, meta-address spendingKey)
369
372
  * @returns True if the address belongs to the recipient
370
373
  */
371
374
  checkStealthAddress(
372
375
  stealthAddress: StealthAddress,
373
- spendingPrivateKey: HexString,
374
- viewingPrivateKey: HexString
376
+ viewingPrivateKey: HexString,
377
+ spendingPublicKey: HexString
375
378
  ): boolean {
376
379
  return checkEthereumStealthAddress(
377
380
  stealthAddress,
378
- spendingPrivateKey,
379
- viewingPrivateKey
381
+ viewingPrivateKey,
382
+ spendingPublicKey
380
383
  )
381
384
  }
382
385
 
@@ -348,9 +348,12 @@ export function deriveEthereumStealthPrivateKey(
348
348
  *
349
349
  * Used during scanning to quickly filter announcements.
350
350
  *
351
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
352
+ * private key plus their spending PUBLIC key (no spending private key needed).
353
+ *
351
354
  * @param stealthAddress - The stealth address to check
352
- * @param spendingPrivateKey - Recipient's spending private key
353
355
  * @param viewingPrivateKey - Recipient's viewing private key
356
+ * @param spendingPublicKey - Recipient's spending public key (meta-address spendingKey)
354
357
  * @returns True if the address belongs to this recipient
355
358
  *
356
359
  * @example
@@ -359,8 +362,8 @@ export function deriveEthereumStealthPrivateKey(
359
362
  * for (const announcement of announcements) {
360
363
  * const isMine = checkEthereumStealthAddress(
361
364
  * announcement.stealthAddress,
362
- * mySpendingPrivateKey,
363
- * myViewingPrivateKey
365
+ * myViewingPrivateKey,
366
+ * mySpendingPublicKey
364
367
  * )
365
368
  * if (isMine) {
366
369
  * console.log('Found incoming payment!')
@@ -370,13 +373,13 @@ export function deriveEthereumStealthPrivateKey(
370
373
  */
371
374
  export function checkEthereumStealthAddress(
372
375
  stealthAddress: StealthAddress,
373
- spendingPrivateKey: HexString,
374
- viewingPrivateKey: HexString
376
+ viewingPrivateKey: HexString,
377
+ spendingPublicKey: HexString
375
378
  ): boolean {
376
379
  return checkSecp256k1StealthAddress(
377
380
  stealthAddress,
378
- spendingPrivateKey,
379
- viewingPrivateKey
381
+ viewingPrivateKey,
382
+ spendingPublicKey
380
383
  )
381
384
  }
382
385
 
@@ -419,10 +422,10 @@ export function checkEthereumStealthByEthAddress(
419
422
  const ephemeralPubBytes = hexToBytes(ephemeralPublicKey.slice(2))
420
423
 
421
424
  try {
422
- // Compute shared secret: S = spendingPrivateKey * ephemeralPublicKey
423
- // Mirrors generation: S = ephemeralPrivate * spendingPublic
425
+ // Compute shared secret: S = viewingPrivateKey * ephemeralPublicKey
426
+ // Canonical EIP-5564: ECDH on the viewing key (mirrors generation S = r * K_view)
424
427
  const sharedSecretPoint = secp256k1.getSharedSecret(
425
- spendingPrivBytes,
428
+ viewingPrivBytes,
426
429
  ephemeralPubBytes,
427
430
  )
428
431
  const sharedSecretHash = sha256(sharedSecretPoint)
@@ -432,11 +435,11 @@ export function checkEthereumStealthByEthAddress(
432
435
  return null
433
436
  }
434
437
 
435
- // Derive stealth private key: viewingPriv + hash(S) mod n
436
- // Mirrors generation: stealth = viewingPub + hash(S)*G
437
- const viewingScalar = BigInt('0x' + bytesToHex(viewingPrivBytes))
438
+ // Derive stealth private key: spendingPriv + hash(S) mod n
439
+ // Mirrors generation: stealth = spendingPub + hash(S)*G
440
+ const spendingScalar = BigInt('0x' + bytesToHex(spendingPrivBytes))
438
441
  const hashScalar = BigInt('0x' + bytesToHex(sharedSecretHash))
439
- const stealthPrivScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n
442
+ const stealthPrivScalar = (spendingScalar + hashScalar) % secp256k1.CURVE.n
440
443
 
441
444
  // Compute expected public key from derived private key
442
445
  const stealthPrivHex = stealthPrivScalar.toString(16).padStart(64, '0')
@@ -426,20 +426,23 @@ export class NEARPrivacyAdapter {
426
426
  /**
427
427
  * Check if a stealth address belongs to a recipient
428
428
  *
429
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
430
+ * private key plus their spending PUBLIC key (no spending private key needed).
431
+ *
429
432
  * @param stealthAddress - Stealth address object
430
- * @param spendingPrivateKey - Spending private key (hex)
431
433
  * @param viewingPrivateKey - Viewing private key (hex)
434
+ * @param spendingPublicKey - Spending public key (hex, meta-address spendingKey)
432
435
  * @returns True if the address belongs to the recipient
433
436
  */
434
437
  checkStealthAddress(
435
438
  stealthAddress: StealthAddress,
436
- spendingPrivateKey: HexString,
437
- viewingPrivateKey: HexString
439
+ viewingPrivateKey: HexString,
440
+ spendingPublicKey: HexString
438
441
  ): boolean {
439
442
  return checkNEARStealthAddress(
440
443
  stealthAddress,
441
- spendingPrivateKey,
442
- viewingPrivateKey
444
+ viewingPrivateKey,
445
+ spendingPublicKey
443
446
  )
444
447
  }
445
448