Swap tokens
This commit is contained in:
@@ -14,7 +14,21 @@ const Modal: React.FC<IModalProps> = ({
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
{showModal ? (
|
||||
<form onSubmit={onSubmit}>
|
||||
onSubmit ? (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="modal modal-bottom sm:modal-middle modal-open">
|
||||
<div className="modal-box !max-w-4xl !w-11/12">
|
||||
<ModalHeader
|
||||
titleHeader={titleHeader}
|
||||
onClose={onClose}
|
||||
onSubmit={onSubmit}
|
||||
showModal={showModal}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="modal modal-bottom sm:modal-middle modal-open">
|
||||
<div className="modal-box !max-w-4xl !w-11/12">
|
||||
<ModalHeader
|
||||
@@ -26,7 +40,7 @@ const Modal: React.FC<IModalProps> = ({
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -247,6 +247,49 @@ export class AccountClient extends AuthorizedApiBase {
|
||||
}
|
||||
return Promise.resolve<GmxClaimableSummary>(null as any);
|
||||
}
|
||||
|
||||
account_SwapGmxTokens(name: string, request: SwapTokensRequest): Promise<SwapInfos> {
|
||||
let url_ = this.baseUrl + "/Account/{name}/gmx-swap";
|
||||
if (name === undefined || name === null)
|
||||
throw new Error("The parameter 'name' must be defined.");
|
||||
url_ = url_.replace("{name}", encodeURIComponent("" + name));
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(request);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processAccount_SwapGmxTokens(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processAccount_SwapGmxTokens(response: Response): Promise<SwapInfos> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as SwapInfos;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
console.log(_responseText)
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<SwapInfos>(null as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class BacktestClient extends AuthorizedApiBase {
|
||||
@@ -2871,68 +2914,22 @@ export interface RebateStatsData {
|
||||
discountFactor?: number;
|
||||
}
|
||||
|
||||
export interface Backtest {
|
||||
id: string;
|
||||
finalPnl: number;
|
||||
winRate: number;
|
||||
growthPercentage: number;
|
||||
hodlPercentage: number;
|
||||
config: TradingBotConfig;
|
||||
positions: Position[];
|
||||
signals: Signal[];
|
||||
candles: Candle[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
statistics: PerformanceMetrics;
|
||||
fees: number;
|
||||
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
||||
optimizedMoneyManagement: MoneyManagement;
|
||||
user: User;
|
||||
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
||||
score: number;
|
||||
export interface SwapInfos {
|
||||
success?: boolean;
|
||||
hash?: string | null;
|
||||
message?: string | null;
|
||||
error?: string | null;
|
||||
errorType?: string | null;
|
||||
suggestion?: string | null;
|
||||
}
|
||||
|
||||
export interface TradingBotConfig {
|
||||
accountName: string;
|
||||
moneyManagement: MoneyManagement;
|
||||
ticker: Ticker;
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
isForBacktest: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
flipPosition: boolean;
|
||||
name: string;
|
||||
riskManagement?: RiskManagement | null;
|
||||
scenario?: Scenario | null;
|
||||
scenarioName?: string | null;
|
||||
maxPositionTimeHours?: number | null;
|
||||
closeEarlyWhenProfitable?: boolean;
|
||||
flipOnlyWhenInProfit: boolean;
|
||||
useSynthApi?: boolean;
|
||||
useForPositionSizing?: boolean;
|
||||
useForSignalFiltering?: boolean;
|
||||
useForDynamicStopLoss?: boolean;
|
||||
}
|
||||
|
||||
export interface MoneyManagement {
|
||||
name: string;
|
||||
timeframe: Timeframe;
|
||||
stopLoss: number;
|
||||
takeProfit: number;
|
||||
leverage: number;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export enum Timeframe {
|
||||
FiveMinutes = "FiveMinutes",
|
||||
FifteenMinutes = "FifteenMinutes",
|
||||
ThirtyMinutes = "ThirtyMinutes",
|
||||
OneHour = "OneHour",
|
||||
FourHour = "FourHour",
|
||||
OneDay = "OneDay",
|
||||
OneMinute = "OneMinute",
|
||||
export interface SwapTokensRequest {
|
||||
fromTicker: Ticker;
|
||||
toTicker: Ticker;
|
||||
amount: number;
|
||||
orderType?: string | null;
|
||||
triggerRatio?: number | null;
|
||||
allowedSlippage?: number;
|
||||
}
|
||||
|
||||
export enum Ticker {
|
||||
@@ -3044,6 +3041,70 @@ export enum Ticker {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export interface Backtest {
|
||||
id: string;
|
||||
finalPnl: number;
|
||||
winRate: number;
|
||||
growthPercentage: number;
|
||||
hodlPercentage: number;
|
||||
config: TradingBotConfig;
|
||||
positions: Position[];
|
||||
signals: Signal[];
|
||||
candles: Candle[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
statistics: PerformanceMetrics;
|
||||
fees: number;
|
||||
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
||||
optimizedMoneyManagement: MoneyManagement;
|
||||
user: User;
|
||||
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface TradingBotConfig {
|
||||
accountName: string;
|
||||
moneyManagement: MoneyManagement;
|
||||
ticker: Ticker;
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
isForBacktest: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
flipPosition: boolean;
|
||||
name: string;
|
||||
riskManagement?: RiskManagement | null;
|
||||
scenario?: Scenario | null;
|
||||
scenarioName?: string | null;
|
||||
maxPositionTimeHours?: number | null;
|
||||
closeEarlyWhenProfitable?: boolean;
|
||||
flipOnlyWhenInProfit: boolean;
|
||||
useSynthApi?: boolean;
|
||||
useForPositionSizing?: boolean;
|
||||
useForSignalFiltering?: boolean;
|
||||
useForDynamicStopLoss?: boolean;
|
||||
}
|
||||
|
||||
export interface MoneyManagement {
|
||||
name: string;
|
||||
timeframe: Timeframe;
|
||||
stopLoss: number;
|
||||
takeProfit: number;
|
||||
leverage: number;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export enum Timeframe {
|
||||
FiveMinutes = "FiveMinutes",
|
||||
FifteenMinutes = "FifteenMinutes",
|
||||
ThirtyMinutes = "ThirtyMinutes",
|
||||
OneHour = "OneHour",
|
||||
FourHour = "FourHour",
|
||||
OneDay = "OneDay",
|
||||
OneMinute = "OneMinute",
|
||||
}
|
||||
|
||||
export interface RiskManagement {
|
||||
adverseProbabilityThreshold: number;
|
||||
favorableProbabilityThreshold: number;
|
||||
|
||||
@@ -83,68 +83,22 @@ export interface RebateStatsData {
|
||||
discountFactor?: number;
|
||||
}
|
||||
|
||||
export interface Backtest {
|
||||
id: string;
|
||||
finalPnl: number;
|
||||
winRate: number;
|
||||
growthPercentage: number;
|
||||
hodlPercentage: number;
|
||||
config: TradingBotConfig;
|
||||
positions: Position[];
|
||||
signals: Signal[];
|
||||
candles: Candle[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
statistics: PerformanceMetrics;
|
||||
fees: number;
|
||||
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
||||
optimizedMoneyManagement: MoneyManagement;
|
||||
user: User;
|
||||
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
||||
score: number;
|
||||
export interface SwapInfos {
|
||||
success?: boolean;
|
||||
hash?: string | null;
|
||||
message?: string | null;
|
||||
error?: string | null;
|
||||
errorType?: string | null;
|
||||
suggestion?: string | null;
|
||||
}
|
||||
|
||||
export interface TradingBotConfig {
|
||||
accountName: string;
|
||||
moneyManagement: MoneyManagement;
|
||||
ticker: Ticker;
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
isForBacktest: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
flipPosition: boolean;
|
||||
name: string;
|
||||
riskManagement?: RiskManagement | null;
|
||||
scenario?: Scenario | null;
|
||||
scenarioName?: string | null;
|
||||
maxPositionTimeHours?: number | null;
|
||||
closeEarlyWhenProfitable?: boolean;
|
||||
flipOnlyWhenInProfit: boolean;
|
||||
useSynthApi?: boolean;
|
||||
useForPositionSizing?: boolean;
|
||||
useForSignalFiltering?: boolean;
|
||||
useForDynamicStopLoss?: boolean;
|
||||
}
|
||||
|
||||
export interface MoneyManagement {
|
||||
name: string;
|
||||
timeframe: Timeframe;
|
||||
stopLoss: number;
|
||||
takeProfit: number;
|
||||
leverage: number;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export enum Timeframe {
|
||||
FiveMinutes = "FiveMinutes",
|
||||
FifteenMinutes = "FifteenMinutes",
|
||||
ThirtyMinutes = "ThirtyMinutes",
|
||||
OneHour = "OneHour",
|
||||
FourHour = "FourHour",
|
||||
OneDay = "OneDay",
|
||||
OneMinute = "OneMinute",
|
||||
export interface SwapTokensRequest {
|
||||
fromTicker: Ticker;
|
||||
toTicker: Ticker;
|
||||
amount: number;
|
||||
orderType?: string | null;
|
||||
triggerRatio?: number | null;
|
||||
allowedSlippage?: number;
|
||||
}
|
||||
|
||||
export enum Ticker {
|
||||
@@ -256,6 +210,70 @@ export enum Ticker {
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export interface Backtest {
|
||||
id: string;
|
||||
finalPnl: number;
|
||||
winRate: number;
|
||||
growthPercentage: number;
|
||||
hodlPercentage: number;
|
||||
config: TradingBotConfig;
|
||||
positions: Position[];
|
||||
signals: Signal[];
|
||||
candles: Candle[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
statistics: PerformanceMetrics;
|
||||
fees: number;
|
||||
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
|
||||
optimizedMoneyManagement: MoneyManagement;
|
||||
user: User;
|
||||
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface TradingBotConfig {
|
||||
accountName: string;
|
||||
moneyManagement: MoneyManagement;
|
||||
ticker: Ticker;
|
||||
timeframe: Timeframe;
|
||||
isForWatchingOnly: boolean;
|
||||
botTradingBalance: number;
|
||||
isForBacktest: boolean;
|
||||
cooldownPeriod: number;
|
||||
maxLossStreak: number;
|
||||
flipPosition: boolean;
|
||||
name: string;
|
||||
riskManagement?: RiskManagement | null;
|
||||
scenario?: Scenario | null;
|
||||
scenarioName?: string | null;
|
||||
maxPositionTimeHours?: number | null;
|
||||
closeEarlyWhenProfitable?: boolean;
|
||||
flipOnlyWhenInProfit: boolean;
|
||||
useSynthApi?: boolean;
|
||||
useForPositionSizing?: boolean;
|
||||
useForSignalFiltering?: boolean;
|
||||
useForDynamicStopLoss?: boolean;
|
||||
}
|
||||
|
||||
export interface MoneyManagement {
|
||||
name: string;
|
||||
timeframe: Timeframe;
|
||||
stopLoss: number;
|
||||
takeProfit: number;
|
||||
leverage: number;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
export enum Timeframe {
|
||||
FiveMinutes = "FiveMinutes",
|
||||
FifteenMinutes = "FifteenMinutes",
|
||||
ThirtyMinutes = "ThirtyMinutes",
|
||||
OneHour = "OneHour",
|
||||
FourHour = "FourHour",
|
||||
OneDay = "OneDay",
|
||||
OneMinute = "OneMinute",
|
||||
}
|
||||
|
||||
export interface RiskManagement {
|
||||
adverseProbabilityThreshold: number;
|
||||
favorableProbabilityThreshold: number;
|
||||
|
||||
@@ -299,6 +299,7 @@ export type ICardPositionFlipped = {
|
||||
export type IAccountRowDetail = {
|
||||
balances: Balance[]
|
||||
showTotal?: boolean
|
||||
account?: Account
|
||||
}
|
||||
|
||||
export type IGridTile = {
|
||||
|
||||
42
src/Managing.WebApp/src/hooks/useApiError.ts
Normal file
42
src/Managing.WebApp/src/hooks/useApiError.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {extractErrorMessage} from '../utils/apiErrorHandler'
|
||||
|
||||
interface UseApiErrorReturn {
|
||||
error: string | null
|
||||
setError: (error: string | null) => void
|
||||
clearError: () => void
|
||||
handleError: (err: unknown) => string
|
||||
handleApiErrorWithToast: (err: unknown, toast: any) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for handling API errors consistently across components
|
||||
* @returns Error handling utilities
|
||||
*/
|
||||
export function useApiError(): UseApiErrorReturn {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
const handleError = useCallback((err: unknown): string => {
|
||||
const errorMessage = extractErrorMessage(err)
|
||||
setError(errorMessage)
|
||||
return errorMessage
|
||||
}, [])
|
||||
|
||||
const handleApiErrorWithToast = useCallback((err: unknown, toast: any) => {
|
||||
const errorMessage = extractErrorMessage(err)
|
||||
setError(errorMessage)
|
||||
toast.update('error', `Error: ${errorMessage}`)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
error,
|
||||
setError,
|
||||
clearError,
|
||||
handleError,
|
||||
handleApiErrorWithToast
|
||||
}
|
||||
}
|
||||
263
src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx
Normal file
263
src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import React, {useState} from 'react'
|
||||
import type {SubmitHandler} from 'react-hook-form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {Account, AccountClient, Ticker,} from '../../../generated/ManagingApi'
|
||||
import Modal from '../../../components/mollecules/Modal/Modal'
|
||||
import useApiUrlStore from '../../../app/store/apiStore'
|
||||
import {FormInput, Toast} from '../../../components/mollecules'
|
||||
import {useApiError} from '../../../hooks/useApiError'
|
||||
|
||||
interface SwapModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
account: Account
|
||||
fromTicker: Ticker
|
||||
availableAmount: number
|
||||
}
|
||||
|
||||
interface SwapFormInput {
|
||||
fromTicker: Ticker
|
||||
toTicker: Ticker
|
||||
amount: number
|
||||
orderType: string
|
||||
triggerRatio?: number
|
||||
allowedSlippage: number
|
||||
}
|
||||
|
||||
const SwapModal: React.FC<SwapModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
fromTicker,
|
||||
availableAmount,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { error, setError, handleApiErrorWithToast } = useApiError()
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const client = new AccountClient({}, apiUrl)
|
||||
const [selectedToTicker, setSelectedToTicker] = useState<Ticker>(Ticker.USDC)
|
||||
const [selectedOrderType, setSelectedOrderType] = useState<string>('market')
|
||||
|
||||
const { register, handleSubmit, watch, setValue } = useForm<SwapFormInput>({
|
||||
defaultValues: {
|
||||
fromTicker: fromTicker,
|
||||
toTicker: Ticker.USDC,
|
||||
amount: availableAmount * 0.1, // Start with 10% of available amount
|
||||
orderType: 'market',
|
||||
allowedSlippage: 0.5,
|
||||
}
|
||||
})
|
||||
|
||||
const watchedAmount = watch('amount')
|
||||
|
||||
function setSelectedToTickerEvent(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setSelectedToTicker(e.target.value as Ticker)
|
||||
}
|
||||
|
||||
function setSelectedOrderTypeEvent(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setSelectedOrderType(e.target.value)
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<SwapFormInput> = async (form) => {
|
||||
const t = new Toast(`Swapping ${form.amount} ${form.fromTicker} to ${form.toTicker} on ${account.name}`)
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await client.account_SwapGmxTokens(
|
||||
account.name,
|
||||
{
|
||||
fromTicker: form.fromTicker,
|
||||
toTicker: form.toTicker,
|
||||
amount: form.amount,
|
||||
orderType: form.orderType,
|
||||
triggerRatio: form.triggerRatio,
|
||||
allowedSlippage: form.allowedSlippage,
|
||||
}
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
t.update('success', `Swap successful! Hash: ${result.hash}`)
|
||||
onClose()
|
||||
} else {
|
||||
console.log(result)
|
||||
const errorMessage = result.error || result.message || 'Swap failed'
|
||||
setError(errorMessage)
|
||||
t.update('error', `Swap failed: ${errorMessage}`)
|
||||
}
|
||||
} catch (err) {
|
||||
handleApiErrorWithToast(err, t)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
handleSubmit(onSubmit)(e)
|
||||
}
|
||||
|
||||
const modalContent = (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
<p>Processing swap...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert alert-error mb-4">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<p className="mb-2">
|
||||
<strong>Account:</strong> {account.name}
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
<strong>From:</strong> {fromTicker}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="space-y-4 mb-4">
|
||||
<FormInput label="To Ticker" htmlFor="toTicker">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('toTicker', {
|
||||
onChange(e) {
|
||||
setSelectedToTickerEvent(e)
|
||||
},
|
||||
value: selectedToTicker,
|
||||
})}
|
||||
>
|
||||
{Object.values(Ticker)
|
||||
.filter(ticker => ticker !== fromTicker) // Exclude the from ticker
|
||||
.map((ticker) => (
|
||||
<option key={ticker} value={ticker}>
|
||||
{ticker}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Amount" htmlFor="amount">
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="Enter amount to swap"
|
||||
className="input input-bordered w-full mb-2"
|
||||
{...register('amount', {
|
||||
valueAsNumber: true,
|
||||
min: 0.0001,
|
||||
max: availableAmount,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={availableAmount}
|
||||
step={availableAmount / 100}
|
||||
className="range range-primary w-full"
|
||||
value={watchedAmount || 0}
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
setValue('amount', value)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="text-center text-xs text-gray-500 mt-1">
|
||||
{watchedAmount && availableAmount > 0 ? (
|
||||
<span>{((watchedAmount / availableAmount) * 100).toFixed(1)}% of available balance</span>
|
||||
) : (
|
||||
<span>0% of available balance</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Order Type" htmlFor="orderType">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
{...register('orderType', {
|
||||
onChange(e) {
|
||||
setSelectedOrderTypeEvent(e)
|
||||
},
|
||||
value: selectedOrderType,
|
||||
})}
|
||||
>
|
||||
<option value="market">Market</option>
|
||||
<option value="limit">Limit</option>
|
||||
<option value="stop">Stop</option>
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
<FormInput label="Allowed Slippage (%)" htmlFor="allowedSlippage">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.5"
|
||||
className="input input-bordered w-full"
|
||||
{...register('allowedSlippage', {
|
||||
valueAsNumber: true,
|
||||
min: 0.1,
|
||||
max: 10,
|
||||
value: 0.5
|
||||
})}
|
||||
/>
|
||||
</FormInput>
|
||||
|
||||
{selectedOrderType === 'limit' && (
|
||||
<FormInput label="Trigger Ratio" htmlFor="triggerRatio">
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="Enter trigger ratio"
|
||||
className="input input-bordered w-full"
|
||||
{...register('triggerRatio', { valueAsNumber: true })}
|
||||
/>
|
||||
</FormInput>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full mt-2"
|
||||
disabled={isLoading || !watchedAmount || watchedAmount <= 0}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="loading loading-spinner"></span>
|
||||
) : (
|
||||
`Swap ${watchedAmount || 0} ${fromTicker} to ${selectedToTicker}`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="alert alert-info">
|
||||
<p className="text-sm">
|
||||
<strong>Note:</strong> Ensure account has sufficient balance for the swap.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Swap Tokens on GMX"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwapModal
|
||||
@@ -1,53 +1,118 @@
|
||||
import React from 'react'
|
||||
import React, {useState} from 'react'
|
||||
import {FiRefreshCw} from 'react-icons/fi'
|
||||
|
||||
import { SelectColumnFilter, Table } from '../../../components/mollecules'
|
||||
import { IAccountRowDetail } from '../../../global/type'
|
||||
import {SelectColumnFilter, Table} from '../../../components/mollecules'
|
||||
import type {IAccountRowDetail} from '../../../global/type.tsx'
|
||||
import type {Account, Balance} from '../../../generated/ManagingApi'
|
||||
import {Ticker} from '../../../generated/ManagingApi'
|
||||
import SwapModal from './SwapModal'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Chain',
|
||||
accessor: 'chain.name',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Assets',
|
||||
accessor: 'tokenName',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => (
|
||||
<>
|
||||
<div className="tooltip" data-tip={cell.row.tokenName}>
|
||||
{cell.row.values.amount.toFixed(4)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
Header: 'Quantity',
|
||||
accessor: 'amount',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => <>{cell.row.values.value.toFixed(2)} $</>,
|
||||
Header: 'USD',
|
||||
accessor: 'value',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => <> {cell.row.values.price} $</>,
|
||||
Header: 'Price',
|
||||
accessor: 'price',
|
||||
disableFilters: true,
|
||||
},
|
||||
]
|
||||
interface IAccountRowDetailProps extends IAccountRowDetail {
|
||||
account: Account
|
||||
}
|
||||
|
||||
const AccountRowDetails: React.FC<IAccountRowDetail> = ({
|
||||
const AccountRowDetails: React.FC<IAccountRowDetailProps> = ({
|
||||
balances,
|
||||
showTotal,
|
||||
account,
|
||||
}) => {
|
||||
const [swapModalState, setSwapModalState] = useState<{
|
||||
isOpen: boolean
|
||||
fromTicker: Ticker | null
|
||||
availableAmount: number
|
||||
}>({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
|
||||
const handleSwapClick = (balance: Balance) => {
|
||||
if (balance.tokenName && balance.amount) {
|
||||
// Convert tokenName to Ticker enum
|
||||
const ticker = balance.tokenName.toUpperCase() as Ticker
|
||||
if (Object.values(Ticker).includes(ticker)) {
|
||||
setSwapModalState({
|
||||
isOpen: true,
|
||||
fromTicker: ticker,
|
||||
availableAmount: balance.amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const closeSwapModal = () => {
|
||||
setSwapModalState({
|
||||
isOpen: false,
|
||||
fromTicker: null,
|
||||
availableAmount: 0,
|
||||
})
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Header: 'Chain',
|
||||
accessor: 'chain.name',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Filter: SelectColumnFilter,
|
||||
Header: 'Assets',
|
||||
accessor: 'tokenName',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => (
|
||||
<>
|
||||
<div className="tooltip" data-tip={cell.row.tokenName}>
|
||||
{cell.row.values.amount.toFixed(4)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
Header: 'Quantity',
|
||||
accessor: 'amount',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => <>{cell.row.values.value.toFixed(2)} $</>,
|
||||
Header: 'USD',
|
||||
accessor: 'value',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => <> {cell.row.values.price} $</>,
|
||||
Header: 'Price',
|
||||
accessor: 'price',
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ cell }: any) => {
|
||||
const balance = cell.row.original as Balance
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
{balance.tokenName && balance.amount && balance.amount > 0 && Object.values(Ticker).includes(balance.tokenName.toUpperCase() as Ticker) && (
|
||||
<button
|
||||
className="btn btn-xs btn-outline btn-info"
|
||||
onClick={() => handleSwapClick(balance)}
|
||||
title={`Swap ${balance.tokenName}`}
|
||||
>
|
||||
<FiRefreshCw className="h-3 w-3" />
|
||||
<span className="ml-1">Swap</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
Header: 'Actions',
|
||||
accessor: 'actions',
|
||||
disableFilters: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
@@ -56,6 +121,16 @@ const AccountRowDetails: React.FC<IAccountRowDetail> = ({
|
||||
showTotal={showTotal}
|
||||
showPagination={false}
|
||||
/>
|
||||
|
||||
{swapModalState.isOpen && swapModalState.fromTicker && (
|
||||
<SwapModal
|
||||
isOpen={swapModalState.isOpen}
|
||||
onClose={closeSwapModal}
|
||||
account={account}
|
||||
fromTicker={swapModalState.fromTicker}
|
||||
availableAmount={swapModalState.availableAmount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ const AccountTable: React.FC<IAccountList> = ({ list, isFetching }) => {
|
||||
<AccountRowDetails
|
||||
balances={balances}
|
||||
showTotal={true}
|
||||
account={row.original}
|
||||
></AccountRowDetails>
|
||||
) : (
|
||||
<div>No balances</div>
|
||||
|
||||
161
src/Managing.WebApp/src/utils/apiErrorHandler.md
Normal file
161
src/Managing.WebApp/src/utils/apiErrorHandler.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# API Error Handling Utilities
|
||||
|
||||
This module provides utilities for handling API errors consistently across the application.
|
||||
|
||||
## Functions
|
||||
|
||||
### `extractErrorMessage(err: unknown): string`
|
||||
|
||||
Extracts meaningful error messages from API exceptions and other errors.
|
||||
|
||||
**Parameters:**
|
||||
- `err` - The error object caught from API calls
|
||||
|
||||
**Returns:**
|
||||
- A user-friendly error message string
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { extractErrorMessage } from '../utils/apiErrorHandler'
|
||||
|
||||
try {
|
||||
const result = await apiCall()
|
||||
} catch (err) {
|
||||
const errorMessage = extractErrorMessage(err)
|
||||
console.log(errorMessage) // "Insufficient balance for swap"
|
||||
}
|
||||
```
|
||||
|
||||
### `handleApiError(err: unknown)`
|
||||
|
||||
Handles API errors and returns a standardized error object with additional context.
|
||||
|
||||
**Parameters:**
|
||||
- `err` - The error object caught from API calls
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
message: string
|
||||
isApiException: boolean
|
||||
status?: number
|
||||
response?: string
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { handleApiError } from '../utils/apiErrorHandler'
|
||||
|
||||
try {
|
||||
const result = await apiCall()
|
||||
} catch (err) {
|
||||
const errorInfo = handleApiError(err)
|
||||
console.log(errorInfo.message) // "Insufficient balance for swap"
|
||||
console.log(errorInfo.status) // 400
|
||||
console.log(errorInfo.isApiException) // true
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Hook
|
||||
|
||||
### `useApiError()`
|
||||
|
||||
A React hook that provides error handling utilities for API calls.
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
error: string | null
|
||||
setError: (error: string | null) => void
|
||||
clearError: () => void
|
||||
handleError: (err: unknown) => string
|
||||
handleApiErrorWithToast: (err: unknown, toast: any) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { useApiError } from '../hooks/useApiError'
|
||||
|
||||
function MyComponent() {
|
||||
const { error, setError, clearError, handleError, handleApiErrorWithToast } = useApiError()
|
||||
|
||||
const handleApiCall = async () => {
|
||||
try {
|
||||
const result = await apiCall()
|
||||
clearError() // Clear any previous errors
|
||||
} catch (err) {
|
||||
// Option 1: Simple error handling
|
||||
const errorMessage = handleError(err)
|
||||
|
||||
// Option 2: Error handling with toast
|
||||
handleApiErrorWithToast(err, toast)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
<button onClick={handleApiCall}>Make API Call</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Types Handled
|
||||
|
||||
1. **ApiException** - Generated client exceptions with server response details
|
||||
2. **Error** - Standard JavaScript Error objects
|
||||
3. **Unknown** - Any other error types
|
||||
|
||||
## Response Parsing
|
||||
|
||||
The utilities attempt to parse JSON responses in this order:
|
||||
1. `message` - Standard error message field
|
||||
2. `error` - Alternative error field
|
||||
3. `detail` - ASP.NET Core validation error field
|
||||
4. Raw response text if JSON parsing fails
|
||||
5. Exception message as final fallback
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
```typescript
|
||||
try {
|
||||
const result = await client.someApiCall()
|
||||
} catch (err) {
|
||||
const errorMessage = extractErrorMessage(err)
|
||||
setError(errorMessage)
|
||||
}
|
||||
```
|
||||
|
||||
### With Toast Notifications
|
||||
```typescript
|
||||
const { handleApiErrorWithToast } = useApiError()
|
||||
|
||||
try {
|
||||
const result = await client.someApiCall()
|
||||
toast.update('success', 'Operation successful!')
|
||||
} catch (err) {
|
||||
handleApiErrorWithToast(err, toast)
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Error Handling
|
||||
```typescript
|
||||
const { handleError } = useApiError()
|
||||
|
||||
try {
|
||||
const result = await client.someApiCall()
|
||||
} catch (err) {
|
||||
const errorMessage = handleError(err)
|
||||
|
||||
// Custom logic based on error type
|
||||
if (errorMessage.includes('insufficient')) {
|
||||
showBalanceWarning()
|
||||
} else if (errorMessage.includes('network')) {
|
||||
showRetryButton()
|
||||
}
|
||||
}
|
||||
```
|
||||
50
src/Managing.WebApp/src/utils/apiErrorHandler.ts
Normal file
50
src/Managing.WebApp/src/utils/apiErrorHandler.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Extracts meaningful error messages from API exceptions and other errors
|
||||
* @param err - The error object caught from API calls
|
||||
* @returns A user-friendly error message
|
||||
*/
|
||||
export function extractErrorMessage(err: unknown): string {
|
||||
// Handle ApiException specifically to extract the actual error message
|
||||
if (err && typeof err === 'object' && 'isApiException' in err) {
|
||||
const apiException = err as any
|
||||
try {
|
||||
// Try to parse the response as JSON to get the actual error message
|
||||
const responseData = JSON.parse(apiException.response)
|
||||
return responseData.message || responseData.error || responseData.detail || apiException.message
|
||||
} catch {
|
||||
// If parsing fails, use the response text directly
|
||||
return apiException.response || apiException.message
|
||||
}
|
||||
} else if (err instanceof Error) {
|
||||
return err.message
|
||||
}
|
||||
|
||||
return 'An unknown error occurred'
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles API errors and returns a standardized error object
|
||||
* @param err - The error object caught from API calls
|
||||
* @returns An object containing the error message and additional context
|
||||
*/
|
||||
export function handleApiError(err: unknown): {
|
||||
message: string
|
||||
isApiException: boolean
|
||||
status?: number
|
||||
response?: string
|
||||
} {
|
||||
if (err && typeof err === 'object' && 'isApiException' in err) {
|
||||
const apiException = err as any
|
||||
return {
|
||||
message: extractErrorMessage(err),
|
||||
isApiException: true,
|
||||
status: apiException.status,
|
||||
response: apiException.response
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: extractErrorMessage(err),
|
||||
isApiException: false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user