Fix swap btc + fix bot stoping
This commit is contained in:
@@ -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<string, string[]>
|
||||
traceId: string
|
||||
}
|
||||
|
||||
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')
|
||||
isOpen,
|
||||
onClose,
|
||||
account,
|
||||
fromTicker,
|
||||
availableAmount,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string[]>>({})
|
||||
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,
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
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<HTMLSelectElement>) {
|
||||
setSelectedToTicker(e.target.value as Ticker)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
handleSubmit(onSubmit)(e)
|
||||
}
|
||||
function setSelectedOrderTypeEvent(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
setSelectedOrderType(e.target.value)
|
||||
}
|
||||
|
||||
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>
|
||||
) : (
|
||||
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)
|
||||
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 = (
|
||||
<>
|
||||
<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>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-4">
|
||||
<span className="loading loading-spinner loading-md"></span>
|
||||
<p>Processing swap...</p>
|
||||
</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">
|
||||
<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>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<div>
|
||||
<h4 className="font-bold">Validation Errors:</h4>
|
||||
{Object.entries(validationErrors).map(([field, errors]) => (
|
||||
<div key={field} className="mt-1">
|
||||
<strong>{field}:</strong> {errors.join(', ')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<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="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>
|
||||
<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 ${validationErrors.Amount ? 'input-error' : ''}`}
|
||||
{...register('amount', {
|
||||
valueAsNumber: true,
|
||||
min: 0.00000000000001,
|
||||
max: availableAmount,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
{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' && (
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<FormInput label="Allowed Slippage (%)" htmlFor="allowedSlippage">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
placeholder="0.5"
|
||||
className={`input input-bordered w-full ${validationErrors.AllowedSlippage ? 'input-error' : ''}`}
|
||||
{...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 (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Swap Tokens on GMX"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
return (
|
||||
<Modal
|
||||
showModal={isOpen}
|
||||
onClose={onClose}
|
||||
titleHeader="Swap Tokens on GMX"
|
||||
>
|
||||
{modalContent}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwapModal
|
||||
Reference in New Issue
Block a user