@orbs-network/spot-react 0.0.46 → 0.0.48
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/.cursor/rules.md +1098 -0
- package/dist/{ccip-RoU8UheD.js → ccip-rjZm_oL7.js} +1 -1
- package/dist/{index-C6Z1TcvT.js → index-Bn4A8QXC.js} +2 -2
- package/dist/spot-react/package.json.d.ts +1 -1
- package/dist/spot-react.js +1 -1
- package/dist/spot-react.umd.cjs +1 -1
- package/package.json +1 -1
- package/.cursor/rules +0 -831
package/.cursor/rules.md
ADDED
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
# @orbs-network/spot-react Integration Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
`@orbs-network/spot-react` is a React component library for building Spot trading interfaces (TWAP, Limit, Stop-Loss, Take-Profit orders) on EVM chains. It provides hooks and components that manage order state, validation, and submission.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
**Important:** If `@orbs-network/twap-ui` is installed, uninstall it first - it's deprecated and replaced by `spot-react`:
|
|
9
|
+
```bash
|
|
10
|
+
npm uninstall @orbs-network/twap-ui
|
|
11
|
+
# or
|
|
12
|
+
pnpm remove @orbs-network/twap-ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then install spot-react:
|
|
16
|
+
```bash
|
|
17
|
+
npm install @orbs-network/spot-react
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @orbs-network/spot-react
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Required Peer Dependencies
|
|
23
|
+
- react ^18.0.0 || ^19.0.0
|
|
24
|
+
- react-dom ^18.0.0 || ^19.0.0
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Setup
|
|
29
|
+
|
|
30
|
+
### SpotProvider Props
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { SpotProvider, Module, Partners } from "@orbs-network/spot-react";
|
|
34
|
+
|
|
35
|
+
<SpotProvider
|
|
36
|
+
// === REQUIRED PROPS ===
|
|
37
|
+
|
|
38
|
+
partner={Partners.Quick} // Partner identifier (Quick, Thena, Spooky, etc.)
|
|
39
|
+
module={Module.TWAP} // Order type: TWAP | LIMIT | STOP_LOSS | TAKE_PROFIT
|
|
40
|
+
priceProtection={0.05} // Slippage tolerance (0.05 = 5%)
|
|
41
|
+
minChunkSizeUsd={5} // Minimum individual trade size in USD
|
|
42
|
+
|
|
43
|
+
// Market reference price - CRITICAL
|
|
44
|
+
// This must be the expected OUTPUT amount (in wei) for the current INPUT amount
|
|
45
|
+
marketReferencePrice={{
|
|
46
|
+
value: "1850000000", // Output amount in wei (NOT a ratio)
|
|
47
|
+
isLoading: false,
|
|
48
|
+
noLiquidity: false,
|
|
49
|
+
}}
|
|
50
|
+
|
|
51
|
+
// Required UI components (you must implement these)
|
|
52
|
+
components={{
|
|
53
|
+
Button: YourButton, // ButtonProps
|
|
54
|
+
Tooltip: YourTooltip, // TooltipProps
|
|
55
|
+
TokenLogo: YourTokenLogo, // TokenLogoProps
|
|
56
|
+
Spinner: <YourSpinner />, // ReactNode
|
|
57
|
+
}}
|
|
58
|
+
|
|
59
|
+
// === TOKEN DATA ===
|
|
60
|
+
|
|
61
|
+
srcToken={{ // Source (sell) token
|
|
62
|
+
address: "0x...",
|
|
63
|
+
symbol: "ETH",
|
|
64
|
+
decimals: 18,
|
|
65
|
+
logoUrl: "https://..."
|
|
66
|
+
}}
|
|
67
|
+
dstToken={{ // Destination (buy) token
|
|
68
|
+
address: "0x...",
|
|
69
|
+
symbol: "USDC",
|
|
70
|
+
decimals: 6,
|
|
71
|
+
logoUrl: "https://..."
|
|
72
|
+
}}
|
|
73
|
+
srcBalance="1000000000000000000" // Source balance in wei (string)
|
|
74
|
+
dstBalance="1000000000" // Destination balance in wei (string)
|
|
75
|
+
srcUsd1Token="1850.50" // USD price for 1 source token
|
|
76
|
+
dstUsd1Token="1.00" // USD price for 1 destination token
|
|
77
|
+
|
|
78
|
+
// === WALLET ===
|
|
79
|
+
|
|
80
|
+
chainId={1} // Current chain ID
|
|
81
|
+
account="0x..." // Connected wallet address
|
|
82
|
+
provider={walletClient?.transport} // EIP-1193 provider
|
|
83
|
+
|
|
84
|
+
// === OPTIONAL ===
|
|
85
|
+
|
|
86
|
+
callbacks={callbacks} // Order lifecycle callbacks
|
|
87
|
+
refetchBalances={() => {}} // Called after order progress updates
|
|
88
|
+
useToken={useTokenByAddress} // Hook: (address?: string) => Token | undefined
|
|
89
|
+
fees={0.25} // Fee percentage to display
|
|
90
|
+
|
|
91
|
+
// Override components for custom views
|
|
92
|
+
// components.SubmitOrderSuccessView
|
|
93
|
+
// components.SubmitOrderErrorView
|
|
94
|
+
// components.SubmitOrderMainView
|
|
95
|
+
// components.USD
|
|
96
|
+
// components.Link
|
|
97
|
+
// components.SuccessIcon
|
|
98
|
+
// components.ErrorIcon
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</SpotProvider>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Token Type
|
|
105
|
+
```tsx
|
|
106
|
+
type Token = {
|
|
107
|
+
address: string;
|
|
108
|
+
symbol: string;
|
|
109
|
+
decimals: number;
|
|
110
|
+
logoUrl: string;
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Module Types
|
|
115
|
+
```tsx
|
|
116
|
+
enum Module {
|
|
117
|
+
TWAP = "twap",
|
|
118
|
+
LIMIT = "limit",
|
|
119
|
+
STOP_LOSS = "stop_loss",
|
|
120
|
+
TAKE_PROFIT = "take_profit"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Hooks Reference
|
|
127
|
+
|
|
128
|
+
### `useSrcTokenPanel()` / `useDstTokenPanel()`
|
|
129
|
+
|
|
130
|
+
Token input panel state and handlers.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
const {
|
|
134
|
+
value, // string - Current amount value
|
|
135
|
+
valueWei, // string - Amount in wei
|
|
136
|
+
balance, // string - Token balance (UI format)
|
|
137
|
+
usd, // string - USD value of amount
|
|
138
|
+
token, // Token | undefined
|
|
139
|
+
isLoading, // boolean - True when calculating output
|
|
140
|
+
isInsufficientBalance,// InputError | undefined - Balance error
|
|
141
|
+
onChange, // (value: string) => void - Update amount
|
|
142
|
+
onMax, // () => void - Set to max balance
|
|
143
|
+
} = useSrcTokenPanel();
|
|
144
|
+
|
|
145
|
+
// Note: Only srcTokenPanel.onChange works - dstTokenPanel is read-only
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### `useTypedSrcAmount()`
|
|
149
|
+
|
|
150
|
+
Get/reset the typed source amount.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const {
|
|
154
|
+
amount, // string - Current typed amount
|
|
155
|
+
reset, // () => void - Clear the amount
|
|
156
|
+
} = useTypedSrcAmount();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `useTradesPanel()` (TWAP only)
|
|
160
|
+
|
|
161
|
+
Configure number of trades.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
const {
|
|
165
|
+
totalTrades, // number - Current number of trades
|
|
166
|
+
maxTrades, // number - Maximum allowed trades
|
|
167
|
+
amountPerTrade, // string - Formatted amount per trade
|
|
168
|
+
amountPerTradeWei, // string - Amount per trade in wei
|
|
169
|
+
amountPerTradeUsd, // string - USD value per trade
|
|
170
|
+
fromToken, // Token | undefined
|
|
171
|
+
toToken, // Token | undefined
|
|
172
|
+
label, // string - "Over"
|
|
173
|
+
tooltip, // string - Tooltip text
|
|
174
|
+
error, // InputError | undefined
|
|
175
|
+
onChange, // (trades: number) => void
|
|
176
|
+
} = useTradesPanel();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### `useDurationPanel()`
|
|
180
|
+
|
|
181
|
+
Configure order expiration (for LIMIT, STOP_LOSS, TAKE_PROFIT).
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { DEFAULT_DURATION_OPTIONS, TimeUnit } from "@orbs-network/spot-react";
|
|
185
|
+
|
|
186
|
+
const {
|
|
187
|
+
duration, // { value: number, unit: TimeUnit }
|
|
188
|
+
milliseconds, // number - Duration in milliseconds
|
|
189
|
+
label, // string - "Expiry"
|
|
190
|
+
tooltip, // string - Tooltip text
|
|
191
|
+
error, // InputError | undefined
|
|
192
|
+
onChange, // (duration: TimeDuration) => void
|
|
193
|
+
onInputChange, // (value: string) => void - Change value only
|
|
194
|
+
onUnitSelect, // (unit: TimeUnit) => void - Change unit only
|
|
195
|
+
} = useDurationPanel();
|
|
196
|
+
|
|
197
|
+
// TimeUnit values:
|
|
198
|
+
// TimeUnit.Minutes = 60000 (60 * 1000)
|
|
199
|
+
// TimeUnit.Hours = 3600000 (60 * 60 * 1000)
|
|
200
|
+
// TimeUnit.Days = 86400000 (24 * 60 * 60 * 1000)
|
|
201
|
+
|
|
202
|
+
// DEFAULT_DURATION_OPTIONS:
|
|
203
|
+
// [{ text: "Minutes", value: TimeUnit.Minutes }, ...]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `useFillDelayPanel()` (TWAP only)
|
|
207
|
+
|
|
208
|
+
Configure delay between trades.
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
const {
|
|
212
|
+
fillDelay, // { value: number, unit: TimeUnit }
|
|
213
|
+
milliseconds, // number - Delay in milliseconds
|
|
214
|
+
label, // string - "Every"
|
|
215
|
+
tooltip, // string - Tooltip text
|
|
216
|
+
error, // InputError | undefined
|
|
217
|
+
onChange, // (fillDelay: TimeDuration) => void
|
|
218
|
+
onInputChange, // (value: string) => void
|
|
219
|
+
onUnitSelect, // (unit: TimeUnit) => void
|
|
220
|
+
} = useFillDelayPanel();
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `useLimitPricePanel()`
|
|
224
|
+
|
|
225
|
+
Configure limit price.
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
const {
|
|
229
|
+
price, // string - Current price value
|
|
230
|
+
usd, // string - USD value
|
|
231
|
+
percentage, // string - Percentage from market price
|
|
232
|
+
isLimitPrice, // boolean - Is limit price enabled
|
|
233
|
+
isLoading, // boolean - Loading market price
|
|
234
|
+
isInverted, // boolean - Price direction inverted
|
|
235
|
+
fromToken, // Token | undefined
|
|
236
|
+
toToken, // Token | undefined
|
|
237
|
+
label, // string - "Limit Price"
|
|
238
|
+
tooltip, // string
|
|
239
|
+
error, // InputError | undefined
|
|
240
|
+
warning, // { text: string, url: string } | undefined
|
|
241
|
+
onChange, // (price: string) => void
|
|
242
|
+
onPercentageChange, // (percentage: string) => void
|
|
243
|
+
onReset, // () => void - Reset to default
|
|
244
|
+
toggleLimitPrice, // () => void - Toggle on/off
|
|
245
|
+
onInvert, // () => void - Invert price direction
|
|
246
|
+
} = useLimitPricePanel();
|
|
247
|
+
|
|
248
|
+
// Note: For Module.LIMIT, isLimitPrice is always true
|
|
249
|
+
// For TWAP, STOP_LOSS, TAKE_PROFIT it's optional (toggle)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `useTriggerPricePanel()` (STOP_LOSS / TAKE_PROFIT only)
|
|
253
|
+
|
|
254
|
+
Configure trigger price for conditional orders.
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const {
|
|
258
|
+
price, // string - Current trigger price
|
|
259
|
+
usd, // string - USD value
|
|
260
|
+
percentage, // string - Percentage from market price
|
|
261
|
+
isLoading, // boolean
|
|
262
|
+
isInverted, // boolean
|
|
263
|
+
isActive, // boolean - Is trigger active
|
|
264
|
+
hide, // boolean - True if not STOP_LOSS/TAKE_PROFIT
|
|
265
|
+
fromToken, // Token | undefined
|
|
266
|
+
toToken, // Token | undefined
|
|
267
|
+
label, // string - "Trigger Price"
|
|
268
|
+
tooltip, // string
|
|
269
|
+
error, // InputError | undefined
|
|
270
|
+
onChange, // (price: string) => void
|
|
271
|
+
onPercentageChange, // (percentage: string) => void
|
|
272
|
+
onReset, // () => void
|
|
273
|
+
onInvert, // () => void
|
|
274
|
+
} = useTriggerPricePanel();
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `useInvertTradePanel()`
|
|
278
|
+
|
|
279
|
+
Invert price display direction.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
const {
|
|
283
|
+
isInverted, // boolean - Is price inverted
|
|
284
|
+
isMarketPrice, // boolean - Is using market price (no limit)
|
|
285
|
+
fromToken, // Token | undefined - Display "from" token
|
|
286
|
+
toToken, // Token | undefined - Display "to" token
|
|
287
|
+
onInvert, // () => void - Toggle inversion
|
|
288
|
+
} = useInvertTradePanel();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### `useInputErrors()`
|
|
292
|
+
|
|
293
|
+
Get current validation error (returns first error).
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
const error = useInputErrors();
|
|
297
|
+
// Returns: InputError | undefined
|
|
298
|
+
|
|
299
|
+
type InputError = {
|
|
300
|
+
type: InputErrors; // Error type enum
|
|
301
|
+
value: string | number;
|
|
302
|
+
message: string; // Translated error message
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
enum InputErrors {
|
|
306
|
+
EMPTY_LIMIT_PRICE,
|
|
307
|
+
MAX_CHUNKS,
|
|
308
|
+
MIN_CHUNKS,
|
|
309
|
+
MIN_TRADE_SIZE,
|
|
310
|
+
MAX_FILL_DELAY,
|
|
311
|
+
MIN_FILL_DELAY,
|
|
312
|
+
MAX_ORDER_DURATION,
|
|
313
|
+
MIN_ORDER_DURATION,
|
|
314
|
+
MISSING_LIMIT_PRICE,
|
|
315
|
+
STOP_LOSS_TRIGGER_PRICE_GREATER_THAN_MARKET_PRICE,
|
|
316
|
+
TRIGGER_LIMIT_PRICE_GREATER_THAN_TRIGGER_PRICE,
|
|
317
|
+
TAKE_PROFIT_TRIGGER_PRICE_LESS_THAN_MARKET_PRICE,
|
|
318
|
+
EMPTY_TRIGGER_PRICE,
|
|
319
|
+
INSUFFICIENT_BALANCE,
|
|
320
|
+
MAX_ORDER_SIZE,
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### `useSubmitOrderPanel()`
|
|
325
|
+
|
|
326
|
+
Full order submission flow control.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
const {
|
|
330
|
+
// Actions
|
|
331
|
+
onSubmit, // () => Promise<void> - Submit order
|
|
332
|
+
onOpenModal, // () => void - Call when opening modal
|
|
333
|
+
onCloseModal, // () => void - Call when closing modal
|
|
334
|
+
reset, // () => void - Reset all state
|
|
335
|
+
|
|
336
|
+
// Status
|
|
337
|
+
status, // SwapStatus - Current status
|
|
338
|
+
isLoading, // boolean - Submission in progress
|
|
339
|
+
isSuccess, // boolean - Order created
|
|
340
|
+
isFailed, // boolean - Order failed
|
|
341
|
+
|
|
342
|
+
// Data
|
|
343
|
+
parsedError, // { message: string, code: number } | undefined
|
|
344
|
+
orderId, // string | undefined - Created order ID
|
|
345
|
+
step, // Steps - Current step (WRAP | APPROVE | CREATE)
|
|
346
|
+
stepIndex, // number | undefined
|
|
347
|
+
totalSteps, // number | undefined
|
|
348
|
+
srcToken, // Token | undefined
|
|
349
|
+
dstToken, // Token | undefined
|
|
350
|
+
approveTxHash, // string | undefined
|
|
351
|
+
wrapTxHash, // string | undefined
|
|
352
|
+
} = useSubmitOrderPanel();
|
|
353
|
+
|
|
354
|
+
// SwapStatus enum (from @orbs-network/swap-ui):
|
|
355
|
+
// LOADING, SUCCESS, FAILED
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### `useSubmitOrderButton()`
|
|
359
|
+
|
|
360
|
+
Just the submit button state.
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
const {
|
|
364
|
+
disabled, // boolean - Button disabled
|
|
365
|
+
text, // string - "Place Order" | "Enter Amount" | "Insufficient Balance" | "No Liquidity"
|
|
366
|
+
loading, // boolean - Loading state
|
|
367
|
+
} = useSubmitOrderButton();
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### `useOrderHistoryPanel()`
|
|
371
|
+
|
|
372
|
+
Order history and management.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
const {
|
|
376
|
+
// Orders data
|
|
377
|
+
orders, // { all, open, completed, expired, canceled } - Order arrays
|
|
378
|
+
ordersToDisplay, // Order[] - Filtered orders
|
|
379
|
+
openOrdersCount, // number
|
|
380
|
+
isLoading, // boolean
|
|
381
|
+
isRefetching, // boolean
|
|
382
|
+
|
|
383
|
+
// Selected order
|
|
384
|
+
selectedOrder, // HistoryOrder | undefined - Selected order details
|
|
385
|
+
onHideSelectedOrder, // () => void - Deselect order
|
|
386
|
+
|
|
387
|
+
// Filtering
|
|
388
|
+
statuses, // SelectMenuItem[] - Available status filters
|
|
389
|
+
selectedStatus, // string - Current filter
|
|
390
|
+
onSelectStatus, // (status?: OrderStatus) => void
|
|
391
|
+
|
|
392
|
+
// Cancel
|
|
393
|
+
cancelOrdersMode, // boolean
|
|
394
|
+
ordersToCancel, // Order[]
|
|
395
|
+
isCancelOrdersLoading, // boolean
|
|
396
|
+
onCancelOrder, // (order: Order) => Promise<string>
|
|
397
|
+
onCancelOrders, // (orders: Order[]) => void
|
|
398
|
+
onToggleCancelOrdersMode, // (enabled: boolean) => void
|
|
399
|
+
onSelectOrder, // (id: string) => void - Toggle order selection
|
|
400
|
+
onSelectAllOrdersToCancel, // () => void
|
|
401
|
+
|
|
402
|
+
refetch, // () => Promise<Order[]>
|
|
403
|
+
} = useOrderHistoryPanel();
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### `useDisclaimerPanel()`
|
|
407
|
+
|
|
408
|
+
Get module-specific disclaimer message.
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
const disclaimer = useDisclaimerPanel();
|
|
412
|
+
// Returns: { text: string, url: string } | undefined
|
|
413
|
+
|
|
414
|
+
// Shows different messages based on module and isMarketOrder state
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### `useTranslations()`
|
|
418
|
+
|
|
419
|
+
Get translation function.
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
const t = useTranslations();
|
|
423
|
+
|
|
424
|
+
// Available keys:
|
|
425
|
+
t("placeOrder") // "Place order"
|
|
426
|
+
t("enterAmount") // "Enter an amount"
|
|
427
|
+
t("insufficientFunds") // "Insufficient Balance"
|
|
428
|
+
t("noLiquidity") // "No liquidity for this pair"
|
|
429
|
+
t("limitPrice") // "Limit Price"
|
|
430
|
+
t("expiry") // "Expiry"
|
|
431
|
+
t("tradesAmountTitle") // "Over"
|
|
432
|
+
t("tradeIntervalTitle") // "Every"
|
|
433
|
+
t("stopLossLabel") // "Trigger Price"
|
|
434
|
+
// ... and more (see translations for full list)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### `useFormatNumber()`
|
|
438
|
+
|
|
439
|
+
Format numbers with locale.
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
const formatted = useFormatNumber({
|
|
443
|
+
value: "1234.5678",
|
|
444
|
+
decimalScale: 2
|
|
445
|
+
});
|
|
446
|
+
// Returns: { value: "1,234.57" }
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### `useOrderInfo()`
|
|
450
|
+
|
|
451
|
+
Get complete order details for display.
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
const orderInfo = useOrderInfo();
|
|
455
|
+
// Returns formatted order details including:
|
|
456
|
+
// - srcUsd, dstUsd
|
|
457
|
+
// - deadline { label, tooltip, value }
|
|
458
|
+
// - limitPrice { label, value, usd }
|
|
459
|
+
// - triggerPricePerTrade { label, tooltip, value, usd }
|
|
460
|
+
// - minDestAmountPerTrade { label, tooltip, value, usd }
|
|
461
|
+
// - sizePerTrade { label, tooltip, value }
|
|
462
|
+
// - totalTrades { label, tooltip, value }
|
|
463
|
+
// - tradeInterval { label, tooltip, value }
|
|
464
|
+
// - fees { label, amount, usd, percentage }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `usePartnerChains()`
|
|
468
|
+
|
|
469
|
+
Get supported chains for current partner.
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
const chains = usePartnerChains();
|
|
473
|
+
// Returns: number[] - Array of supported chain IDs
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### `useAddresses()`
|
|
477
|
+
|
|
478
|
+
Get contract addresses for current config.
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
const addresses = useAddresses();
|
|
482
|
+
// Returns contract addresses for the current chain/partner
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Pre-built Components
|
|
488
|
+
|
|
489
|
+
### `Components.SubmitOrderPanel`
|
|
490
|
+
|
|
491
|
+
Displays the order submission flow with steps (Wrap → Approve → Sign).
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
import { Components } from "@orbs-network/spot-react";
|
|
495
|
+
|
|
496
|
+
<Components.SubmitOrderPanel
|
|
497
|
+
reviewDetails={
|
|
498
|
+
// Your custom review UI - shown before submission starts
|
|
499
|
+
<div>
|
|
500
|
+
<YourOrderSummary />
|
|
501
|
+
<Button onClick={onSubmit}>Create Order</Button>
|
|
502
|
+
</div>
|
|
503
|
+
}
|
|
504
|
+
/>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The panel automatically shows:
|
|
508
|
+
- Token logos and amounts (from/to)
|
|
509
|
+
- Order details (deadline, limit price, trades, etc.)
|
|
510
|
+
- Step progress during submission
|
|
511
|
+
- Success/Error states
|
|
512
|
+
|
|
513
|
+
### `Components.Orders`
|
|
514
|
+
|
|
515
|
+
Pre-built orders list with detail view.
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
<Components.Orders />
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Callbacks
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
const callbacks = {
|
|
527
|
+
// === WRAP (ETH → WETH) ===
|
|
528
|
+
onWrapRequest: () => void,
|
|
529
|
+
onWrapSuccess: ({ txHash, explorerUrl, amount }) => void,
|
|
530
|
+
|
|
531
|
+
// === APPROVAL ===
|
|
532
|
+
onApproveRequest: () => void,
|
|
533
|
+
onApproveSuccess: ({ txHash, explorerUrl, token, amount }) => void,
|
|
534
|
+
|
|
535
|
+
// === ORDER SIGNING ===
|
|
536
|
+
onSignOrderRequest: () => void,
|
|
537
|
+
onSignOrderSuccess: (signature: string) => void,
|
|
538
|
+
onSignOrderError: (error: Error) => void,
|
|
539
|
+
|
|
540
|
+
// === ORDER CREATED ===
|
|
541
|
+
onOrderCreated: (order: Order) => void,
|
|
542
|
+
|
|
543
|
+
// === ORDER FILLED ===
|
|
544
|
+
onOrderFilled: (order: Order) => void,
|
|
545
|
+
onOrdersProgressUpdate: (orders: Order[]) => void,
|
|
546
|
+
|
|
547
|
+
// === ERRORS ===
|
|
548
|
+
onSubmitOrderFailed: ({ message, code }) => void,
|
|
549
|
+
onSubmitOrderRejected: () => void,
|
|
550
|
+
|
|
551
|
+
// === CANCEL ===
|
|
552
|
+
onCancelOrderRequest: (orders: Order[]) => void,
|
|
553
|
+
onCancelOrderSuccess: ({ orders, txHash, explorerUrl }) => void,
|
|
554
|
+
onCancelOrderFailed: (error: Error) => void,
|
|
555
|
+
|
|
556
|
+
// === UI ===
|
|
557
|
+
onCopy: () => void,
|
|
558
|
+
};
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Required UI Components Implementation
|
|
564
|
+
|
|
565
|
+
### ButtonProps
|
|
566
|
+
```tsx
|
|
567
|
+
interface ButtonProps {
|
|
568
|
+
children: ReactNode;
|
|
569
|
+
onClick: () => void;
|
|
570
|
+
disabled?: boolean;
|
|
571
|
+
loading?: boolean;
|
|
572
|
+
style?: CSSProperties;
|
|
573
|
+
allowClickWhileLoading?: boolean;
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### TooltipProps
|
|
578
|
+
```tsx
|
|
579
|
+
interface TooltipProps {
|
|
580
|
+
children?: ReactNode;
|
|
581
|
+
tooltipText?: string;
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### TokenLogoProps
|
|
586
|
+
```tsx
|
|
587
|
+
interface TokenLogoProps {
|
|
588
|
+
token?: Token;
|
|
589
|
+
size?: number;
|
|
590
|
+
className?: string;
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Module-Specific UI Logic
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
// Different modules show different inputs:
|
|
600
|
+
|
|
601
|
+
if (module === Module.TWAP) {
|
|
602
|
+
// Show: TradesPanel, FillDelayPanel
|
|
603
|
+
// Optional: LimitPricePanel (with toggle)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (module === Module.LIMIT) {
|
|
607
|
+
// Show: DurationPanel, LimitPricePanel (always on, no toggle)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (module === Module.STOP_LOSS || module === Module.TAKE_PROFIT) {
|
|
611
|
+
// Show: DurationPanel, TriggerPricePanel, LimitPricePanel (with toggle)
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Market Reference Price
|
|
618
|
+
|
|
619
|
+
**CRITICAL**: The `marketReferencePrice.value` must be the expected OUTPUT amount in wei for the current input amount.
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
// Get the output amount from your DEX's quote/trade function
|
|
623
|
+
const marketReferencePrice = useMemo(() => {
|
|
624
|
+
return {
|
|
625
|
+
value: dexQuote?.outAmount, // Wei string - e.g., "1850000000"
|
|
626
|
+
isLoading: isQuoteLoading,
|
|
627
|
+
noLiquidity: !dexQuote && !isQuoteLoading,
|
|
628
|
+
};
|
|
629
|
+
}, [dexQuote, isQuoteLoading]);
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Complete Integration Example
|
|
635
|
+
|
|
636
|
+
```tsx
|
|
637
|
+
import {
|
|
638
|
+
SpotProvider,
|
|
639
|
+
Module,
|
|
640
|
+
Partners,
|
|
641
|
+
Components,
|
|
642
|
+
useSrcTokenPanel,
|
|
643
|
+
useDstTokenPanel,
|
|
644
|
+
useTradesPanel,
|
|
645
|
+
useDurationPanel,
|
|
646
|
+
useFillDelayPanel,
|
|
647
|
+
useLimitPricePanel,
|
|
648
|
+
useTriggerPricePanel,
|
|
649
|
+
useSubmitOrderPanel,
|
|
650
|
+
useSubmitOrderButton,
|
|
651
|
+
useInputErrors,
|
|
652
|
+
useDisclaimerPanel,
|
|
653
|
+
useOrderHistoryPanel,
|
|
654
|
+
useInvertTradePanel,
|
|
655
|
+
DEFAULT_DURATION_OPTIONS,
|
|
656
|
+
} from "@orbs-network/spot-react";
|
|
657
|
+
import "@orbs-network/spot-react/styles.css";
|
|
658
|
+
|
|
659
|
+
// === MAIN WIDGET ===
|
|
660
|
+
|
|
661
|
+
function SpotTradingWidget({ module }: { module: Module }) {
|
|
662
|
+
const { address, chainId } = useWallet();
|
|
663
|
+
const srcToken = useSelectedToken("src");
|
|
664
|
+
const dstToken = useSelectedToken("dst");
|
|
665
|
+
const srcBalance = useBalance(srcToken?.address);
|
|
666
|
+
const dstBalance = useBalance(dstToken?.address);
|
|
667
|
+
const srcUsd = useUSDPrice(srcToken?.address);
|
|
668
|
+
const dstUsd = useUSDPrice(dstToken?.address);
|
|
669
|
+
const marketPrice = useDexQuote(srcToken, dstToken);
|
|
670
|
+
|
|
671
|
+
return (
|
|
672
|
+
<SpotProvider
|
|
673
|
+
partner={Partners.Quick}
|
|
674
|
+
module={module}
|
|
675
|
+
chainId={chainId}
|
|
676
|
+
account={address}
|
|
677
|
+
provider={walletProvider}
|
|
678
|
+
srcToken={srcToken}
|
|
679
|
+
dstToken={dstToken}
|
|
680
|
+
srcBalance={srcBalance}
|
|
681
|
+
dstBalance={dstBalance}
|
|
682
|
+
srcUsd1Token={srcUsd}
|
|
683
|
+
dstUsd1Token={dstUsd}
|
|
684
|
+
marketReferencePrice={marketPrice}
|
|
685
|
+
priceProtection={0.05}
|
|
686
|
+
minChunkSizeUsd={5}
|
|
687
|
+
components={{
|
|
688
|
+
Button: MyButton,
|
|
689
|
+
Tooltip: MyTooltip,
|
|
690
|
+
TokenLogo: MyTokenLogo,
|
|
691
|
+
Spinner: <MySpinner />,
|
|
692
|
+
}}
|
|
693
|
+
callbacks={{
|
|
694
|
+
onOrderCreated: (order) => toast.success("Order created!"),
|
|
695
|
+
onApproveSuccess: () => toast.success("Approved!"),
|
|
696
|
+
}}
|
|
697
|
+
>
|
|
698
|
+
<TradingUI module={module} />
|
|
699
|
+
</SpotProvider>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// === MAIN UI LAYOUT ===
|
|
704
|
+
|
|
705
|
+
function TradingUI({ module }: { module: Module }) {
|
|
706
|
+
return (
|
|
707
|
+
<div className="flex flex-col gap-4">
|
|
708
|
+
<TokenInputs />
|
|
709
|
+
<PriceConfig module={module} />
|
|
710
|
+
<OrderConfig module={module} />
|
|
711
|
+
<ValidationErrors />
|
|
712
|
+
<SubmitOrderFlow />
|
|
713
|
+
<Disclaimer />
|
|
714
|
+
</div>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// === TOKEN INPUTS ===
|
|
719
|
+
|
|
720
|
+
function TokenInputs() {
|
|
721
|
+
return (
|
|
722
|
+
<div className="flex flex-col gap-2">
|
|
723
|
+
<SrcTokenInput />
|
|
724
|
+
<ToggleTokensButton />
|
|
725
|
+
<DstTokenInput />
|
|
726
|
+
</div>
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function SrcTokenInput() {
|
|
731
|
+
const { value, balance, usd, token, onChange, onMax } = useSrcTokenPanel();
|
|
732
|
+
|
|
733
|
+
return (
|
|
734
|
+
<div className="bg-card p-4 rounded-lg">
|
|
735
|
+
<div className="flex justify-between">
|
|
736
|
+
<span>From</span>
|
|
737
|
+
<span>Balance: {balance} <button onClick={onMax}>MAX</button></span>
|
|
738
|
+
</div>
|
|
739
|
+
<div className="flex items-center gap-2">
|
|
740
|
+
<input
|
|
741
|
+
type="text"
|
|
742
|
+
value={value}
|
|
743
|
+
onChange={(e) => onChange(e.target.value)}
|
|
744
|
+
placeholder="0.0"
|
|
745
|
+
/>
|
|
746
|
+
<TokenSelector token={token} />
|
|
747
|
+
</div>
|
|
748
|
+
<span className="text-sm text-muted">${usd}</span>
|
|
749
|
+
</div>
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function DstTokenInput() {
|
|
754
|
+
const { value, balance, usd, token, isLoading } = useDstTokenPanel();
|
|
755
|
+
|
|
756
|
+
return (
|
|
757
|
+
<div className="bg-card p-4 rounded-lg">
|
|
758
|
+
<div className="flex justify-between">
|
|
759
|
+
<span>To (estimated)</span>
|
|
760
|
+
<span>Balance: {balance}</span>
|
|
761
|
+
</div>
|
|
762
|
+
<div className="flex items-center gap-2">
|
|
763
|
+
{isLoading ? <Spinner /> : <span>{value || "0.0"}</span>}
|
|
764
|
+
<TokenSelector token={token} />
|
|
765
|
+
</div>
|
|
766
|
+
<span className="text-sm text-muted">${usd}</span>
|
|
767
|
+
</div>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// === PRICE CONFIGURATION ===
|
|
772
|
+
|
|
773
|
+
function PriceConfig({ module }: { module: Module }) {
|
|
774
|
+
return (
|
|
775
|
+
<div className="bg-card p-4 rounded-lg flex flex-col gap-4">
|
|
776
|
+
<PriceHeader />
|
|
777
|
+
{(module === Module.STOP_LOSS || module === Module.TAKE_PROFIT) && (
|
|
778
|
+
<TriggerPriceInput />
|
|
779
|
+
)}
|
|
780
|
+
<LimitPriceInput module={module} />
|
|
781
|
+
</div>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function PriceHeader() {
|
|
786
|
+
const { isInverted, isMarketPrice, fromToken, onInvert } = useInvertTradePanel();
|
|
787
|
+
|
|
788
|
+
return (
|
|
789
|
+
<div className="flex justify-between items-center">
|
|
790
|
+
<span>
|
|
791
|
+
{isInverted ? "Buy" : "Sell"} {fromToken?.symbol}{" "}
|
|
792
|
+
{isMarketPrice ? "at best rate" : "at rate"}
|
|
793
|
+
</span>
|
|
794
|
+
{!isMarketPrice && (
|
|
795
|
+
<button onClick={onInvert}>⇄</button>
|
|
796
|
+
)}
|
|
797
|
+
</div>
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function TriggerPriceInput() {
|
|
802
|
+
const {
|
|
803
|
+
price,
|
|
804
|
+
usd,
|
|
805
|
+
percentage,
|
|
806
|
+
toToken,
|
|
807
|
+
label,
|
|
808
|
+
tooltip,
|
|
809
|
+
hide,
|
|
810
|
+
onChange,
|
|
811
|
+
onPercentageChange,
|
|
812
|
+
onReset,
|
|
813
|
+
} = useTriggerPricePanel();
|
|
814
|
+
|
|
815
|
+
if (hide) return null;
|
|
816
|
+
|
|
817
|
+
return (
|
|
818
|
+
<div className="flex flex-col gap-2">
|
|
819
|
+
<div className="flex justify-between">
|
|
820
|
+
<Label text={label} tooltip={tooltip} />
|
|
821
|
+
<button onClick={onReset}>Reset</button>
|
|
822
|
+
</div>
|
|
823
|
+
<div className="flex gap-2">
|
|
824
|
+
<div className="flex-1">
|
|
825
|
+
<span>{toToken?.symbol}</span>
|
|
826
|
+
<input value={price} onChange={(e) => onChange(e.target.value)} />
|
|
827
|
+
<span>${usd}</span>
|
|
828
|
+
</div>
|
|
829
|
+
<input
|
|
830
|
+
className="w-24"
|
|
831
|
+
value={percentage}
|
|
832
|
+
onChange={(e) => onPercentageChange(e.target.value)}
|
|
833
|
+
placeholder="0%"
|
|
834
|
+
/>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function LimitPriceInput({ module }: { module: Module }) {
|
|
841
|
+
const {
|
|
842
|
+
price,
|
|
843
|
+
usd,
|
|
844
|
+
percentage,
|
|
845
|
+
toToken,
|
|
846
|
+
label,
|
|
847
|
+
tooltip,
|
|
848
|
+
isLimitPrice,
|
|
849
|
+
isLoading,
|
|
850
|
+
onChange,
|
|
851
|
+
onPercentageChange,
|
|
852
|
+
onReset,
|
|
853
|
+
toggleLimitPrice,
|
|
854
|
+
} = useLimitPricePanel();
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<div className="flex flex-col gap-2">
|
|
858
|
+
<div className="flex items-center gap-2">
|
|
859
|
+
{/* Show toggle only for non-LIMIT modules */}
|
|
860
|
+
{module !== Module.LIMIT && (
|
|
861
|
+
<Switch checked={isLimitPrice} onCheckedChange={toggleLimitPrice} />
|
|
862
|
+
)}
|
|
863
|
+
<Label text={label} tooltip={tooltip} />
|
|
864
|
+
{isLimitPrice && <button onClick={onReset}>Reset</button>}
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
{isLimitPrice && (
|
|
868
|
+
<div className="flex gap-2">
|
|
869
|
+
<div className="flex-1">
|
|
870
|
+
<span>{toToken?.symbol}</span>
|
|
871
|
+
{isLoading ? (
|
|
872
|
+
<Spinner />
|
|
873
|
+
) : (
|
|
874
|
+
<input value={price} onChange={(e) => onChange(e.target.value)} />
|
|
875
|
+
)}
|
|
876
|
+
<span>${usd}</span>
|
|
877
|
+
</div>
|
|
878
|
+
<input
|
|
879
|
+
className="w-24"
|
|
880
|
+
value={percentage}
|
|
881
|
+
onChange={(e) => onPercentageChange(e.target.value)}
|
|
882
|
+
placeholder="0%"
|
|
883
|
+
/>
|
|
884
|
+
</div>
|
|
885
|
+
)}
|
|
886
|
+
</div>
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// === ORDER CONFIGURATION ===
|
|
891
|
+
|
|
892
|
+
function OrderConfig({ module }: { module: Module }) {
|
|
893
|
+
if (module === Module.TWAP) {
|
|
894
|
+
return (
|
|
895
|
+
<div className="flex gap-4">
|
|
896
|
+
<TradesInput />
|
|
897
|
+
<FillDelayInput />
|
|
898
|
+
</div>
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// LIMIT, STOP_LOSS, TAKE_PROFIT
|
|
903
|
+
return <DurationInput />;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function TradesInput() {
|
|
907
|
+
const { totalTrades, label, tooltip, error, onChange } = useTradesPanel();
|
|
908
|
+
|
|
909
|
+
return (
|
|
910
|
+
<div className={`bg-card p-4 rounded-lg ${error ? "border-red-500" : ""}`}>
|
|
911
|
+
<Label text={label} tooltip={tooltip} />
|
|
912
|
+
<div className="flex items-center gap-2">
|
|
913
|
+
<input
|
|
914
|
+
type="number"
|
|
915
|
+
value={totalTrades || ""}
|
|
916
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
917
|
+
/>
|
|
918
|
+
<span>Trades</span>
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function FillDelayInput() {
|
|
925
|
+
const { fillDelay, label, tooltip, onInputChange, onUnitSelect } = useFillDelayPanel();
|
|
926
|
+
|
|
927
|
+
return (
|
|
928
|
+
<div className="bg-card p-4 rounded-lg">
|
|
929
|
+
<Label text={label} tooltip={tooltip} />
|
|
930
|
+
<div className="flex items-center gap-2">
|
|
931
|
+
<input
|
|
932
|
+
type="number"
|
|
933
|
+
value={fillDelay.value || ""}
|
|
934
|
+
onChange={(e) => onInputChange(e.target.value)}
|
|
935
|
+
/>
|
|
936
|
+
<select
|
|
937
|
+
value={fillDelay.unit}
|
|
938
|
+
onChange={(e) => onUnitSelect(Number(e.target.value))}
|
|
939
|
+
>
|
|
940
|
+
{DEFAULT_DURATION_OPTIONS.map((opt) => (
|
|
941
|
+
<option key={opt.value} value={opt.value}>{opt.text}</option>
|
|
942
|
+
))}
|
|
943
|
+
</select>
|
|
944
|
+
</div>
|
|
945
|
+
</div>
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function DurationInput() {
|
|
950
|
+
const { duration, label, tooltip, onInputChange, onUnitSelect } = useDurationPanel();
|
|
951
|
+
|
|
952
|
+
return (
|
|
953
|
+
<div className="bg-card p-4 rounded-lg">
|
|
954
|
+
<Label text={label} tooltip={tooltip} />
|
|
955
|
+
<div className="flex items-center gap-2">
|
|
956
|
+
<input
|
|
957
|
+
type="number"
|
|
958
|
+
value={duration.value || ""}
|
|
959
|
+
onChange={(e) => onInputChange(e.target.value)}
|
|
960
|
+
/>
|
|
961
|
+
<select
|
|
962
|
+
value={duration.unit}
|
|
963
|
+
onChange={(e) => onUnitSelect(Number(e.target.value))}
|
|
964
|
+
>
|
|
965
|
+
{DEFAULT_DURATION_OPTIONS.map((opt) => (
|
|
966
|
+
<option key={opt.value} value={opt.value}>{opt.text}</option>
|
|
967
|
+
))}
|
|
968
|
+
</select>
|
|
969
|
+
</div>
|
|
970
|
+
</div>
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// === VALIDATION ===
|
|
975
|
+
|
|
976
|
+
function ValidationErrors() {
|
|
977
|
+
const error = useInputErrors();
|
|
978
|
+
|
|
979
|
+
if (!error) return null;
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<div className="bg-red-500/20 p-3 rounded-lg flex items-center gap-2">
|
|
983
|
+
<AlertIcon />
|
|
984
|
+
<span>{error.message}</span>
|
|
985
|
+
</div>
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// === SUBMIT ORDER ===
|
|
990
|
+
|
|
991
|
+
function SubmitOrderFlow() {
|
|
992
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
993
|
+
const {
|
|
994
|
+
onSubmit,
|
|
995
|
+
onOpenModal,
|
|
996
|
+
onCloseModal,
|
|
997
|
+
isLoading,
|
|
998
|
+
parsedError,
|
|
999
|
+
} = useSubmitOrderPanel();
|
|
1000
|
+
|
|
1001
|
+
const handleOpen = () => {
|
|
1002
|
+
setIsOpen(true);
|
|
1003
|
+
onOpenModal();
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
const handleClose = () => {
|
|
1007
|
+
setIsOpen(false);
|
|
1008
|
+
onCloseModal();
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
return (
|
|
1012
|
+
<>
|
|
1013
|
+
<SubmitButton onClick={handleOpen} />
|
|
1014
|
+
|
|
1015
|
+
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
1016
|
+
<DialogContent>
|
|
1017
|
+
{parsedError ? (
|
|
1018
|
+
<ErrorView error={parsedError} onClose={handleClose} />
|
|
1019
|
+
) : (
|
|
1020
|
+
<Components.SubmitOrderPanel
|
|
1021
|
+
reviewDetails={
|
|
1022
|
+
<div className="flex flex-col gap-4">
|
|
1023
|
+
<DisclaimerCheckbox />
|
|
1024
|
+
<Button onClick={onSubmit} isLoading={isLoading}>
|
|
1025
|
+
Create Order
|
|
1026
|
+
</Button>
|
|
1027
|
+
</div>
|
|
1028
|
+
}
|
|
1029
|
+
/>
|
|
1030
|
+
)}
|
|
1031
|
+
</DialogContent>
|
|
1032
|
+
</Dialog>
|
|
1033
|
+
</>
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function SubmitButton({ onClick }: { onClick: () => void }) {
|
|
1038
|
+
const { disabled, text, loading } = useSubmitOrderButton();
|
|
1039
|
+
|
|
1040
|
+
return (
|
|
1041
|
+
<Button onClick={onClick} disabled={disabled} isLoading={loading}>
|
|
1042
|
+
{text}
|
|
1043
|
+
</Button>
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// === DISCLAIMER ===
|
|
1048
|
+
|
|
1049
|
+
function Disclaimer() {
|
|
1050
|
+
const disclaimer = useDisclaimerPanel();
|
|
1051
|
+
|
|
1052
|
+
if (!disclaimer) return null;
|
|
1053
|
+
|
|
1054
|
+
return (
|
|
1055
|
+
<div className="bg-card p-3 rounded-lg flex items-start gap-2">
|
|
1056
|
+
<InfoIcon />
|
|
1057
|
+
<p>
|
|
1058
|
+
{disclaimer.text}{" "}
|
|
1059
|
+
<a href={disclaimer.url} target="_blank" rel="noopener">
|
|
1060
|
+
Learn more
|
|
1061
|
+
</a>
|
|
1062
|
+
</p>
|
|
1063
|
+
</div>
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// === HELPER COMPONENTS ===
|
|
1068
|
+
|
|
1069
|
+
function Label({ text, tooltip }: { text: string; tooltip?: string }) {
|
|
1070
|
+
return (
|
|
1071
|
+
<div className="flex items-center gap-1">
|
|
1072
|
+
<span>{text}</span>
|
|
1073
|
+
{tooltip && <Tooltip text={tooltip}><InfoIcon /></Tooltip>}
|
|
1074
|
+
</div>
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
---
|
|
1080
|
+
|
|
1081
|
+
## Integration Checklist
|
|
1082
|
+
|
|
1083
|
+
1. [ ] Install `@orbs-network/spot-react`
|
|
1084
|
+
2. [ ] Import styles: `import "@orbs-network/spot-react/styles.css"`
|
|
1085
|
+
3. [ ] Implement required components: Button, Tooltip, TokenLogo, Spinner
|
|
1086
|
+
4. [ ] Set up wallet connection
|
|
1087
|
+
5. [ ] Implement token data fetching (address, symbol, decimals, logoUrl)
|
|
1088
|
+
6. [ ] Implement balance fetching (wei strings)
|
|
1089
|
+
7. [ ] Implement USD price fetching (price for 1 token)
|
|
1090
|
+
8. [ ] Implement market reference price (output amount for input)
|
|
1091
|
+
9. [ ] Wrap UI with SpotProvider
|
|
1092
|
+
10. [ ] Build token panels with `useSrcTokenPanel`/`useDstTokenPanel`
|
|
1093
|
+
11. [ ] Build order config based on module type
|
|
1094
|
+
12. [ ] Build price panels with `useLimitPricePanel`/`useTriggerPricePanel`
|
|
1095
|
+
13. [ ] Add validation with `useInputErrors`
|
|
1096
|
+
14. [ ] Build submission with `useSubmitOrderPanel` + `Components.SubmitOrderPanel`
|
|
1097
|
+
15. [ ] Add order history with `useOrderHistoryPanel` + `Components.Orders`
|
|
1098
|
+
16. [ ] Implement callbacks for toast notifications
|