@luxfi/dex 1.2.1 → 1.3.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 (62) hide show
  1. package/dist/client/clob.d.ts +52 -0
  2. package/dist/client/clob.d.ts.map +1 -0
  3. package/dist/client/clob.js +196 -0
  4. package/dist/client/index.d.ts +7 -0
  5. package/dist/client/index.d.ts.map +1 -0
  6. package/dist/client/index.js +6 -0
  7. package/dist/client/types.d.ts +126 -0
  8. package/dist/client/types.d.ts.map +1 -0
  9. package/dist/client/types.js +5 -0
  10. package/dist/hooks/index.d.ts +7 -0
  11. package/dist/hooks/index.d.ts.map +1 -0
  12. package/dist/hooks/index.js +6 -0
  13. package/dist/hooks/use-quote.d.ts +18 -0
  14. package/dist/hooks/use-quote.d.ts.map +1 -0
  15. package/dist/hooks/use-quote.js +65 -0
  16. package/dist/hooks/use-swap.d.ts +17 -0
  17. package/dist/hooks/use-swap.d.ts.map +1 -0
  18. package/dist/hooks/use-swap.js +75 -0
  19. package/dist/index.d.ts +33 -115
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +41 -225
  22. package/dist/precompile/abis.d.ts +400 -0
  23. package/dist/precompile/abis.d.ts.map +1 -0
  24. package/dist/precompile/abis.js +287 -0
  25. package/dist/precompile/addresses.d.ts +65 -0
  26. package/dist/precompile/addresses.d.ts.map +1 -0
  27. package/dist/precompile/addresses.js +52 -0
  28. package/dist/precompile/index.d.ts +8 -0
  29. package/dist/precompile/index.d.ts.map +1 -0
  30. package/dist/precompile/index.js +7 -0
  31. package/dist/precompile/types.d.ts +76 -0
  32. package/dist/precompile/types.d.ts.map +1 -0
  33. package/dist/precompile/types.js +17 -0
  34. package/dist/router/index.d.ts +7 -0
  35. package/dist/router/index.d.ts.map +1 -0
  36. package/dist/router/index.js +6 -0
  37. package/dist/router/router.d.ts +58 -0
  38. package/dist/router/router.d.ts.map +1 -0
  39. package/dist/router/router.js +272 -0
  40. package/dist/router/types.d.ts +76 -0
  41. package/dist/router/types.d.ts.map +1 -0
  42. package/dist/router/types.js +1 -0
  43. package/package.json +55 -29
  44. package/src/client/clob.ts +256 -0
  45. package/src/client/index.ts +6 -0
  46. package/src/client/types.ts +148 -0
  47. package/src/hooks/index.ts +6 -0
  48. package/src/hooks/use-quote.ts +92 -0
  49. package/src/hooks/use-swap.ts +103 -0
  50. package/src/index.ts +76 -309
  51. package/src/precompile/abis.ts +291 -0
  52. package/src/precompile/addresses.ts +72 -0
  53. package/src/precompile/index.ts +7 -0
  54. package/src/precompile/types.ts +96 -0
  55. package/src/router/index.ts +6 -0
  56. package/src/router/router.ts +338 -0
  57. package/src/router/types.ts +87 -0
  58. package/dist/marketData.d.ts +0 -152
  59. package/dist/marketData.d.ts.map +0 -1
  60. package/dist/marketData.js +0 -253
  61. package/src/marketData.ts +0 -351
  62. package/tsconfig.json +0 -19
package/package.json CHANGED
@@ -1,41 +1,67 @@
1
1
  {
2
2
  "name": "@luxfi/dex",
3
- "version": "1.2.1",
4
- "description": "TypeScript SDK for LX",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
3
+ "version": "1.3.0",
4
+ "description": "Lux DEX SDK - CLOB client, AMM precompiles, and omnichain routing",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./client": {
15
+ "types": "./dist/client/index.d.ts",
16
+ "import": "./dist/client/index.js"
17
+ },
18
+ "./router": {
19
+ "types": "./dist/router/index.d.ts",
20
+ "import": "./dist/router/index.js"
21
+ },
22
+ "./precompile": {
23
+ "types": "./dist/precompile/index.d.ts",
24
+ "import": "./dist/precompile/index.js"
25
+ },
26
+ "./hooks": {
27
+ "types": "./dist/hooks/index.d.ts",
28
+ "import": "./dist/hooks/index.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src"
34
+ ],
7
35
  "scripts": {
36
+ "typecheck": "tsc --noEmit",
8
37
  "build": "tsc",
9
- "test": "jest",
10
- "lint": "eslint src/**/*.ts",
11
- "prepublish": "npm run build"
38
+ "prepublishOnly": "bun run build"
12
39
  },
13
- "keywords": [
14
- "luxfi",
15
- "dex",
16
- "trading",
17
- "blockchain",
18
- "sdk"
19
- ],
20
- "author": "Lux Network",
21
- "license": "MIT",
22
40
  "dependencies": {
23
- "axios": "^1.6.0",
24
- "ws": "^8.14.0"
41
+ "viem": "^2.30.6",
42
+ "wagmi": "^3.1.3",
43
+ "zustand": "^5.0.5"
25
44
  },
26
45
  "devDependencies": {
27
- "@types/jest": "^29.5.0",
28
- "@types/node": "^20.0.0",
29
- "@types/ws": "^8.5.0",
30
- "@typescript-eslint/eslint-plugin": "^6.0.0",
31
- "@typescript-eslint/parser": "^6.0.0",
32
- "eslint": "^8.0.0",
33
- "jest": "^29.5.0",
34
- "ts-jest": "^29.1.0",
35
- "typescript": "^5.0.0"
46
+ "@types/node": "^25.0.3",
47
+ "@types/react": "^19.2.3",
48
+ "typescript": "^5.8.3"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^19.0.0"
36
52
  },
37
53
  "repository": {
38
54
  "type": "git",
39
- "url": "https://github.com/luxfi/dex.git"
40
- }
55
+ "url": "https://github.com/luxfi/exchange.git",
56
+ "directory": "packages/dex"
57
+ },
58
+ "keywords": [
59
+ "lux",
60
+ "dex",
61
+ "defi",
62
+ "clob",
63
+ "amm",
64
+ "swap",
65
+ "precompile"
66
+ ]
41
67
  }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * CLOB Client
3
+ * WebSocket client for lux/dex order book
4
+ */
5
+ import type {
6
+ ICLOBClient,
7
+ OrderRequest,
8
+ Order,
9
+ OrderBook,
10
+ Trade,
11
+ Position,
12
+ Balance
13
+ } from './types'
14
+
15
+ interface CLOBClientOptions {
16
+ url: string
17
+ debug?: boolean
18
+ reconnect?: boolean
19
+ reconnectInterval?: number
20
+ }
21
+
22
+ type MessageHandler = (data: any) => void
23
+
24
+ /**
25
+ * Central Limit Order Book client
26
+ * Connects to lux/dex WebSocket server
27
+ */
28
+ export class CLOBClient implements ICLOBClient {
29
+ private ws: WebSocket | null = null
30
+ private url: string
31
+ private debug: boolean
32
+ private reconnect: boolean
33
+ private reconnectInterval: number
34
+ private connected: boolean = false
35
+ private authenticated: boolean = false
36
+ private requestId: number = 0
37
+ private pendingRequests: Map<number, { resolve: Function; reject: Function }> = new Map()
38
+ private subscriptions: Map<string, Set<MessageHandler>> = new Map()
39
+
40
+ constructor(options: CLOBClientOptions) {
41
+ this.url = options.url
42
+ this.debug = options.debug ?? false
43
+ this.reconnect = options.reconnect ?? true
44
+ this.reconnectInterval = options.reconnectInterval ?? 5000
45
+ }
46
+
47
+ private log(...args: any[]) {
48
+ if (this.debug) {
49
+ console.log('[CLOBClient]', ...args)
50
+ }
51
+ }
52
+
53
+ async connect(): Promise<void> {
54
+ return new Promise((resolve, reject) => {
55
+ try {
56
+ this.ws = new WebSocket(this.url)
57
+
58
+ this.ws.onopen = () => {
59
+ this.connected = true
60
+ this.log('Connected to', this.url)
61
+ resolve()
62
+ }
63
+
64
+ this.ws.onclose = () => {
65
+ this.connected = false
66
+ this.authenticated = false
67
+ this.log('Disconnected')
68
+
69
+ if (this.reconnect) {
70
+ setTimeout(() => this.connect(), this.reconnectInterval)
71
+ }
72
+ }
73
+
74
+ this.ws.onerror = (error) => {
75
+ this.log('Error:', error)
76
+ reject(error)
77
+ }
78
+
79
+ this.ws.onmessage = (event) => {
80
+ this.handleMessage(event.data)
81
+ }
82
+ } catch (error) {
83
+ reject(error)
84
+ }
85
+ })
86
+ }
87
+
88
+ async disconnect(): Promise<void> {
89
+ this.reconnect = false
90
+ this.ws?.close()
91
+ this.ws = null
92
+ this.connected = false
93
+ this.authenticated = false
94
+ }
95
+
96
+ isConnected(): boolean {
97
+ return this.connected
98
+ }
99
+
100
+ private handleMessage(data: string) {
101
+ try {
102
+ const message = JSON.parse(data)
103
+ this.log('Received:', message)
104
+
105
+ // Handle response to request
106
+ if (message.id && this.pendingRequests.has(message.id)) {
107
+ const { resolve, reject } = this.pendingRequests.get(message.id)!
108
+ this.pendingRequests.delete(message.id)
109
+
110
+ if (message.error) {
111
+ reject(new Error(message.error))
112
+ } else {
113
+ resolve(message.result || message)
114
+ }
115
+ return
116
+ }
117
+
118
+ // Handle subscription updates
119
+ const type = message.type || message.channel
120
+ if (type && this.subscriptions.has(type)) {
121
+ this.subscriptions.get(type)!.forEach(handler => handler(message))
122
+ }
123
+ } catch (error) {
124
+ this.log('Parse error:', error)
125
+ }
126
+ }
127
+
128
+ private async request<T>(method: string, params?: any): Promise<T> {
129
+ if (!this.connected) {
130
+ throw new Error('Not connected')
131
+ }
132
+
133
+ const id = ++this.requestId
134
+ const message = {
135
+ jsonrpc: '2.0',
136
+ id,
137
+ method,
138
+ params,
139
+ }
140
+
141
+ return new Promise((resolve, reject) => {
142
+ this.pendingRequests.set(id, { resolve, reject })
143
+ this.ws!.send(JSON.stringify(message))
144
+ this.log('Sent:', message)
145
+
146
+ // Timeout after 30 seconds
147
+ setTimeout(() => {
148
+ if (this.pendingRequests.has(id)) {
149
+ this.pendingRequests.delete(id)
150
+ reject(new Error('Request timeout'))
151
+ }
152
+ }, 30000)
153
+ })
154
+ }
155
+
156
+ async authenticate(apiKey: string, apiSecret: string): Promise<void> {
157
+ await this.request('authenticate', { apiKey, apiSecret })
158
+ this.authenticated = true
159
+ }
160
+
161
+ async placeOrder(order: OrderRequest): Promise<Order> {
162
+ return this.request('placeOrder', order)
163
+ }
164
+
165
+ async cancelOrder(orderId: string): Promise<void> {
166
+ await this.request('cancelOrder', { orderId })
167
+ }
168
+
169
+ async getOrder(orderId: string): Promise<Order> {
170
+ return this.request('getOrder', { orderId })
171
+ }
172
+
173
+ async getOrders(symbol?: string): Promise<Order[]> {
174
+ return this.request('getOrders', { symbol })
175
+ }
176
+
177
+ async getOrderBook(symbol: string, depth: number = 20): Promise<OrderBook> {
178
+ return this.request('getOrderBook', { symbol, depth })
179
+ }
180
+
181
+ async getTrades(symbol: string, limit: number = 100): Promise<Trade[]> {
182
+ return this.request('getTrades', { symbol, limit })
183
+ }
184
+
185
+ async getPositions(): Promise<Position[]> {
186
+ return this.request('getPositions', {})
187
+ }
188
+
189
+ async getBalances(): Promise<Balance[]> {
190
+ return this.request('getBalances', {})
191
+ }
192
+
193
+ subscribeOrderBook(symbol: string, callback: (book: OrderBook) => void): () => void {
194
+ const channel = `orderbook:${symbol}`
195
+
196
+ if (!this.subscriptions.has(channel)) {
197
+ this.subscriptions.set(channel, new Set())
198
+ this.request('subscribe', { channel: 'orderbook', symbol }).catch(console.error)
199
+ }
200
+
201
+ this.subscriptions.get(channel)!.add(callback)
202
+
203
+ return () => {
204
+ this.subscriptions.get(channel)?.delete(callback)
205
+ if (this.subscriptions.get(channel)?.size === 0) {
206
+ this.subscriptions.delete(channel)
207
+ this.request('unsubscribe', { channel: 'orderbook', symbol }).catch(console.error)
208
+ }
209
+ }
210
+ }
211
+
212
+ subscribeTrades(symbol: string, callback: (trade: Trade) => void): () => void {
213
+ const channel = `trades:${symbol}`
214
+
215
+ if (!this.subscriptions.has(channel)) {
216
+ this.subscriptions.set(channel, new Set())
217
+ this.request('subscribe', { channel: 'trades', symbol }).catch(console.error)
218
+ }
219
+
220
+ this.subscriptions.get(channel)!.add(callback)
221
+
222
+ return () => {
223
+ this.subscriptions.get(channel)?.delete(callback)
224
+ if (this.subscriptions.get(channel)?.size === 0) {
225
+ this.subscriptions.delete(channel)
226
+ this.request('unsubscribe', { channel: 'trades', symbol }).catch(console.error)
227
+ }
228
+ }
229
+ }
230
+
231
+ subscribeOrders(callback: (order: Order) => void): () => void {
232
+ const channel = 'orders'
233
+
234
+ if (!this.subscriptions.has(channel)) {
235
+ this.subscriptions.set(channel, new Set())
236
+ this.request('subscribe', { channel: 'orders' }).catch(console.error)
237
+ }
238
+
239
+ this.subscriptions.get(channel)!.add(callback)
240
+
241
+ return () => {
242
+ this.subscriptions.get(channel)?.delete(callback)
243
+ if (this.subscriptions.get(channel)?.size === 0) {
244
+ this.subscriptions.delete(channel)
245
+ this.request('unsubscribe', { channel: 'orders' }).catch(console.error)
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create a CLOB client
253
+ */
254
+ export function createCLOBClient(url: string, debug: boolean = false): ICLOBClient {
255
+ return new CLOBClient({ url, debug })
256
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * DEX Client Exports
3
+ * CLOB client for lux/dex
4
+ */
5
+ export * from './types'
6
+ export * from './clob'
@@ -0,0 +1,148 @@
1
+ /**
2
+ * CLOB Client Types
3
+ * Interfaces for Central Limit Order Book operations
4
+ */
5
+
6
+ /**
7
+ * Order side
8
+ */
9
+ export type OrderSide = 'buy' | 'sell'
10
+
11
+ /**
12
+ * Order type
13
+ */
14
+ export type OrderType = 'limit' | 'market' | 'stop' | 'stop_limit'
15
+
16
+ /**
17
+ * Order status
18
+ */
19
+ export type OrderStatus = 'pending' | 'open' | 'partial' | 'filled' | 'cancelled' | 'rejected'
20
+
21
+ /**
22
+ * Time in force
23
+ */
24
+ export type TimeInForce = 'GTC' | 'IOC' | 'FOK' | 'GTD'
25
+
26
+ /**
27
+ * Order request
28
+ */
29
+ export interface OrderRequest {
30
+ symbol: string
31
+ side: OrderSide
32
+ type: OrderType
33
+ price?: number // Required for limit orders
34
+ size: number
35
+ timeInForce?: TimeInForce
36
+ clientOrderId?: string
37
+ reduceOnly?: boolean
38
+ postOnly?: boolean
39
+ }
40
+
41
+ /**
42
+ * Order response
43
+ */
44
+ export interface Order {
45
+ orderId: string
46
+ clientOrderId?: string
47
+ symbol: string
48
+ side: OrderSide
49
+ type: OrderType
50
+ price: number
51
+ size: number
52
+ filledSize: number
53
+ remainingSize: number
54
+ status: OrderStatus
55
+ timeInForce: TimeInForce
56
+ createdAt: number
57
+ updatedAt: number
58
+ }
59
+
60
+ /**
61
+ * Order book entry
62
+ */
63
+ export interface OrderBookEntry {
64
+ price: number
65
+ size: number
66
+ count: number
67
+ }
68
+
69
+ /**
70
+ * Order book
71
+ */
72
+ export interface OrderBook {
73
+ symbol: string
74
+ bids: OrderBookEntry[]
75
+ asks: OrderBookEntry[]
76
+ timestamp: number
77
+ }
78
+
79
+ /**
80
+ * Trade
81
+ */
82
+ export interface Trade {
83
+ tradeId: string
84
+ symbol: string
85
+ side: OrderSide
86
+ price: number
87
+ size: number
88
+ timestamp: number
89
+ makerOrderId: string
90
+ takerOrderId: string
91
+ }
92
+
93
+ /**
94
+ * Position
95
+ */
96
+ export interface Position {
97
+ symbol: string
98
+ side: 'long' | 'short'
99
+ size: number
100
+ entryPrice: number
101
+ markPrice: number
102
+ liquidationPrice: number
103
+ unrealizedPnl: number
104
+ margin: number
105
+ leverage: number
106
+ }
107
+
108
+ /**
109
+ * Balance
110
+ */
111
+ export interface Balance {
112
+ currency: string
113
+ available: number
114
+ locked: number
115
+ total: number
116
+ }
117
+
118
+ /**
119
+ * CLOB client interface
120
+ */
121
+ export interface ICLOBClient {
122
+ // Connection
123
+ connect(): Promise<void>
124
+ disconnect(): Promise<void>
125
+ isConnected(): boolean
126
+
127
+ // Authentication
128
+ authenticate(apiKey: string, apiSecret: string): Promise<void>
129
+
130
+ // Orders
131
+ placeOrder(order: OrderRequest): Promise<Order>
132
+ cancelOrder(orderId: string): Promise<void>
133
+ getOrder(orderId: string): Promise<Order>
134
+ getOrders(symbol?: string): Promise<Order[]>
135
+
136
+ // Market data
137
+ getOrderBook(symbol: string, depth?: number): Promise<OrderBook>
138
+ getTrades(symbol: string, limit?: number): Promise<Trade[]>
139
+
140
+ // Account
141
+ getPositions(): Promise<Position[]>
142
+ getBalances(): Promise<Balance[]>
143
+
144
+ // Subscriptions
145
+ subscribeOrderBook(symbol: string, callback: (book: OrderBook) => void): () => void
146
+ subscribeTrades(symbol: string, callback: (trade: Trade) => void): () => void
147
+ subscribeOrders(callback: (order: Order) => void): () => void
148
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * DEX Hooks Exports
3
+ * React hooks for DEX integration
4
+ */
5
+ export * from './use-quote'
6
+ export * from './use-swap'
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback, useEffect, useRef } from 'react'
4
+ import type { Address } from 'viem'
5
+ import { usePublicClient } from 'wagmi'
6
+ import { OmnichainRouter, type Quote, type QuoteRequest } from '../router'
7
+
8
+ interface UseQuoteOptions {
9
+ refreshInterval?: number // ms
10
+ enabled?: boolean
11
+ }
12
+
13
+ interface UseQuoteResult {
14
+ quote: Quote | null
15
+ isLoading: boolean
16
+ error: Error | null
17
+ refetch: () => Promise<void>
18
+ }
19
+
20
+ /**
21
+ * Hook to get swap quotes from the omnichain router
22
+ */
23
+ export function useQuote(
24
+ tokenIn: Address | undefined,
25
+ tokenOut: Address | undefined,
26
+ amountIn: bigint | undefined,
27
+ options: UseQuoteOptions = {}
28
+ ): UseQuoteResult {
29
+ const { refreshInterval = 10000, enabled = true } = options
30
+ const publicClient = usePublicClient()
31
+ const routerRef = useRef<OmnichainRouter | null>(null)
32
+
33
+ const [quote, setQuote] = useState<Quote | null>(null)
34
+ const [isLoading, setIsLoading] = useState(false)
35
+ const [error, setError] = useState<Error | null>(null)
36
+
37
+ // Initialize router
38
+ useEffect(() => {
39
+ if (!routerRef.current) {
40
+ routerRef.current = new OmnichainRouter()
41
+ }
42
+ if (publicClient) {
43
+ routerRef.current.setPublicClient(publicClient)
44
+ }
45
+ }, [publicClient])
46
+
47
+ const fetchQuote = useCallback(async () => {
48
+ if (!tokenIn || !tokenOut || !amountIn || amountIn === 0n || !enabled) {
49
+ setQuote(null)
50
+ return
51
+ }
52
+
53
+ setIsLoading(true)
54
+ setError(null)
55
+
56
+ try {
57
+ const request: QuoteRequest = {
58
+ tokenIn,
59
+ tokenOut,
60
+ amountIn,
61
+ }
62
+
63
+ const newQuote = await routerRef.current!.getQuote(request)
64
+ setQuote(newQuote)
65
+ } catch (err) {
66
+ setError(err instanceof Error ? err : new Error('Failed to get quote'))
67
+ setQuote(null)
68
+ } finally {
69
+ setIsLoading(false)
70
+ }
71
+ }, [tokenIn, tokenOut, amountIn, enabled])
72
+
73
+ // Fetch on mount and when inputs change
74
+ useEffect(() => {
75
+ fetchQuote()
76
+ }, [fetchQuote])
77
+
78
+ // Auto-refresh
79
+ useEffect(() => {
80
+ if (!enabled || refreshInterval <= 0) return
81
+
82
+ const interval = setInterval(fetchQuote, refreshInterval)
83
+ return () => clearInterval(interval)
84
+ }, [fetchQuote, enabled, refreshInterval])
85
+
86
+ return {
87
+ quote,
88
+ isLoading,
89
+ error,
90
+ refetch: fetchQuote,
91
+ }
92
+ }
@@ -0,0 +1,103 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import type { Address } from 'viem'
5
+ import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
6
+ import { DEX_PRECOMPILES } from '../precompile/addresses'
7
+ import { SWAP_ROUTER_ABI } from '../precompile/abis'
8
+ import { createPoolKey } from '../precompile/types'
9
+ import type { Quote } from '../router'
10
+
11
+ interface UseSwapResult {
12
+ swap: (quote: Quote, recipient: Address) => Promise<void>
13
+ isPending: boolean
14
+ isConfirming: boolean
15
+ isSuccess: boolean
16
+ error: Error | null
17
+ txHash: `0x${string}` | undefined
18
+ reset: () => void
19
+ }
20
+
21
+ /**
22
+ * Hook to execute swaps via the DEX precompiles
23
+ */
24
+ export function useSwap(): UseSwapResult {
25
+ const [error, setError] = useState<Error | null>(null)
26
+
27
+ const {
28
+ data: txHash,
29
+ writeContractAsync,
30
+ isPending,
31
+ reset: resetWrite,
32
+ } = useWriteContract()
33
+
34
+ const {
35
+ isLoading: isConfirming,
36
+ isSuccess,
37
+ } = useWaitForTransactionReceipt({
38
+ hash: txHash,
39
+ })
40
+
41
+ const swap = useCallback(async (quote: Quote, recipient: Address) => {
42
+ setError(null)
43
+
44
+ if (!quote || quote.route.length === 0) {
45
+ setError(new Error('Invalid quote'))
46
+ return
47
+ }
48
+
49
+ const step = quote.route[0]
50
+
51
+ if (step.source === 'amm') {
52
+ try {
53
+ // Execute AMM swap via precompile
54
+ const poolKey = createPoolKey(quote.tokenIn, quote.tokenOut)
55
+ const zeroForOne = quote.tokenIn.toLowerCase() < quote.tokenOut.toLowerCase()
56
+
57
+ await writeContractAsync({
58
+ address: DEX_PRECOMPILES.SWAP_ROUTER,
59
+ abi: SWAP_ROUTER_ABI,
60
+ functionName: 'exactInputSingle',
61
+ args: [{
62
+ poolKey,
63
+ zeroForOne,
64
+ amountIn: quote.amountIn,
65
+ amountOutMinimum: quote.minimumAmountOut,
66
+ sqrtPriceLimitX96: 0n,
67
+ hookData: '0x',
68
+ }],
69
+ // Include native value if swapping from native LUX
70
+ value: quote.tokenIn === '0x0000000000000000000000000000000000000000'
71
+ ? quote.amountIn
72
+ : 0n,
73
+ })
74
+ } catch (err) {
75
+ setError(err instanceof Error ? err : new Error('Swap failed'))
76
+ throw err
77
+ }
78
+ } else if (step.source === 'clob') {
79
+ // CLOB swaps are handled off-chain
80
+ // The frontend should use the CLOB client directly
81
+ setError(new Error('CLOB swaps should be executed via CLOB client'))
82
+ throw new Error('CLOB swaps not supported in this hook')
83
+ } else {
84
+ setError(new Error(`Unknown route source: ${step.source}`))
85
+ throw new Error(`Unknown route source: ${step.source}`)
86
+ }
87
+ }, [writeContractAsync])
88
+
89
+ const reset = useCallback(() => {
90
+ setError(null)
91
+ resetWrite()
92
+ }, [resetWrite])
93
+
94
+ return {
95
+ swap,
96
+ isPending,
97
+ isConfirming,
98
+ isSuccess,
99
+ error,
100
+ txHash,
101
+ reset,
102
+ }
103
+ }