@orbs-network/spot-react 0.0.47 → 0.0.49

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