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} - - - - - - - - {Object.values(Ticker) - .filter(ticker => ticker !== fromTicker) // Exclude the from ticker - .map((ticker) => ( - - {ticker} - - ))} - - - - - - - - { - 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} + + - - - Market - Limit - Stop - - + + {Object.keys(validationErrors).length > 0 && ( + + + Validation Errors: + {Object.entries(validationErrors).map(([field, errors]) => ( + + {field}: {errors.join(', ')} + + ))} + + + )} + + + + {Object.values(Ticker) + .filter(ticker => ticker !== fromTicker) // Exclude the from ticker + .map((ticker) => ( + + {ticker} + + ))} + + - - - + + + + {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 + )} + + + + - - {isLoading ? ( - - ) : ( - `Swap ${watchedAmount || 0} ${fromTicker} to ${selectedToTicker}` - )} - - - + + + Market + Limit + Stop + + - - - - 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} + ))} + + )} + + )} + + + {isLoading ? ( + + ) : ( + `Swap ${watchedAmount || 0} ${fromTicker} to ${selectedToTicker}` + )} + + + + + + + + Note: Ensure account has sufficient balance for the swap. + + + + > + )} > - )} - > - ) + ) - return ( - - {modalContent} - - ) + return ( + + {modalContent} + + ) } export default SwapModal \ No newline at end of file
Processing swap...
{error}
- Account: {account.name} -
- From: {fromTicker} -
+ Account: {account.name} +
+ From: {fromTicker} +
- Note: Ensure account has sufficient balance for the swap. -
+ Note: Ensure account has sufficient balance for the swap. +