@sip-protocol/sdk 0.9.0 → 0.11.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 (70) 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 +1363 -844
  6. package/dist/browser.mjs +13 -3
  7. package/dist/{chunk-64AYA5F5.mjs → chunk-7IUKXWDN.mjs} +229 -148
  8. package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
  9. package/dist/{chunk-6EU6WQFK.mjs → chunk-L4RKPNIJ.mjs} +266 -239
  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-Cwo3WhxX.d.mts} +128 -37
  13. package/dist/{index-DXh2IGkz.d.ts → index-X8qPQdp6.d.ts} +128 -37
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1356 -837
  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-7QOA3HBZ.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/index.ts +2 -0
  34. package/src/chains/ethereum/privacy-adapter.ts +64 -5
  35. package/src/chains/ethereum/stealth.ts +89 -14
  36. package/src/chains/ethereum/types.ts +18 -2
  37. package/src/chains/near/constants.ts +13 -1
  38. package/src/chains/near/index.ts +2 -0
  39. package/src/chains/near/privacy-adapter.ts +8 -5
  40. package/src/chains/near/resolver.ts +24 -10
  41. package/src/chains/near/stealth.ts +9 -9
  42. package/src/chains/near/types.ts +20 -9
  43. package/src/chains/solana/constants.ts +13 -1
  44. package/src/chains/solana/ephemeral-keys.ts +3 -257
  45. package/src/chains/solana/index.ts +2 -3
  46. package/src/chains/solana/providers/helius-enhanced.ts +6 -6
  47. package/src/chains/solana/providers/webhook.ts +2 -2
  48. package/src/chains/solana/scan.ts +9 -8
  49. package/src/chains/solana/stealth-scanner.ts +3 -3
  50. package/src/chains/solana/transfer.ts +1 -1
  51. package/src/chains/solana/types.ts +18 -4
  52. package/src/cosmos/ibc-stealth.ts +6 -6
  53. package/src/index.ts +6 -0
  54. package/src/move/aptos.ts +15 -9
  55. package/src/move/sui.ts +15 -9
  56. package/src/nft/private-nft.ts +10 -6
  57. package/src/privacy-backends/shadowwire.ts +13 -0
  58. package/src/stealth/ed25519.ts +173 -12
  59. package/src/stealth/index.ts +47 -4
  60. package/src/stealth/secp256k1.ts +157 -9
  61. package/src/stealth.ts +7 -0
  62. package/src/wallet/ethereum/privacy-adapter.ts +1 -1
  63. package/src/wallet/hardware/ledger-privacy.ts +2 -2
  64. package/src/wallet/near/adapter.ts +2 -2
  65. package/src/wallet/near/meteor-wallet.ts +2 -2
  66. package/src/wallet/near/my-near-wallet.ts +2 -2
  67. package/src/wallet/near/wallet-selector.ts +2 -2
  68. package/src/wallet/solana/privacy-adapter.ts +9 -9
  69. package/dist/chunk-5EKF243P.mjs +0 -33809
  70. 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
+ }
@@ -67,6 +67,7 @@ export type {
67
67
  EthereumPrivacyAdapterState,
68
68
  EthereumScanRecipient,
69
69
  EthereumDetectedPaymentResult,
70
+ EthereumViewOnlyDetectionResult,
70
71
  } from './types'
71
72
 
72
73
  // ─── Stealth Addresses ────────────────────────────────────────────────────────
@@ -85,6 +86,7 @@ export {
85
86
  deriveEthereumStealthPrivateKey,
86
87
  checkEthereumStealthAddress,
87
88
  checkEthereumStealthByEthAddress,
89
+ checkEthereumStealthByEthAddressViewOnly,
88
90
  stealthPublicKeyToEthAddress,
89
91
  extractPublicKeys,
90
92
  createMetaAddressFromPublicKeys,
@@ -18,6 +18,7 @@ import {
18
18
  deriveEthereumStealthPrivateKey,
19
19
  checkEthereumStealthAddress,
20
20
  checkEthereumStealthByEthAddress,
21
+ checkEthereumStealthByEthAddressViewOnly,
21
22
  stealthPublicKeyToEthAddress,
22
23
  type EthereumStealthMetaAddress,
23
24
  type EthereumStealthAddress,
@@ -59,6 +60,7 @@ import type {
59
60
  EthereumPrivacyAdapterState,
60
61
  EthereumScanRecipient,
61
62
  EthereumDetectedPaymentResult,
63
+ EthereumViewOnlyDetectionResult,
62
64
  EthereumViewingKeyExport,
63
65
  EthereumViewingKeyPair,
64
66
  EthereumPedersenCommitment,
@@ -363,20 +365,23 @@ export class EthereumPrivacyAdapter {
363
365
  /**
364
366
  * Check if a stealth address belongs to a recipient
365
367
  *
368
+ * Canonical EIP-5564 view-only check: requires only the recipient's viewing
369
+ * private key plus their spending PUBLIC key (no spending private key needed).
370
+ *
366
371
  * @param stealthAddress - Stealth address object
367
- * @param spendingPrivateKey - Spending private key (hex)
368
372
  * @param viewingPrivateKey - Viewing private key (hex)
373
+ * @param spendingPublicKey - Spending public key (hex, meta-address spendingKey)
369
374
  * @returns True if the address belongs to the recipient
370
375
  */
371
376
  checkStealthAddress(
372
377
  stealthAddress: StealthAddress,
373
- spendingPrivateKey: HexString,
374
- viewingPrivateKey: HexString
378
+ viewingPrivateKey: HexString,
379
+ spendingPublicKey: HexString
375
380
  ): boolean {
376
381
  return checkEthereumStealthAddress(
377
382
  stealthAddress,
378
- spendingPrivateKey,
379
- viewingPrivateKey
383
+ viewingPrivateKey,
384
+ spendingPublicKey
380
385
  )
381
386
  }
382
387
 
@@ -586,6 +591,11 @@ export class EthereumPrivacyAdapter {
586
591
 
587
592
  // Check each recipient
588
593
  for (const recipient of this.scanRecipients.values()) {
594
+ // The full scan derives the claimable key, which needs the spending private key.
595
+ // View-only recipients (no spending private key) are handled by scanAnnouncementsViewOnly.
596
+ if (!recipient.spendingPrivateKey) {
597
+ continue
598
+ }
589
599
  // Use ETH address comparison since announcements store 20-byte addresses
590
600
  // Returns the stealth private key if match found, null otherwise
591
601
  const stealthPrivateKey = checkEthereumStealthByEthAddress(
@@ -617,6 +627,55 @@ export class EthereumPrivacyAdapter {
617
627
  return results
618
628
  }
619
629
 
630
+ /**
631
+ * Scan announcements for incoming payments — VIEW-ONLY.
632
+ *
633
+ * Detects payments using each registered recipient's viewing private key + spending
634
+ * public key only (never the spending private key), so a compliance auditor or a
635
+ * delegated watcher can find incoming payments without spend authority. Results carry
636
+ * no derived private key — claim by deriving it separately at claim time (with both
637
+ * private keys).
638
+ *
639
+ * @param announcements - Announcements to scan
640
+ * @returns Detected payments (without private keys)
641
+ */
642
+ scanAnnouncementsViewOnly(
643
+ announcements: EthereumAnnouncement[]
644
+ ): EthereumViewOnlyDetectionResult[] {
645
+ const results: EthereumViewOnlyDetectionResult[] = []
646
+
647
+ for (const announcement of announcements) {
648
+ const stealthAddress = announcementToStealthAddress(announcement)
649
+
650
+ for (const recipient of this.scanRecipients.values()) {
651
+ const isMatch = checkEthereumStealthByEthAddressViewOnly(
652
+ announcement.stealthAddress,
653
+ announcement.ephemeralPublicKey,
654
+ announcement.viewTag,
655
+ recipient.spendingPublicKey,
656
+ recipient.viewingPrivateKey,
657
+ )
658
+
659
+ if (isMatch) {
660
+ results.push({
661
+ payment: {
662
+ stealthAddress,
663
+ stealthEthAddress: announcement.stealthAddress,
664
+ txHash: announcement.txHash!,
665
+ blockNumber: announcement.blockNumber!,
666
+ logIndex: announcement.logIndex,
667
+ timestamp: announcement.timestamp,
668
+ },
669
+ recipient,
670
+ })
671
+ break // Found owner, no need to check other recipients
672
+ }
673
+ }
674
+ }
675
+
676
+ return results
677
+ }
678
+
620
679
  /**
621
680
  * Get topics for filtering announcement logs
622
681
  *