@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
DELETED
|
@@ -1,831 +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.
|
|
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
|
-
## Core Setup Pattern
|
|
18
|
-
|
|
19
|
-
### 1. Wrap your trading UI with SpotProvider
|
|
20
|
-
|
|
21
|
-
```tsx
|
|
22
|
-
import {
|
|
23
|
-
SpotProvider,
|
|
24
|
-
Module,
|
|
25
|
-
Partners,
|
|
26
|
-
Components,
|
|
27
|
-
Token,
|
|
28
|
-
useSrcTokenPanel,
|
|
29
|
-
useDstTokenPanel,
|
|
30
|
-
// ... other hooks
|
|
31
|
-
} from "@orbs-network/spot-react";
|
|
32
|
-
|
|
33
|
-
function TradingPage() {
|
|
34
|
-
// Get wallet connection (wagmi example)
|
|
35
|
-
const { address, chainId } = useAccount();
|
|
36
|
-
const { data: walletClient } = useWalletClient();
|
|
37
|
-
|
|
38
|
-
// Your token selection state
|
|
39
|
-
const [srcToken, setSrcToken] = useState<Token>();
|
|
40
|
-
const [dstToken, setDstToken] = useState<Token>();
|
|
41
|
-
|
|
42
|
-
// Fetch balances (wei strings)
|
|
43
|
-
const srcBalance = useBalance(srcToken?.address); // "1000000000000000000"
|
|
44
|
-
const dstBalance = useBalance(dstToken?.address);
|
|
45
|
-
|
|
46
|
-
// Fetch USD prices for 1 token
|
|
47
|
-
const srcUsd1Token = useUSDPrice(srcToken?.address); // "1850.50"
|
|
48
|
-
const dstUsd1Token = useUSDPrice(dstToken?.address);
|
|
49
|
-
|
|
50
|
-
// Calculate market reference price (expected output for input amount)
|
|
51
|
-
const marketReferencePrice = useMarketPrice(srcToken, dstToken);
|
|
52
|
-
|
|
53
|
-
// Settings
|
|
54
|
-
const slippage = 0.05; // 5%
|
|
55
|
-
|
|
56
|
-
// Callbacks for order lifecycle events
|
|
57
|
-
const callbacks = useOrderCallbacks();
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<SpotProvider
|
|
61
|
-
partner={Partners.Quick} // Your partner ID
|
|
62
|
-
chainId={chainId}
|
|
63
|
-
module={Module.TWAP} // TWAP | LIMIT | STOP_LOSS | TAKE_PROFIT
|
|
64
|
-
priceProtection={slippage}
|
|
65
|
-
minChunkSizeUsd={5}
|
|
66
|
-
|
|
67
|
-
// Token data
|
|
68
|
-
srcToken={srcToken}
|
|
69
|
-
dstToken={dstToken}
|
|
70
|
-
srcBalance={srcBalance}
|
|
71
|
-
dstBalance={dstBalance}
|
|
72
|
-
srcUsd1Token={srcUsd1Token}
|
|
73
|
-
dstUsd1Token={dstUsd1Token}
|
|
74
|
-
|
|
75
|
-
// Market price - CRITICAL: This should be the expected output amount
|
|
76
|
-
// for the typed input amount, not just a 1:1 ratio
|
|
77
|
-
marketReferencePrice={marketReferencePrice}
|
|
78
|
-
|
|
79
|
-
// Wallet
|
|
80
|
-
account={address}
|
|
81
|
-
provider={walletClient?.transport}
|
|
82
|
-
|
|
83
|
-
// UI Components (required)
|
|
84
|
-
components={{
|
|
85
|
-
Button: YourButton,
|
|
86
|
-
Tooltip: YourTooltip,
|
|
87
|
-
TokenLogo: YourTokenLogo,
|
|
88
|
-
Spinner: <YourSpinner />,
|
|
89
|
-
}}
|
|
90
|
-
|
|
91
|
-
// Optional
|
|
92
|
-
callbacks={callbacks}
|
|
93
|
-
refetchBalances={() => refetchBalances()}
|
|
94
|
-
useToken={useTokenByAddress} // Hook to get token by address
|
|
95
|
-
fees={0.25} // Fee percentage
|
|
96
|
-
>
|
|
97
|
-
<YourTradingUI />
|
|
98
|
-
</SpotProvider>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### 2. Token Type Definition
|
|
104
|
-
```tsx
|
|
105
|
-
type Token = {
|
|
106
|
-
address: string; // Contract address
|
|
107
|
-
symbol: string; // Token symbol (e.g., "ETH")
|
|
108
|
-
decimals: number; // Token decimals (e.g., 18)
|
|
109
|
-
logoUrl: string; // Logo image URL
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Example conversion from your Currency type:
|
|
113
|
-
const parseToSpotToken = (currency?: Currency): Token | undefined => {
|
|
114
|
-
if (!currency) return undefined;
|
|
115
|
-
return {
|
|
116
|
-
address: currency.address,
|
|
117
|
-
decimals: currency.decimals,
|
|
118
|
-
symbol: currency.symbol,
|
|
119
|
-
logoUrl: currency.logoUrl,
|
|
120
|
-
};
|
|
121
|
-
};
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### 3. Required UI Components
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
// Button Component
|
|
128
|
-
type ButtonProps = {
|
|
129
|
-
children: ReactNode;
|
|
130
|
-
onClick: () => void;
|
|
131
|
-
disabled?: boolean;
|
|
132
|
-
loading?: boolean;
|
|
133
|
-
style?: CSSProperties;
|
|
134
|
-
allowClickWhileLoading?: boolean;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const SpotButton = (props: ButtonProps) => {
|
|
138
|
-
return (
|
|
139
|
-
<Button isLoading={props.loading} onClick={props.onClick} disabled={props.disabled}>
|
|
140
|
-
{props.children}
|
|
141
|
-
</Button>
|
|
142
|
-
);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Tooltip Component
|
|
146
|
-
type TooltipProps = {
|
|
147
|
-
children?: ReactNode;
|
|
148
|
-
tooltipText?: string;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const SpotTooltip = (props: TooltipProps) => {
|
|
152
|
-
if (!props.tooltipText) return null;
|
|
153
|
-
return (
|
|
154
|
-
<Tooltip>
|
|
155
|
-
<TooltipTrigger>{props.children || <InfoIcon />}</TooltipTrigger>
|
|
156
|
-
<TooltipContent>{props.tooltipText}</TooltipContent>
|
|
157
|
-
</Tooltip>
|
|
158
|
-
);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Token Logo Component
|
|
162
|
-
type TokenLogoProps = {
|
|
163
|
-
token?: Token;
|
|
164
|
-
size?: number;
|
|
165
|
-
className?: string;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const SpotTokenLogo = ({ token }: TokenLogoProps) => {
|
|
169
|
-
return <Avatar src={token?.logoUrl ?? ""} />;
|
|
170
|
-
};
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## Using the Hooks
|
|
176
|
-
|
|
177
|
-
### Token Panel Hooks
|
|
178
|
-
|
|
179
|
-
#### `useSrcTokenPanel` / `useDstTokenPanel`
|
|
180
|
-
Controls the source (sell) and destination (buy) token inputs.
|
|
181
|
-
|
|
182
|
-
```tsx
|
|
183
|
-
const TokenPanel = ({ isSrcToken }: { isSrcToken: boolean }) => {
|
|
184
|
-
const srcTokenPanel = useSrcTokenPanel();
|
|
185
|
-
const dstTokenPanel = useDstTokenPanel();
|
|
186
|
-
const panel = isSrcToken ? srcTokenPanel : dstTokenPanel;
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<div>
|
|
190
|
-
<input
|
|
191
|
-
value={panel.value}
|
|
192
|
-
onChange={(e) => panel.onChange(e.target.value)}
|
|
193
|
-
disabled={!isSrcToken} // Only src is editable
|
|
194
|
-
/>
|
|
195
|
-
{panel.isLoading && <Spinner />}
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
#### `useTypedSrcAmount`
|
|
202
|
-
Get the current typed source amount.
|
|
203
|
-
|
|
204
|
-
```tsx
|
|
205
|
-
const Listener = () => {
|
|
206
|
-
const { amount } = useTypedSrcAmount();
|
|
207
|
-
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
// Sync with your app state
|
|
210
|
-
setInputAmount(amount ?? "");
|
|
211
|
-
}, [amount]);
|
|
212
|
-
|
|
213
|
-
return null;
|
|
214
|
-
};
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Order Configuration Hooks
|
|
218
|
-
|
|
219
|
-
#### `useTradesPanel` (TWAP only)
|
|
220
|
-
Configure number of trades for TWAP orders.
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
const TradesPanel = () => {
|
|
224
|
-
const { totalTrades, onChange, label, tooltip, error } = useTradesPanel();
|
|
225
|
-
|
|
226
|
-
return (
|
|
227
|
-
<Card error={Boolean(error)}>
|
|
228
|
-
<Label title={label} tooltip={tooltip} />
|
|
229
|
-
<div className="flex items-center gap-2">
|
|
230
|
-
<NumericInput
|
|
231
|
-
value={totalTrades?.toString() ?? ""}
|
|
232
|
-
onChange={(val) => onChange(Number(val))}
|
|
233
|
-
/>
|
|
234
|
-
<span>Trades</span>
|
|
235
|
-
</div>
|
|
236
|
-
</Card>
|
|
237
|
-
);
|
|
238
|
-
};
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
#### `useDurationPanel`
|
|
242
|
-
Configure order expiration duration.
|
|
243
|
-
|
|
244
|
-
```tsx
|
|
245
|
-
import { DEFAULT_DURATION_OPTIONS } from "@orbs-network/spot-react";
|
|
246
|
-
// Options: [{ text: "Minutes", value: 60 }, { text: "Hours", value: 3600 }, { text: "Days", value: 86400 }]
|
|
247
|
-
|
|
248
|
-
const DurationPanel = () => {
|
|
249
|
-
const { duration, onInputChange, onUnitSelect, label, tooltip } = useDurationPanel();
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<Card>
|
|
253
|
-
<Label title={label} tooltip={tooltip} />
|
|
254
|
-
<div className="flex items-center gap-2">
|
|
255
|
-
<NumericInput
|
|
256
|
-
value={duration.value?.toString() ?? ""}
|
|
257
|
-
onChange={(val) => onInputChange(val)}
|
|
258
|
-
/>
|
|
259
|
-
<Select
|
|
260
|
-
value={duration.unit}
|
|
261
|
-
onChange={(unit) => onUnitSelect(unit)}
|
|
262
|
-
options={DEFAULT_DURATION_OPTIONS}
|
|
263
|
-
/>
|
|
264
|
-
</div>
|
|
265
|
-
</Card>
|
|
266
|
-
);
|
|
267
|
-
};
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
#### `useFillDelayPanel` (TWAP only)
|
|
271
|
-
Configure delay between individual trades.
|
|
272
|
-
|
|
273
|
-
```tsx
|
|
274
|
-
const FillDelayPanel = () => {
|
|
275
|
-
const { fillDelay, onInputChange, onUnitSelect, label, tooltip } = useFillDelayPanel();
|
|
276
|
-
|
|
277
|
-
return (
|
|
278
|
-
<Card>
|
|
279
|
-
<Label title={label} tooltip={tooltip} />
|
|
280
|
-
<div className="flex items-center gap-2">
|
|
281
|
-
<NumericInput
|
|
282
|
-
value={fillDelay.value?.toString() ?? ""}
|
|
283
|
-
onChange={(val) => onInputChange(val)}
|
|
284
|
-
/>
|
|
285
|
-
<Select
|
|
286
|
-
value={fillDelay.unit}
|
|
287
|
-
onChange={(unit) => onUnitSelect(unit)}
|
|
288
|
-
options={DEFAULT_DURATION_OPTIONS}
|
|
289
|
-
/>
|
|
290
|
-
</div>
|
|
291
|
-
</Card>
|
|
292
|
-
);
|
|
293
|
-
};
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Price Hooks
|
|
297
|
-
|
|
298
|
-
#### `useLimitPricePanel`
|
|
299
|
-
Configure limit price for orders.
|
|
300
|
-
|
|
301
|
-
```tsx
|
|
302
|
-
const LimitPricePanel = () => {
|
|
303
|
-
const {
|
|
304
|
-
price, // Current price value
|
|
305
|
-
onChange, // Update price
|
|
306
|
-
percentage, // Percentage from market price
|
|
307
|
-
onPercentageChange, // Update via percentage
|
|
308
|
-
usd, // USD value
|
|
309
|
-
isLimitPrice, // Is limit price enabled
|
|
310
|
-
toggleLimitPrice, // Toggle on/off
|
|
311
|
-
onReset, // Reset to market price
|
|
312
|
-
isLoading,
|
|
313
|
-
toToken, // Destination token
|
|
314
|
-
label,
|
|
315
|
-
tooltip,
|
|
316
|
-
} = useLimitPricePanel();
|
|
317
|
-
|
|
318
|
-
return (
|
|
319
|
-
<div>
|
|
320
|
-
<div className="flex items-center gap-2">
|
|
321
|
-
{/* Toggle for non-LIMIT modules */}
|
|
322
|
-
<Switch checked={isLimitPrice} onCheckedChange={toggleLimitPrice} />
|
|
323
|
-
<Label title={label} tooltip={tooltip} />
|
|
324
|
-
{isLimitPrice && <button onClick={onReset}>Set to default</button>}
|
|
325
|
-
</div>
|
|
326
|
-
|
|
327
|
-
{isLimitPrice && (
|
|
328
|
-
<div className="flex gap-2">
|
|
329
|
-
<div>
|
|
330
|
-
<span>{toToken?.symbol}</span>
|
|
331
|
-
<NumericInput value={price} onChange={onChange} isLoading={isLoading} />
|
|
332
|
-
<span>${usd}</span>
|
|
333
|
-
</div>
|
|
334
|
-
<NumericInput
|
|
335
|
-
value={percentage}
|
|
336
|
-
onChange={onPercentageChange}
|
|
337
|
-
suffix="%"
|
|
338
|
-
allowNegative
|
|
339
|
-
/>
|
|
340
|
-
</div>
|
|
341
|
-
)}
|
|
342
|
-
</div>
|
|
343
|
-
);
|
|
344
|
-
};
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
#### `useTriggerPricePanel` (Stop-Loss / Take-Profit only)
|
|
348
|
-
Configure trigger price for conditional orders.
|
|
349
|
-
|
|
350
|
-
```tsx
|
|
351
|
-
const TriggerPricePanel = () => {
|
|
352
|
-
const {
|
|
353
|
-
price,
|
|
354
|
-
onChange,
|
|
355
|
-
percentage,
|
|
356
|
-
onPercentageChange,
|
|
357
|
-
onReset,
|
|
358
|
-
toToken,
|
|
359
|
-
usd,
|
|
360
|
-
label,
|
|
361
|
-
tooltip,
|
|
362
|
-
} = useTriggerPricePanel();
|
|
363
|
-
|
|
364
|
-
return (
|
|
365
|
-
<div>
|
|
366
|
-
<Label title={label} tooltip={tooltip} />
|
|
367
|
-
<button onClick={onReset}>Set to default</button>
|
|
368
|
-
<PriceInput
|
|
369
|
-
symbol={toToken?.symbol}
|
|
370
|
-
value={price}
|
|
371
|
-
onChange={onChange}
|
|
372
|
-
percentage={percentage}
|
|
373
|
-
onPercentageChange={onPercentageChange}
|
|
374
|
-
usd={usd}
|
|
375
|
-
/>
|
|
376
|
-
</div>
|
|
377
|
-
);
|
|
378
|
-
};
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### `useInvertTradePanel`
|
|
382
|
-
Invert the price display direction.
|
|
383
|
-
|
|
384
|
-
```tsx
|
|
385
|
-
const PricesHeader = () => {
|
|
386
|
-
const { onInvert, isInverted, fromToken, isMarketPrice } = useInvertTradePanel();
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
<div className="flex items-center justify-between">
|
|
390
|
-
<span>
|
|
391
|
-
{isInverted ? "Buy " : "Sell "}
|
|
392
|
-
{fromToken?.symbol} {isMarketPrice ? "at best rate" : "at rate"}
|
|
393
|
-
</span>
|
|
394
|
-
{!isMarketPrice && (
|
|
395
|
-
<button onClick={onInvert}>
|
|
396
|
-
<ArrowLeftRightIcon />
|
|
397
|
-
</button>
|
|
398
|
-
)}
|
|
399
|
-
</div>
|
|
400
|
-
);
|
|
401
|
-
};
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Validation
|
|
405
|
-
|
|
406
|
-
#### `useInputErrors`
|
|
407
|
-
Get current validation errors.
|
|
408
|
-
|
|
409
|
-
```tsx
|
|
410
|
-
const InputsErrorPanel = () => {
|
|
411
|
-
const error = useInputErrors();
|
|
412
|
-
// Returns: { type: InputErrors, value: string | number, message: string } | null
|
|
413
|
-
|
|
414
|
-
if (!error) return null;
|
|
415
|
-
|
|
416
|
-
return (
|
|
417
|
-
<div className="bg-destructive/50 p-2 rounded-md">
|
|
418
|
-
<AlertTriangleIcon />
|
|
419
|
-
<p>{error.message}</p>
|
|
420
|
-
</div>
|
|
421
|
-
);
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
// Available error types:
|
|
425
|
-
enum InputErrors {
|
|
426
|
-
EMPTY_LIMIT_PRICE,
|
|
427
|
-
MAX_CHUNKS,
|
|
428
|
-
MIN_CHUNKS,
|
|
429
|
-
MIN_TRADE_SIZE,
|
|
430
|
-
MAX_FILL_DELAY,
|
|
431
|
-
MIN_FILL_DELAY,
|
|
432
|
-
MAX_ORDER_DURATION,
|
|
433
|
-
MIN_ORDER_DURATION,
|
|
434
|
-
MISSING_LIMIT_PRICE,
|
|
435
|
-
STOP_LOSS_TRIGGER_PRICE_GREATER_THAN_MARKET_PRICE,
|
|
436
|
-
TRIGGER_LIMIT_PRICE_GREATER_THAN_TRIGGER_PRICE,
|
|
437
|
-
TAKE_PROFIT_TRIGGER_PRICE_LESS_THAN_MARKET_PRICE,
|
|
438
|
-
EMPTY_TRIGGER_PRICE,
|
|
439
|
-
INSUFFICIENT_BALANCE,
|
|
440
|
-
MAX_ORDER_SIZE,
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Order Submission
|
|
445
|
-
|
|
446
|
-
#### `useSubmitOrderPanel`
|
|
447
|
-
Full control over order submission flow.
|
|
448
|
-
|
|
449
|
-
```tsx
|
|
450
|
-
const SubmitSwap = () => {
|
|
451
|
-
const {
|
|
452
|
-
onSubmit, // Call to submit order
|
|
453
|
-
onOpenModal, // Call when opening confirmation modal
|
|
454
|
-
onCloseModal, // Call when closing modal
|
|
455
|
-
isLoading, // Submission in progress
|
|
456
|
-
isSuccess, // Order created successfully
|
|
457
|
-
isFailed, // Order creation failed
|
|
458
|
-
parsedError, // Error details { message, code }
|
|
459
|
-
orderId, // Created order ID
|
|
460
|
-
status, // SwapStatus enum
|
|
461
|
-
reset, // Reset swap state
|
|
462
|
-
} = useSubmitOrderPanel();
|
|
463
|
-
|
|
464
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
465
|
-
|
|
466
|
-
const handleOpen = () => {
|
|
467
|
-
setIsOpen(true);
|
|
468
|
-
onOpenModal(); // Important: call this to prepare state
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const handleClose = () => {
|
|
472
|
-
setIsOpen(false);
|
|
473
|
-
onCloseModal(); // Important: call this to cleanup
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
return (
|
|
477
|
-
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
478
|
-
<SubmitButton onClick={handleOpen} />
|
|
479
|
-
<DialogContent>
|
|
480
|
-
{parsedError ? (
|
|
481
|
-
<ErrorView error={parsedError} onClose={handleClose} />
|
|
482
|
-
) : (
|
|
483
|
-
<Components.SubmitOrderPanel
|
|
484
|
-
reviewDetails={
|
|
485
|
-
<Button onClick={onSubmit} isLoading={isLoading}>
|
|
486
|
-
Create Order
|
|
487
|
-
</Button>
|
|
488
|
-
}
|
|
489
|
-
/>
|
|
490
|
-
)}
|
|
491
|
-
</DialogContent>
|
|
492
|
-
</Dialog>
|
|
493
|
-
);
|
|
494
|
-
};
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
#### `useSubmitOrderButton`
|
|
498
|
-
Just the button state (simpler).
|
|
499
|
-
|
|
500
|
-
```tsx
|
|
501
|
-
const SubmitButton = ({ onClick }: { onClick: () => void }) => {
|
|
502
|
-
const { disabled, text, loading } = useSubmitOrderButton();
|
|
503
|
-
// text: "Place Order" | "Enter Amount" | "Insufficient Funds" | "No Liquidity"
|
|
504
|
-
|
|
505
|
-
return (
|
|
506
|
-
<Button onClick={onClick} disabled={disabled} isLoading={loading}>
|
|
507
|
-
{text}
|
|
508
|
-
</Button>
|
|
509
|
-
);
|
|
510
|
-
};
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
### Order History
|
|
514
|
-
|
|
515
|
-
#### `useOrderHistoryPanel`
|
|
516
|
-
Display and filter order history.
|
|
517
|
-
|
|
518
|
-
```tsx
|
|
519
|
-
import { Components, OrderStatus, SelectMeuItem } from "@orbs-network/spot-react";
|
|
520
|
-
|
|
521
|
-
const OrdersPanel = () => {
|
|
522
|
-
const {
|
|
523
|
-
orders, // { all: Order[], open: Order[], completed: Order[], ... }
|
|
524
|
-
selectedOrder, // Currently selected order details
|
|
525
|
-
onSelectStatus, // Filter by status
|
|
526
|
-
statuses, // Available status filters
|
|
527
|
-
selectedStatus, // Current filter
|
|
528
|
-
onHideSelectedOrder, // Go back to list
|
|
529
|
-
} = useOrderHistoryPanel();
|
|
530
|
-
|
|
531
|
-
const handleSelectStatus = (item: SelectMeuItem) => {
|
|
532
|
-
onSelectStatus(item.value === "all" ? undefined : item.value as OrderStatus);
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
return (
|
|
536
|
-
<div>
|
|
537
|
-
<header>
|
|
538
|
-
{selectedOrder && (
|
|
539
|
-
<button onClick={onHideSelectedOrder}>
|
|
540
|
-
<ArrowLeftIcon />
|
|
541
|
-
</button>
|
|
542
|
-
)}
|
|
543
|
-
<h2>{selectedOrder?.title ?? `Orders (${orders.all?.length})`}</h2>
|
|
544
|
-
</header>
|
|
545
|
-
|
|
546
|
-
{!selectedOrder && (
|
|
547
|
-
<Select
|
|
548
|
-
value={selectedStatus}
|
|
549
|
-
onChange={handleSelectStatus}
|
|
550
|
-
options={statuses}
|
|
551
|
-
/>
|
|
552
|
-
)}
|
|
553
|
-
|
|
554
|
-
{/* Pre-built Orders component */}
|
|
555
|
-
<Components.Orders />
|
|
556
|
-
</div>
|
|
557
|
-
);
|
|
558
|
-
};
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
### Other Useful Hooks
|
|
562
|
-
|
|
563
|
-
#### `useDisclaimerPanel`
|
|
564
|
-
Show module-specific disclaimer.
|
|
565
|
-
|
|
566
|
-
```tsx
|
|
567
|
-
const Disclaimer = () => {
|
|
568
|
-
const message = useDisclaimerPanel();
|
|
569
|
-
// Returns: { text: string, url: string } | null
|
|
570
|
-
|
|
571
|
-
if (!message) return null;
|
|
572
|
-
|
|
573
|
-
return (
|
|
574
|
-
<div>
|
|
575
|
-
<InfoIcon />
|
|
576
|
-
<p>
|
|
577
|
-
{message.text}{" "}
|
|
578
|
-
<a href={message.url} target="_blank">Learn more</a>
|
|
579
|
-
</p>
|
|
580
|
-
</div>
|
|
581
|
-
);
|
|
582
|
-
};
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
#### `useTranslations`
|
|
586
|
-
Get translated strings.
|
|
587
|
-
|
|
588
|
-
```tsx
|
|
589
|
-
const t = useTranslations();
|
|
590
|
-
|
|
591
|
-
// Usage:
|
|
592
|
-
t("placeOrder") // "Place Order"
|
|
593
|
-
t("enterAmount") // "Enter Amount"
|
|
594
|
-
t("insufficientFunds") // "Insufficient Funds"
|
|
595
|
-
t("noLiquidity") // "No Liquidity"
|
|
596
|
-
// ... many more keys
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
#### `useFormatNumber`
|
|
600
|
-
Format numbers with proper locale.
|
|
601
|
-
|
|
602
|
-
```tsx
|
|
603
|
-
const { value: formatted } = useFormatNumber({ value: "1234.5678", decimalScale: 2 });
|
|
604
|
-
// "1,234.57"
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
#### `usePartnerChains`
|
|
608
|
-
Get supported chains for current partner.
|
|
609
|
-
|
|
610
|
-
```tsx
|
|
611
|
-
const chains = usePartnerChains();
|
|
612
|
-
// [1, 137, 56, ...]
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
#### `useAddresses`
|
|
616
|
-
Get contract addresses for current config.
|
|
617
|
-
|
|
618
|
-
```tsx
|
|
619
|
-
const addresses = useAddresses();
|
|
620
|
-
// { twap: "0x...", exchange: "0x...", ... }
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
---
|
|
624
|
-
|
|
625
|
-
## Callbacks Reference
|
|
626
|
-
|
|
627
|
-
Set up callbacks to handle order lifecycle events:
|
|
628
|
-
|
|
629
|
-
```tsx
|
|
630
|
-
const useOrderCallbacks = () => {
|
|
631
|
-
const toastRef = useRef<number>(null);
|
|
632
|
-
|
|
633
|
-
return {
|
|
634
|
-
// Wrap ETH -> WETH
|
|
635
|
-
onWrapRequest: () => {
|
|
636
|
-
toastRef.current = toast.loading("Wrapping ETH...");
|
|
637
|
-
},
|
|
638
|
-
onWrapSuccess: ({ txHash, explorerUrl, amount }) => {
|
|
639
|
-
toast.success("Wrapped ETH", {
|
|
640
|
-
id: toastRef.current,
|
|
641
|
-
action: <a href={explorerUrl}>View</a>,
|
|
642
|
-
});
|
|
643
|
-
},
|
|
644
|
-
|
|
645
|
-
// Token Approval
|
|
646
|
-
onApproveRequest: () => {
|
|
647
|
-
toast.loading("Approving token...");
|
|
648
|
-
},
|
|
649
|
-
onApproveSuccess: ({ txHash, explorerUrl, token, amount }) => {
|
|
650
|
-
toast.success(`Approved ${token.symbol}`);
|
|
651
|
-
},
|
|
652
|
-
|
|
653
|
-
// Order Signing
|
|
654
|
-
onSignOrderRequest: () => {
|
|
655
|
-
toast.loading("Creating order...", { description: "Proceed in wallet" });
|
|
656
|
-
},
|
|
657
|
-
onSignOrderSuccess: (signature) => {
|
|
658
|
-
// Signature received
|
|
659
|
-
},
|
|
660
|
-
onSignOrderError: (error) => {
|
|
661
|
-
toast.error("Failed to sign order");
|
|
662
|
-
},
|
|
663
|
-
|
|
664
|
-
// Order Created
|
|
665
|
-
onOrderCreated: (order) => {
|
|
666
|
-
toast.success(`Order created: ${order.srcTokenSymbol} → ${order.dstTokenSymbol}`);
|
|
667
|
-
},
|
|
668
|
-
|
|
669
|
-
// Order Filled
|
|
670
|
-
onOrderFilled: (order) => {
|
|
671
|
-
toast.success("Order filled!");
|
|
672
|
-
},
|
|
673
|
-
|
|
674
|
-
// Order Progress
|
|
675
|
-
onOrdersProgressUpdate: (orders) => {
|
|
676
|
-
// Orders updated (useful for live progress)
|
|
677
|
-
},
|
|
678
|
-
|
|
679
|
-
// Errors
|
|
680
|
-
onSubmitOrderFailed: ({ message, code }) => {
|
|
681
|
-
toast.error(`Failed: ${code}`);
|
|
682
|
-
},
|
|
683
|
-
onSubmitOrderRejected: () => {
|
|
684
|
-
toast.error("Order rejected");
|
|
685
|
-
},
|
|
686
|
-
|
|
687
|
-
// Cancel
|
|
688
|
-
onCancelOrderRequest: (orders) => {
|
|
689
|
-
toast.loading("Cancelling order...");
|
|
690
|
-
},
|
|
691
|
-
onCancelOrderSuccess: ({ orders, txHash, explorerUrl }) => {
|
|
692
|
-
toast.success("Order cancelled");
|
|
693
|
-
},
|
|
694
|
-
onCancelOrderFailed: (error) => {
|
|
695
|
-
toast.error("Cancel failed");
|
|
696
|
-
},
|
|
697
|
-
|
|
698
|
-
// Clipboard
|
|
699
|
-
onCopy: () => {
|
|
700
|
-
toast.success("Copied to clipboard");
|
|
701
|
-
},
|
|
702
|
-
};
|
|
703
|
-
};
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
---
|
|
707
|
-
|
|
708
|
-
## Pre-built Components
|
|
709
|
-
|
|
710
|
-
```tsx
|
|
711
|
-
import { Components } from "@orbs-network/spot-react";
|
|
712
|
-
|
|
713
|
-
// Submit order panel with steps display (wrap → approve → sign)
|
|
714
|
-
<Components.SubmitOrderPanel
|
|
715
|
-
reviewDetails={
|
|
716
|
-
<div>
|
|
717
|
-
<Switch label="Accept Disclaimer" />
|
|
718
|
-
<Button onClick={onSubmit}>Create Order</Button>
|
|
719
|
-
</div>
|
|
720
|
-
}
|
|
721
|
-
/>
|
|
722
|
-
|
|
723
|
-
// Orders list with details view
|
|
724
|
-
<Components.Orders />
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
---
|
|
728
|
-
|
|
729
|
-
## Market Reference Price
|
|
730
|
-
|
|
731
|
-
**CRITICAL**: The `marketReferencePrice.value` should be the expected output amount (in wei) for the current input amount, NOT a 1:1 price ratio.
|
|
732
|
-
|
|
733
|
-
### Option 1: Use a DEX aggregator quote
|
|
734
|
-
```tsx
|
|
735
|
-
const useMarketReferencePrice = () => {
|
|
736
|
-
const { trade, isLoading } = useDexQuote(srcToken, dstToken, inputAmountWei);
|
|
737
|
-
|
|
738
|
-
return {
|
|
739
|
-
value: trade?.outAmount, // Expected output in wei
|
|
740
|
-
isLoading,
|
|
741
|
-
noLiquidity: !trade && !isLoading,
|
|
742
|
-
};
|
|
743
|
-
};
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
### Option 2: Calculate from USD prices (synthetic)
|
|
747
|
-
```tsx
|
|
748
|
-
const useMarketReferencePrice = () => {
|
|
749
|
-
const srcUSD = useUSDPrice(srcToken?.address); // Price for 1 token
|
|
750
|
-
const dstUSD = useUSDPrice(dstToken?.address);
|
|
751
|
-
const inputAmountUI = toAmountUI(inputAmountWei, srcToken?.decimals);
|
|
752
|
-
|
|
753
|
-
const outputAmountWei = useMemo(() => {
|
|
754
|
-
if (!srcUSD || !dstUSD || !inputAmountUI) return undefined;
|
|
755
|
-
const outputUI = (srcUSD / dstUSD) * parseFloat(inputAmountUI);
|
|
756
|
-
return toAmountWei(outputUI.toString(), dstToken?.decimals);
|
|
757
|
-
}, [srcUSD, dstUSD, inputAmountUI]);
|
|
758
|
-
|
|
759
|
-
return {
|
|
760
|
-
value: outputAmountWei,
|
|
761
|
-
isLoading: !srcUSD || !dstUSD,
|
|
762
|
-
noLiquidity: false,
|
|
763
|
-
};
|
|
764
|
-
};
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
---
|
|
768
|
-
|
|
769
|
-
## Module-Specific UI
|
|
770
|
-
|
|
771
|
-
```tsx
|
|
772
|
-
const ModuleInputs = ({ module }: { module: Module }) => {
|
|
773
|
-
if (module === Module.TWAP) {
|
|
774
|
-
return (
|
|
775
|
-
<>
|
|
776
|
-
<TradesPanel /> {/* Number of trades */}
|
|
777
|
-
<FillDelayPanel /> {/* Delay between trades */}
|
|
778
|
-
</>
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// LIMIT, STOP_LOSS, TAKE_PROFIT
|
|
783
|
-
return <DurationPanel />; {/* Order expiration */}
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
const PricePanels = ({ module }: { module: Module }) => {
|
|
787
|
-
return (
|
|
788
|
-
<>
|
|
789
|
-
{/* Show trigger price for conditional orders */}
|
|
790
|
-
{(module === Module.STOP_LOSS || module === Module.TAKE_PROFIT) && (
|
|
791
|
-
<TriggerPricePanel />
|
|
792
|
-
)}
|
|
793
|
-
|
|
794
|
-
{/* Limit price (optional for TWAP, required for LIMIT) */}
|
|
795
|
-
<LimitPricePanel />
|
|
796
|
-
</>
|
|
797
|
-
);
|
|
798
|
-
};
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
---
|
|
802
|
-
|
|
803
|
-
## Integration Checklist
|
|
804
|
-
|
|
805
|
-
1. [ ] Install package and peer dependencies
|
|
806
|
-
2. [ ] Create Button, Tooltip, TokenLogo, Spinner components
|
|
807
|
-
3. [ ] Set up wallet connection (wagmi recommended)
|
|
808
|
-
4. [ ] Implement token fetching/selection
|
|
809
|
-
5. [ ] Implement balance fetching (wei strings)
|
|
810
|
-
6. [ ] Implement USD price fetching (price for 1 token)
|
|
811
|
-
7. [ ] Implement market reference price (output amount for input)
|
|
812
|
-
8. [ ] Wrap trading UI with SpotProvider
|
|
813
|
-
9. [ ] Build token panels with `useSrcTokenPanel`/`useDstTokenPanel`
|
|
814
|
-
10. [ ] Build order config UI based on module type
|
|
815
|
-
11. [ ] Build price panels with `useLimitPricePanel`/`useTriggerPricePanel`
|
|
816
|
-
12. [ ] Add validation display with `useInputErrors`
|
|
817
|
-
13. [ ] Build submission flow with `useSubmitOrderPanel`
|
|
818
|
-
14. [ ] Build orders history with `useOrderHistoryPanel`
|
|
819
|
-
15. [ ] Set up callbacks for toast notifications
|
|
820
|
-
16. [ ] Import styles: `import "@orbs-network/spot-react/styles.css"`
|
|
821
|
-
|
|
822
|
-
---
|
|
823
|
-
|
|
824
|
-
## Styles
|
|
825
|
-
|
|
826
|
-
Import the default stylesheet:
|
|
827
|
-
```tsx
|
|
828
|
-
import "@orbs-network/spot-react/styles.css";
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
Or customize with your own CSS targeting the component classes.
|