Fix swap btc + fix bot stoping

This commit is contained in:
2025-07-06 15:50:50 +07:00
parent f973be2e08
commit 46b43bce1a
3 changed files with 291 additions and 233 deletions

View File

@@ -24,7 +24,7 @@ public class SwapTokensRequest
/// The amount to swap /// The amount to swap
/// </summary> /// </summary>
[Required] [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; } public double Amount { get; set; }
/// <summary> /// <summary>

View File

@@ -224,7 +224,7 @@ public class TradingBot : Bot, ITradingBot
{ {
// Check broker balance before running // Check broker balance before running
var balance = await ExchangeService.GetBalance(Account, false); 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( await LogWarning(
$"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier} and saving backup."); $"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot {Identifier} and saving backup.");

View File

@@ -8,256 +8,314 @@ import {FormInput, Toast} from '../../../components/mollecules'
import {useApiError} from '../../../hooks/useApiError' import {useApiError} from '../../../hooks/useApiError'
interface SwapModalProps { interface SwapModalProps {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
account: Account account: Account
fromTicker: Ticker fromTicker: Ticker
availableAmount: number availableAmount: number
} }
interface SwapFormInput { interface SwapFormInput {
fromTicker: Ticker fromTicker: Ticker
toTicker: Ticker toTicker: Ticker
amount: number amount: number
orderType: string orderType: string
triggerRatio?: number triggerRatio?: number
allowedSlippage: number allowedSlippage: number
}
interface ValidationErrorResponse {
type: string
title: string
status: number
errors: Record<string, string[]>
traceId: string
} }
const SwapModal: React.FC<SwapModalProps> = ({ const SwapModal: React.FC<SwapModalProps> = ({
isOpen, isOpen,
onClose, onClose,
account, account,
fromTicker, fromTicker,
availableAmount, availableAmount,
}) => { }) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { error, setError, handleApiErrorWithToast } = useApiError() const [validationErrors, setValidationErrors] = useState<Record<string, string[]>>({})
const { apiUrl } = useApiUrlStore() const {error, setError, handleApiErrorWithToast} = useApiError()
const client = new AccountClient({}, apiUrl) const {apiUrl} = useApiUrlStore()
const [selectedToTicker, setSelectedToTicker] = useState<Ticker>(Ticker.USDC) const client = new AccountClient({}, apiUrl)
const [selectedOrderType, setSelectedOrderType] = useState<string>('market') const [selectedToTicker, setSelectedToTicker] = useState<Ticker>(Ticker.USDC)
const [selectedOrderType, setSelectedOrderType] = useState<string>('market')
const { register, handleSubmit, watch, setValue } = useForm<SwapFormInput>({ const {register, handleSubmit, watch, setValue} = useForm<SwapFormInput>({
defaultValues: { defaultValues: {
fromTicker: fromTicker, fromTicker: fromTicker,
toTicker: Ticker.USDC, toTicker: Ticker.USDC,
amount: availableAmount * 0.1, // Start with 10% of available amount amount: availableAmount * 0.1, // Start with 10% of available amount
orderType: 'market', orderType: 'market',
allowedSlippage: 0.5, 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) { const watchedAmount = watch('amount')
t.update('success', `Swap successful! Hash: ${result.hash}`)
onClose() function setSelectedToTickerEvent(e: React.ChangeEvent<HTMLSelectElement>) {
} else { setSelectedToTicker(e.target.value as Ticker)
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) => { function setSelectedOrderTypeEvent(e: React.ChangeEvent<HTMLSelectElement>) {
e.preventDefault() setSelectedOrderType(e.target.value)
handleSubmit(onSubmit)(e) }
}
const modalContent = ( const onSubmit: SubmitHandler<SwapFormInput> = async (form) => {
<> const t = new Toast(`Swapping ${form.amount} ${form.fromTicker} to ${form.toTicker} on ${account.name}`)
{isLoading ? ( setIsLoading(true)
<div className="text-center py-4"> setError(null)
<span className="loading loading-spinner loading-md"></span> setValidationErrors({})
<p>Processing swap...</p>
</div> try {
) : error ? ( const result = await client.account_SwapGmxTokens(
<div className="alert alert-error mb-4"> account.name,
<p>{error}</p> {
</div> 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 = (
<> <>
<div className="mb-4"> {isLoading ? (
<p className="mb-2"> <div className="text-center py-4">
<strong>Account:</strong> {account.name} <span className="loading loading-spinner loading-md"></span>
</p> <p>Processing swap...</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> </div>
</FormInput> ) : 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>
<FormInput label="Order Type" htmlFor="orderType"> <form onSubmit={handleFormSubmit}>
<select {Object.keys(validationErrors).length > 0 && (
className="select select-bordered w-full" <div className="alert alert-error mb-4">
{...register('orderType', { <div>
onChange(e) { <h4 className="font-bold">Validation Errors:</h4>
setSelectedOrderTypeEvent(e) {Object.entries(validationErrors).map(([field, errors]) => (
}, <div key={field} className="mt-1">
value: selectedOrderType, <strong>{field}:</strong> {errors.join(', ')}
})} </div>
> ))}
<option value="market">Market</option> </div>
<option value="limit">Limit</option> </div>
<option value="stop">Stop</option> )}
</select> <div className="space-y-4 mb-4">
</FormInput> <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="Allowed Slippage (%)" htmlFor="allowedSlippage"> <FormInput label="Amount" htmlFor="amount">
<input <div className="w-full">
type="number" <input
step="0.1" type="number"
placeholder="0.5" step="any"
className="input input-bordered w-full" placeholder="Enter amount to swap"
{...register('allowedSlippage', { className={`input input-bordered w-full mb-2 ${validationErrors.Amount ? 'input-error' : ''}`}
valueAsNumber: true, {...register('amount', {
min: 0.1, valueAsNumber: true,
max: 10, min: 0.00000000000001,
value: 0.5 max: availableAmount,
})} required: true
/> })}
</FormInput> />
{validationErrors.Amount && (
<div className="text-error text-xs mt-1">
{validationErrors.Amount.map((error, index) => (
<div key={index}>{error}</div>
))}
</div>
)}
<div className="w-full">
<input
type="range"
min="0"
max={availableAmount}
step={0.00000000000001}
className="range range-primary w-full"
value={watchedAmount || 0}
onChange={(e) => {
const value = parseFloat(e.target.value)
setValue('amount', value)
}}
/>
{selectedOrderType === 'limit' && ( <div className="text-center text-xs text-gray-500 mt-1">
<FormInput label="Trigger Ratio" htmlFor="triggerRatio"> {watchedAmount && availableAmount > 0 ? (
<input <span>{((watchedAmount / availableAmount) * 100).toFixed(1)}% of available balance</span>
type="number" ) : (
step="any" <span>0% of available balance</span>
placeholder="Enter trigger ratio" )}
className="input input-bordered w-full" </div>
{...register('triggerRatio', { valueAsNumber: true })} </div>
/> </div>
</FormInput> </FormInput>
)}
<button <FormInput label="Order Type" htmlFor="orderType">
type="submit" <select
className="btn btn-primary w-full mt-2" className="select select-bordered w-full"
disabled={isLoading || !watchedAmount || watchedAmount <= 0} {...register('orderType', {
> onChange(e) {
{isLoading ? ( setSelectedOrderTypeEvent(e)
<span className="loading loading-spinner"></span> },
) : ( value: selectedOrderType,
`Swap ${watchedAmount || 0} ${fromTicker} to ${selectedToTicker}` })}
)} >
</button> <option value="market">Market</option>
</div> <option value="limit">Limit</option>
</form> <option value="stop">Stop</option>
</select>
</FormInput>
<div className="mt-4"> <FormInput label="Allowed Slippage (%)" htmlFor="allowedSlippage">
<div className="alert alert-info"> <input
<p className="text-sm"> type="number"
<strong>Note:</strong> Ensure account has sufficient balance for the swap. step="0.1"
</p> placeholder="0.5"
</div> className={`input input-bordered w-full ${validationErrors.AllowedSlippage ? 'input-error' : ''}`}
</div> {...register('allowedSlippage', {
valueAsNumber: true,
min: 0.1,
max: 10,
value: 0.5
})}
/>
{validationErrors.AllowedSlippage && (
<div className="text-error text-xs mt-1">
{validationErrors.AllowedSlippage.map((error, index) => (
<div key={index}>{error}</div>
))}
</div>
)}
</FormInput>
{selectedOrderType === 'limit' && (
<FormInput label="Trigger Ratio" htmlFor="triggerRatio">
<input
type="number"
step="any"
placeholder="Enter trigger ratio"
className={`input input-bordered w-full ${validationErrors.TriggerRatio ? 'input-error' : ''}`}
{...register('triggerRatio', {valueAsNumber: true})}
/>
{validationErrors.TriggerRatio && (
<div className="text-error text-xs mt-1">
{validationErrors.TriggerRatio.map((error, index) => (
<div key={index}>{error}</div>
))}
</div>
)}
</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 ( return (
<Modal <Modal
showModal={isOpen} showModal={isOpen}
onClose={onClose} onClose={onClose}
titleHeader="Swap Tokens on GMX" titleHeader="Swap Tokens on GMX"
> >
{modalContent} {modalContent}
</Modal> </Modal>
) )
} }
export default SwapModal export default SwapModal