Fix swap btc + fix bot stoping
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user