From 46b43bce1aaa397fdec911e604a137ecddb73286 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 6 Jul 2025 15:50:50 +0700 Subject: [PATCH] Fix swap btc + fix bot stoping --- .../Models/Requests/SwapTokensRequest.cs | 4 +- src/Managing.Application/Bots/TradingBot.cs | 2 +- .../pages/settingsPage/account/SwapModal.tsx | 518 ++++++++++-------- 3 files changed, 291 insertions(+), 233 deletions(-) diff --git a/src/Managing.Api/Models/Requests/SwapTokensRequest.cs b/src/Managing.Api/Models/Requests/SwapTokensRequest.cs index 20036f7..169ec76 100644 --- a/src/Managing.Api/Models/Requests/SwapTokensRequest.cs +++ b/src/Managing.Api/Models/Requests/SwapTokensRequest.cs @@ -24,7 +24,7 @@ public class SwapTokensRequest /// The amount to swap /// [Required] - [Range(0.000001, double.MaxValue, ErrorMessage = "Amount must be greater than 0")] + [Range(0.0000000000001, double.MaxValue, ErrorMessage = "Amount must be greater than 0")] public double Amount { get; set; } /// @@ -42,4 +42,4 @@ public class SwapTokensRequest /// [Range(0, 100, ErrorMessage = "Allowed slippage must be between 0 and 100")] public double AllowedSlippage { get; set; } = 0.5; -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index 6b46807..facaf4c 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -224,7 +224,7 @@ public class TradingBot : Bot, ITradingBot { // Check broker balance before running var balance = await ExchangeService.GetBalance(Account, false); - if (balance < Constants.GMX.Config.MinimumPositionAmount) + if (balance < Constants.GMX.Config.MinimumPositionAmount && Positions.All(p => p.IsFinished())) { await LogWarning( $"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier} and saving backup."); diff --git a/src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx b/src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx index 97ad9a0..3842d9e 100644 --- a/src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx +++ b/src/Managing.WebApp/src/pages/settingsPage/account/SwapModal.tsx @@ -8,256 +8,314 @@ import {FormInput, Toast} from '../../../components/mollecules' import {useApiError} from '../../../hooks/useApiError' interface SwapModalProps { - isOpen: boolean - onClose: () => void - account: Account - fromTicker: Ticker - availableAmount: number + isOpen: boolean + onClose: () => void + account: Account + fromTicker: Ticker + availableAmount: number } interface SwapFormInput { - fromTicker: Ticker - toTicker: Ticker - amount: number - orderType: string - triggerRatio?: number - allowedSlippage: number + fromTicker: Ticker + toTicker: Ticker + amount: number + orderType: string + triggerRatio?: number + allowedSlippage: number +} + +interface ValidationErrorResponse { + type: string + title: string + status: number + errors: Record + traceId: string } const SwapModal: React.FC = ({ - 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.USDC) - const [selectedOrderType, setSelectedOrderType] = useState('market') + isOpen, + onClose, + account, + fromTicker, + availableAmount, + }) => { + const [isLoading, setIsLoading] = useState(false) + const [validationErrors, setValidationErrors] = useState>({}) + const {error, setError, handleApiErrorWithToast} = useApiError() + const {apiUrl} = useApiUrlStore() + const client = new AccountClient({}, apiUrl) + const [selectedToTicker, setSelectedToTicker] = useState(Ticker.USDC) + const [selectedOrderType, setSelectedOrderType] = useState('market') - const { register, handleSubmit, watch, setValue } = useForm({ - 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) { - setSelectedToTicker(e.target.value as Ticker) - } - - function setSelectedOrderTypeEvent(e: React.ChangeEvent) { - setSelectedOrderType(e.target.value) - } - - const onSubmit: SubmitHandler = 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, + const {register, handleSubmit, watch, setValue} = useForm({ + defaultValues: { + fromTicker: fromTicker, + toTicker: Ticker.USDC, + amount: availableAmount * 0.1, // Start with 10% of available amount + orderType: 'market', + allowedSlippage: 0.5, } - ) - - 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 watchedAmount = watch('amount') + + function setSelectedToTickerEvent(e: React.ChangeEvent) { + setSelectedToTicker(e.target.value as Ticker) } - } - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault() - handleSubmit(onSubmit)(e) - } + function setSelectedOrderTypeEvent(e: React.ChangeEvent) { + setSelectedOrderType(e.target.value) + } - const modalContent = ( - <> - {isLoading ? ( -
- -

Processing swap...

-
- ) : error ? ( -
-

{error}

-
- ) : ( + const onSubmit: SubmitHandler = async (form) => { + const t = new Toast(`Swapping ${form.amount} ${form.fromTicker} to ${form.toTicker} on ${account.name}`) + setIsLoading(true) + setError(null) + setValidationErrors({}) + + 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: any) { + // Handle validation errors from API + if (err.response?.data && typeof err.response.data === 'object') { + const errorData = err.response.data as ValidationErrorResponse + console.log(errorData) + if (errorData.errors && typeof errorData.errors === 'object') { + setValidationErrors(errorData.errors) + const errorMessages = Object.values(errorData.errors).flat() + const errorMessage = errorMessages.join(', ') + setError(errorMessage) + t.update('error', `Validation failed: ${errorMessage}`) + } else { + handleApiErrorWithToast(err, t) + } + } else { + handleApiErrorWithToast(err, t) + } + } finally { + setIsLoading(false) + } + } + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault() + await handleSubmit(onSubmit)(e) + } + + const modalContent = ( <> -
-

- Account: {account.name} -

-

- From: {fromTicker} -

-
- -
-
- - - - - -
- -
- { - const value = parseFloat(e.target.value) - setValue('amount', value) - }} - /> - -
- {watchedAmount && availableAmount > 0 ? ( - {((watchedAmount / availableAmount) * 100).toFixed(1)}% of available balance - ) : ( - 0% of available balance - )} -
-
+ {isLoading ? ( +
+ +

Processing swap...

- + ) : error ? ( +
+

{error}

+
+ ) : ( + <> +
+

+ Account: {account.name} +

+

+ From: {fromTicker} +

+
- - - + + {Object.keys(validationErrors).length > 0 && ( +
+
+

Validation Errors:

+ {Object.entries(validationErrors).map(([field, errors]) => ( +
+ {field}: {errors.join(', ')} +
+ ))} +
+
+ )} +
+ + + - - - + +
+ + {validationErrors.Amount && ( +
+ {validationErrors.Amount.map((error, index) => ( +
{error}
+ ))} +
+ )} +
+ { + const value = parseFloat(e.target.value) + setValue('amount', value) + }} + /> - {selectedOrderType === 'limit' && ( - - - - )} +
+ {watchedAmount && availableAmount > 0 ? ( + {((watchedAmount / availableAmount) * 100).toFixed(1)}% of available balance + ) : ( + 0% of available balance + )} +
+
+
+
- -
- + + + -
-
-

- Note: Ensure account has sufficient balance for the swap. -

-
-
+ + + {validationErrors.AllowedSlippage && ( +
+ {validationErrors.AllowedSlippage.map((error, index) => ( +
{error}
+ ))} +
+ )} +
+ + {selectedOrderType === 'limit' && ( + + + {validationErrors.TriggerRatio && ( +
+ {validationErrors.TriggerRatio.map((error, index) => ( +
{error}
+ ))} +
+ )} +
+ )} + + +
+ + +
+
+

+ Note: Ensure account has sufficient balance for the swap. +

+
+
+ + )} - )} - - ) + ) - return ( - - {modalContent} - - ) + return ( + + {modalContent} + + ) } export default SwapModal \ No newline at end of file