@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.
- package/LICENSE +21 -0
- package/dist/{TransportWebUSB-YQMAGJAJ.mjs → TransportWebUSB-2KITI5HD.mjs} +24 -12
- package/dist/browser.d.mts +4 -4
- package/dist/browser.d.ts +4 -4
- package/dist/browser.js +1346 -838
- package/dist/browser.mjs +13 -3
- package/dist/{chunk-64AYA5F5.mjs → chunk-G3TBBG2K.mjs} +221 -146
- package/dist/{chunk-4GRJ5MAW.mjs → chunk-KXETSSKP.mjs} +4 -0
- package/dist/{chunk-6EU6WQFK.mjs → chunk-PT2DNA7E.mjs} +257 -235
- package/dist/{constants-LHAAUC2T.mjs → constants-DCJYTIU3.mjs} +5 -1
- package/dist/{dist-2OGQ7FED.mjs → dist-PYEXZNFD.mjs} +609 -221
- package/dist/{index-DeE1ZzA4.d.mts → index-B1d8pihL.d.mts} +117 -33
- package/dist/{index-DXh2IGkz.d.ts → index-UQhQJZbM.d.ts} +117 -33
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1339 -831
- package/dist/index.mjs +13 -3
- package/dist/{interface-Bf7w1PLW.d.mts → interface-CQi0-WfS.d.mts} +2 -2
- package/dist/{interface-Bf7w1PLW.d.ts → interface-CQi0-WfS.d.ts} +2 -2
- package/dist/{noir-kzbLVTei.d.mts → noir-CwPIyBLj.d.mts} +1 -1
- package/dist/{noir-kzbLVTei.d.ts → noir-CwPIyBLj.d.ts} +1 -1
- package/dist/proofs/halo2.d.mts +1 -1
- package/dist/proofs/halo2.d.ts +1 -1
- package/dist/proofs/kimchi.d.mts +1 -1
- package/dist/proofs/kimchi.d.ts +1 -1
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/{solana-U3MEGU7W.mjs → solana-ZWNIQTSU.mjs} +6 -6
- package/package.json +32 -32
- package/src/adapters/gelato-relay.ts +386 -0
- package/src/adapters/index.ts +28 -0
- package/src/adapters/oneinch.ts +126 -0
- package/src/chains/ethereum/privacy-adapter.ts +8 -5
- package/src/chains/ethereum/stealth.ts +17 -14
- package/src/chains/near/privacy-adapter.ts +8 -5
- package/src/chains/near/resolver.ts +22 -8
- package/src/chains/near/stealth.ts +9 -9
- package/src/chains/solana/constants.ts +13 -1
- package/src/chains/solana/ephemeral-keys.ts +3 -257
- package/src/chains/solana/index.ts +2 -3
- package/src/chains/solana/providers/helius-enhanced.ts +6 -6
- package/src/chains/solana/providers/webhook.ts +2 -2
- package/src/chains/solana/scan.ts +9 -8
- package/src/chains/solana/stealth-scanner.ts +3 -3
- package/src/chains/solana/types.ts +18 -4
- package/src/cosmos/ibc-stealth.ts +6 -6
- package/src/index.ts +6 -0
- package/src/move/aptos.ts +15 -9
- package/src/move/sui.ts +15 -9
- package/src/nft/private-nft.ts +10 -6
- package/src/privacy-backends/shadowwire.ts +13 -0
- package/src/stealth/ed25519.ts +173 -12
- package/src/stealth/index.ts +47 -4
- package/src/stealth/secp256k1.ts +144 -7
- package/src/stealth.ts +7 -0
- package/src/wallet/ethereum/privacy-adapter.ts +1 -1
- package/src/wallet/hardware/ledger-privacy.ts +2 -2
- package/src/wallet/near/adapter.ts +2 -2
- package/src/wallet/near/meteor-wallet.ts +2 -2
- package/src/wallet/near/my-near-wallet.ts +2 -2
- package/src/wallet/near/wallet-selector.ts +2 -2
- package/src/wallet/solana/privacy-adapter.ts +9 -9
- package/dist/chunk-5EKF243P.mjs +0 -33809
- 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
|
+
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -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
|
-
|
|
374
|
-
|
|
376
|
+
viewingPrivateKey: HexString,
|
|
377
|
+
spendingPublicKey: HexString
|
|
375
378
|
): boolean {
|
|
376
379
|
return checkEthereumStealthAddress(
|
|
377
380
|
stealthAddress,
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
*
|
|
363
|
-
*
|
|
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
|
-
|
|
374
|
-
|
|
376
|
+
viewingPrivateKey: HexString,
|
|
377
|
+
spendingPublicKey: HexString
|
|
375
378
|
): boolean {
|
|
376
379
|
return checkSecp256k1StealthAddress(
|
|
377
380
|
stealthAddress,
|
|
378
|
-
|
|
379
|
-
|
|
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 =
|
|
423
|
-
//
|
|
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
|
-
|
|
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:
|
|
436
|
-
// Mirrors generation: stealth =
|
|
437
|
-
const
|
|
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 = (
|
|
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
|
-
|
|
437
|
-
|
|
439
|
+
viewingPrivateKey: HexString,
|
|
440
|
+
spendingPublicKey: HexString
|
|
438
441
|
): boolean {
|
|
439
442
|
return checkNEARStealthAddress(
|
|
440
443
|
stealthAddress,
|
|
441
|
-
|
|
442
|
-
|
|
444
|
+
viewingPrivateKey,
|
|
445
|
+
spendingPublicKey
|
|
443
446
|
)
|
|
444
447
|
}
|
|
445
448
|
|