feat: swap functionality (#319)
This commit is contained in:
parent
fef9227a0d
commit
e09c2f9d53
@ -25,7 +25,7 @@
|
||||
"classnames": "^2.3.2",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"moment": "^2.29.4",
|
||||
"next": "^13.4.10",
|
||||
"next": "13.4.9",
|
||||
"react": "^18.2.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -8,6 +8,7 @@ import { MarsOracleOsmosisQueryClient } from 'types/generated/mars-oracle-osmosi
|
||||
import { MarsMockRedBankQueryClient } from 'types/generated/mars-mock-red-bank/MarsMockRedBank.client'
|
||||
import { MarsMockVaultQueryClient } from 'types/generated/mars-mock-vault/MarsMockVault.client'
|
||||
import { MarsParamsQueryClient } from 'types/generated/mars-params/MarsParams.client'
|
||||
import { MarsSwapperOsmosisQueryClient } from 'types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.client'
|
||||
|
||||
let _cosmWasmClient: CosmWasmClient
|
||||
let _accountNftQueryClient: MarsAccountNftQueryClient
|
||||
@ -16,6 +17,7 @@ let _oracleQueryClient: MarsOracleOsmosisQueryClient
|
||||
let _redBankQueryClient: MarsMockRedBankQueryClient
|
||||
let _paramsQueryClient: MarsParamsQueryClient
|
||||
let _incentivesQueryClient: MarsIncentivesQueryClient
|
||||
let _swapperOsmosisClient: MarsSwapperOsmosisQueryClient
|
||||
|
||||
const getClient = async () => {
|
||||
try {
|
||||
@ -119,6 +121,19 @@ const getIncentivesQueryClient = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getSwapperQueryClient = async () => {
|
||||
try {
|
||||
if (!_swapperOsmosisClient) {
|
||||
const client = await getClient()
|
||||
_swapperOsmosisClient = new MarsSwapperOsmosisQueryClient(client, ENV.ADDRESS_SWAPPER)
|
||||
}
|
||||
|
||||
return _swapperOsmosisClient
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getClient,
|
||||
getAccountNftQueryClient,
|
||||
@ -128,4 +143,5 @@ export {
|
||||
getRedBankQueryClient,
|
||||
getVaultQueryClient,
|
||||
getIncentivesQueryClient,
|
||||
getSwapperQueryClient,
|
||||
}
|
||||
|
14
src/api/swap/estimateExactIn.ts
Normal file
14
src/api/swap/estimateExactIn.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { getSwapperQueryClient } from 'api/cosmwasm-client'
|
||||
import { ZERO } from 'constants/math'
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
export default async function estimateExactIn(coinIn: Coin, denomOut: string) {
|
||||
try {
|
||||
const swapperClient = await getSwapperQueryClient()
|
||||
const estimatedAmount = (await swapperClient.estimateExactInSwap({ coinIn, denomOut })).amount
|
||||
|
||||
return BN(estimatedAmount)
|
||||
} catch (ex) {
|
||||
return ZERO
|
||||
}
|
||||
}
|
20
src/api/swap/getSwapRoute.ts
Normal file
20
src/api/swap/getSwapRoute.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getSwapperQueryClient } from 'api/cosmwasm-client'
|
||||
|
||||
interface Route {
|
||||
pool_id: string
|
||||
token_out_denom: string
|
||||
}
|
||||
|
||||
export default async function getSwapRoute(denomIn: string, denomOut: string): Promise<Route[]> {
|
||||
try {
|
||||
const swapperClient = await getSwapperQueryClient()
|
||||
const routes = await swapperClient.route({
|
||||
denomIn,
|
||||
denomOut,
|
||||
})
|
||||
|
||||
return routes.route as unknown as Route[]
|
||||
} catch (ex) {
|
||||
return []
|
||||
}
|
||||
}
|
10
src/components/Icons/InfoCircle.svg
Normal file
10
src/components/Icons/InfoCircle.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<g clip-path="url(#clip0_2324_198820)">
|
||||
<path d="M7.99967 10.6673V8.00065M7.99967 5.33398H8.00634M14.6663 8.00065C14.6663 11.6826 11.6816 14.6673 7.99967 14.6673C4.31778 14.6673 1.33301 11.6826 1.33301 8.00065C1.33301 4.31875 4.31778 1.33398 7.99967 1.33398C11.6816 1.33398 14.6663 4.31875 14.6663 8.00065Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2324_198820">
|
||||
<rect width="16" height="16" fill="currentColor"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 588 B |
@ -44,4 +44,5 @@ export { default as SwapIcon } from 'components/Icons/SwapIcon.svg'
|
||||
export { default as TrashBin } from 'components/Icons/TrashBin.svg'
|
||||
export { default as VerticalThreeLine } from 'components/Icons/VerticalThreeLine.svg'
|
||||
export { default as Wallet } from 'components/Icons/Wallet.svg'
|
||||
export { default as InfoCircle } from 'components/Icons/InfoCircle.svg'
|
||||
// @endindex
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChangeEvent, useCallback } from 'react'
|
||||
import { ChangeEvent, useCallback, useMemo } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import InputOverlay from 'components/RangeInput/InputOverlay'
|
||||
@ -17,7 +17,7 @@ function RangeInput(props: Props) {
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(parseInt(event.target.value))
|
||||
onChange(parseFloat(event.target.value))
|
||||
},
|
||||
[onChange],
|
||||
)
|
||||
@ -32,15 +32,20 @@ function RangeInput(props: Props) {
|
||||
<input
|
||||
className={className.input}
|
||||
type='range'
|
||||
value={value}
|
||||
value={value.toFixed(2)}
|
||||
step={max / 100}
|
||||
max={max}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
<InputOverlay max={max} marginThreshold={marginThreshold} value={value} />
|
||||
<InputOverlay
|
||||
max={max}
|
||||
marginThreshold={marginThreshold}
|
||||
value={parseFloat(value.toFixed(2))}
|
||||
/>
|
||||
</div>
|
||||
<div className={className.legendWrapper}>
|
||||
<span>0</span>
|
||||
<span>{max}</span>
|
||||
<span>{max.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -0,0 +1,87 @@
|
||||
import classNames from 'classnames'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import NumberInput from 'components/NumberInput'
|
||||
import { formatValue } from 'utils/formatters'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
max: BigNumber
|
||||
asset: Asset
|
||||
amount: BigNumber
|
||||
maxButtonLabel: string
|
||||
assetUSDValue: BigNumber
|
||||
amountValueText?: string
|
||||
containerClassName?: string
|
||||
setAmount: (amount: BigNumber) => void
|
||||
onFocus?: () => void
|
||||
}
|
||||
|
||||
export default function AssetAmountInput(props: Props) {
|
||||
const {
|
||||
label,
|
||||
amount,
|
||||
setAmount,
|
||||
asset,
|
||||
containerClassName,
|
||||
max,
|
||||
maxButtonLabel,
|
||||
onFocus,
|
||||
assetUSDValue,
|
||||
} = props
|
||||
|
||||
const handleMaxClick = useCallback(() => {
|
||||
setAmount(max)
|
||||
}, [max, setAmount])
|
||||
|
||||
const maxValue = useMemo(() => {
|
||||
const val = max.shiftedBy(-asset.decimals)
|
||||
return val.isGreaterThan(1) ? val.toFixed(2) : val.toPrecision(2)
|
||||
}, [asset.decimals, max])
|
||||
|
||||
return (
|
||||
<div className={classNames(className.container, containerClassName)}>
|
||||
<label>
|
||||
{label}
|
||||
<div className={className.inputWrapper}>
|
||||
<NumberInput
|
||||
asset={asset}
|
||||
amount={amount}
|
||||
className={className.input}
|
||||
maxDecimals={asset.decimals}
|
||||
max={max}
|
||||
onChange={setAmount}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
<span>{asset.symbol}</span>
|
||||
</div>
|
||||
<div className={className.footer}>
|
||||
<div className={className.maxButtonWrapper}>
|
||||
<span className={className.maxButtonLabel}>{maxButtonLabel}</span>
|
||||
<span className={className.maxValue}>{maxValue}</span>
|
||||
<div className={className.maxButton} onClick={handleMaxClick}>
|
||||
MAX
|
||||
</div>
|
||||
</div>
|
||||
<div className={className.assetValue}>
|
||||
{formatValue(assetUSDValue.toString(), { prefix: '~ $', minDecimals: 2 })}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const className = {
|
||||
container: '',
|
||||
inputWrapper:
|
||||
'flex flex-1 flex-row py-3 border-[1px] border-white border-opacity-20 rounded bg-white bg-opacity-5 pl-3 pr-2 mt-2',
|
||||
input: 'border-none bg-transparent outline-none flex-1 !text-left',
|
||||
footer: 'flex flex-1 flex-row',
|
||||
maxButtonWrapper: 'flex flex-1 flex-row mt-2',
|
||||
maxButtonLabel: 'font-bold text-xs',
|
||||
maxValue: 'font-bold text-xs text-white text-opacity-60 mx-1',
|
||||
maxButton:
|
||||
'cursor-pointer select-none bg-white bg-opacity-20 text-2xs !leading-3 font-bold py-0.5 px-1.5 rounded',
|
||||
assetValue: 'text-xs text-white text-opacity-60 mt-2',
|
||||
}
|
33
src/components/Trade/TradeModule/SwapForm/MarginToggle.tsx
Normal file
33
src/components/Trade/TradeModule/SwapForm/MarginToggle.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { InfoCircle } from 'components/Icons'
|
||||
import Switch from 'components/Switch'
|
||||
import Text from 'components/Text'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import ConditionalWrapper from 'hocs/ConditionalWrapper'
|
||||
|
||||
interface Props {
|
||||
checked: boolean
|
||||
onChange: (value: boolean) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function MarginToggle(props: Props) {
|
||||
return (
|
||||
<div className='flex flex-1 flex-row justify-between bg-white/5 px-4 py-2'>
|
||||
<Text size='sm'>Margin</Text>
|
||||
|
||||
<ConditionalWrapper
|
||||
condition={!!props.disabled}
|
||||
wrapper={(children) => (
|
||||
<Tooltip type='info' content={<Text size='sm'>Margin is not supported yet.</Text>}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div className='flex flex-row'>
|
||||
<Switch {...props} name='margin' />
|
||||
<InfoCircle width={16} height={16} className='ml-2 mt-0.5 opacity-20' />
|
||||
</div>
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { OrderTab } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/types'
|
||||
|
||||
const ORDER_TYPE_UNAVAILABLE_MESSAGE =
|
||||
'This type of order is currently unavailable and is coming soon.'
|
||||
|
||||
export const ORDER_TYPE_TABS: OrderTab[] = [
|
||||
{ type: 'Market', isDisabled: false, tooltipText: '' },
|
||||
{
|
||||
type: 'Limit',
|
||||
isDisabled: true,
|
||||
tooltipText: ORDER_TYPE_UNAVAILABLE_MESSAGE,
|
||||
},
|
||||
{
|
||||
type: 'Stop',
|
||||
isDisabled: true,
|
||||
tooltipText: ORDER_TYPE_UNAVAILABLE_MESSAGE,
|
||||
},
|
||||
]
|
@ -0,0 +1,54 @@
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { InfoCircle } from 'components/Icons'
|
||||
import Text from 'components/Text'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { ORDER_TYPE_TABS } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/constants'
|
||||
import ConditionalWrapper from 'hocs/ConditionalWrapper'
|
||||
import { AvailableOrderType } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/types'
|
||||
|
||||
interface Props {
|
||||
selected: AvailableOrderType
|
||||
onChange: (value: AvailableOrderType) => void
|
||||
}
|
||||
|
||||
export default function OrderTypeSelector(props: Props) {
|
||||
const { selected, onChange } = props
|
||||
|
||||
return (
|
||||
<div className={className.wrapper}>
|
||||
{ORDER_TYPE_TABS.map((tab) => {
|
||||
const isSelected = tab.type === selected
|
||||
const classes = classNames(className.tab, {
|
||||
[className.selectedTab]: isSelected,
|
||||
[className.disabledTab]: tab.isDisabled,
|
||||
})
|
||||
|
||||
return (
|
||||
<ConditionalWrapper
|
||||
key={tab.type}
|
||||
condition={tab.isDisabled && !!tab.tooltipText}
|
||||
wrapper={(children) => (
|
||||
<Tooltip type='info' content={<Text size='sm'>{tab.tooltipText}</Text>}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<div onClick={() => onChange(tab.type)} className={classes}>
|
||||
{tab.type}
|
||||
{tab.isDisabled && <InfoCircle className={className.infoCircle} />}
|
||||
</div>
|
||||
</ConditionalWrapper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const className = {
|
||||
wrapper: 'flex flex-1 flex-row px-3 pt-3',
|
||||
tab: 'mr-4 pb-2 cursor-pointer select-none flex flex-row',
|
||||
selectedTab: 'border-b-2 border-pink border-solid',
|
||||
disabledTab: 'opacity-50 pointer-events-none',
|
||||
infoCircle: 'w-4 h-4 ml-2 mt-1',
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export type AvailableOrderType = 'Market' | 'Limit' | 'Stop'
|
||||
export interface OrderTab {
|
||||
type: AvailableOrderType
|
||||
isDisabled: boolean
|
||||
tooltipText: string
|
||||
}
|
74
src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx
Normal file
74
src/components/Trade/TradeModule/SwapForm/TradeSummary.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import classNames from 'classnames'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import Button from 'components/Button'
|
||||
import { hardcodedFee } from 'utils/constants'
|
||||
import { formatAmountWithSymbol } from 'utils/formatters'
|
||||
import { getAssetByDenom } from 'utils/assets'
|
||||
import useSwapRoute from 'hooks/useSwapRoute'
|
||||
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
sellAsset: Asset
|
||||
containerClassName?: string
|
||||
buyButtonDisabled: boolean
|
||||
buyAction: () => void
|
||||
}
|
||||
|
||||
export default function TradeSummary(props: Props) {
|
||||
const { containerClassName, buyAsset, sellAsset, buyAction, buyButtonDisabled } = props
|
||||
const { data: routes, isLoading: isRouteLoading } = useSwapRoute(sellAsset.denom, buyAsset.denom)
|
||||
const [isButtonBusy, setButtonBusy] = useState(false)
|
||||
|
||||
const parsedRoutes = useMemo(() => {
|
||||
if (!routes.length) return '-'
|
||||
|
||||
const routeSymbols = routes.map((r) => getAssetByDenom(r.token_out_denom)?.symbol)
|
||||
routeSymbols.unshift(sellAsset.symbol)
|
||||
|
||||
return routeSymbols.join(' -> ')
|
||||
}, [routes, sellAsset.symbol])
|
||||
|
||||
const handleBuyClick = useCallback(async () => {
|
||||
setButtonBusy(true)
|
||||
await buyAction()
|
||||
setButtonBusy(false)
|
||||
}, [buyAction])
|
||||
|
||||
const buttonText = useMemo(
|
||||
() => (routes.length ? `Buy ${buyAsset.symbol}` : 'No route found'),
|
||||
[buyAsset.symbol, routes],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classNames(containerClassName, className.container)}>
|
||||
<div className={className.summaryWrapper}>
|
||||
<span className={className.title}>Summary</span>
|
||||
<div className={className.infoLine}>
|
||||
<span className={className.infoLineLabel}>Fees</span>
|
||||
<span>{formatAmountWithSymbol(hardcodedFee.amount[0])}</span>
|
||||
</div>
|
||||
<div className={className.infoLine}>
|
||||
<span className={className.infoLineLabel}>Route</span>
|
||||
<span>{parsedRoutes}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
disabled={routes.length === 0 || buyButtonDisabled}
|
||||
showProgressIndicator={isButtonBusy || isRouteLoading}
|
||||
text={buttonText}
|
||||
onClick={handleBuyClick}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const className = {
|
||||
container:
|
||||
'flex flex-1 flex-col bg-white bg-opacity-5 rounded border-[1px] border-white border-opacity-20 ',
|
||||
title: 'text-xs font-bold mb-2',
|
||||
summaryWrapper: 'flex flex-1 flex-col m-3',
|
||||
infoLine: 'flex flex-1 flex-row text-xs text-white justify-between mb-1',
|
||||
infoLineLabel: 'opacity-40',
|
||||
}
|
168
src/components/Trade/TradeModule/SwapForm/index.tsx
Normal file
168
src/components/Trade/TradeModule/SwapForm/index.tsx
Normal file
@ -0,0 +1,168 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import Divider from 'components/Divider'
|
||||
import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
|
||||
import { SLIPPAGE_KEY } from 'constants/localStore'
|
||||
import { ZERO } from 'constants/math'
|
||||
import useCurrentAccount from 'hooks/useCurrentAccount'
|
||||
import useLocalStorage from 'hooks/useLocalStorage'
|
||||
import usePrices from 'hooks/usePrices'
|
||||
import useStore from 'store'
|
||||
import { byDenom } from 'utils/array'
|
||||
import { hardcodedFee } from 'utils/constants'
|
||||
import RangeInput from 'components/RangeInput'
|
||||
import { BN } from 'utils/helpers'
|
||||
import AssetAmountInput from 'components/Trade/TradeModule/SwapForm/AssetAmountInput'
|
||||
import MarginToggle from 'components/Trade/TradeModule/SwapForm/MarginToggle'
|
||||
import OrderTypeSelector from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector'
|
||||
import { AvailableOrderType } from 'components/Trade/TradeModule/SwapForm/OrderTypeSelector/types'
|
||||
import TradeSummary from 'components/Trade/TradeModule/SwapForm/TradeSummary'
|
||||
import { BNCoin } from 'types/classes/BNCoin'
|
||||
import estimateExactIn from 'api/swap/estimateExactIn'
|
||||
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
sellAsset: Asset
|
||||
}
|
||||
|
||||
export default function SwapForm(props: Props) {
|
||||
const { buyAsset, sellAsset } = props
|
||||
const account = useCurrentAccount()
|
||||
const { data: prices } = usePrices()
|
||||
const swap = useStore((s) => s.swap)
|
||||
const [isMarginChecked, setMarginChecked] = useState(false)
|
||||
const [buyAssetAmount, setBuyAssetAmount] = useState(ZERO)
|
||||
const [sellAssetAmount, setSellAssetAmount] = useState(ZERO)
|
||||
const [slippage] = useLocalStorage(SLIPPAGE_KEY, DEFAULT_SETTINGS.slippage)
|
||||
const [focusedInput, setFocusedInput] = useState<'buy' | 'sell' | null>(null)
|
||||
const [maxBuyableAmountEstimation, setMaxBuyableAmountEstimation] = useState(ZERO)
|
||||
const [selectedOrderType, setSelectedOrderType] = useState<AvailableOrderType>('Market')
|
||||
|
||||
const accountSellAssetDeposit = useMemo(
|
||||
() => account?.deposits.find(byDenom(sellAsset.denom))?.amount || ZERO,
|
||||
[account, sellAsset.denom],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
estimateExactIn(
|
||||
{ denom: sellAsset.denom, amount: accountSellAssetDeposit },
|
||||
buyAsset.denom,
|
||||
).then(setMaxBuyableAmountEstimation)
|
||||
}, [accountSellAssetDeposit, buyAsset.denom, sellAsset.denom])
|
||||
|
||||
const [buyAssetValue, sellAssetValue] = useMemo(() => {
|
||||
const buyAssetPrice = prices.find(byDenom(buyAsset.denom))?.amount ?? ZERO
|
||||
const sellAssetPrice = prices.find(byDenom(sellAsset.denom))?.amount ?? ZERO
|
||||
|
||||
return [
|
||||
buyAssetPrice.multipliedBy(buyAssetAmount.shiftedBy(-buyAsset.decimals)),
|
||||
sellAssetPrice.multipliedBy(sellAssetAmount.shiftedBy(-sellAsset.decimals)),
|
||||
]
|
||||
}, [
|
||||
prices,
|
||||
buyAsset.denom,
|
||||
buyAsset.decimals,
|
||||
sellAsset.denom,
|
||||
sellAsset.decimals,
|
||||
buyAssetAmount,
|
||||
sellAssetAmount,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedInput === 'sell') {
|
||||
estimateExactIn(
|
||||
{ denom: sellAsset.denom, amount: sellAssetAmount.toString() },
|
||||
buyAsset.denom,
|
||||
).then(setBuyAssetAmount)
|
||||
}
|
||||
}, [buyAsset.denom, focusedInput, sellAsset.denom, sellAssetAmount])
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedInput === 'buy') {
|
||||
estimateExactIn(
|
||||
{
|
||||
denom: buyAsset.denom,
|
||||
amount: buyAssetAmount.toString(),
|
||||
},
|
||||
sellAsset.denom,
|
||||
).then(setSellAssetAmount)
|
||||
}
|
||||
}, [buyAsset.denom, buyAssetAmount, focusedInput, sellAsset.denom])
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedInput(null)
|
||||
setBuyAssetAmount(ZERO)
|
||||
setSellAssetAmount(ZERO)
|
||||
}, [sellAsset.denom])
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedInput(null)
|
||||
}, [buyAsset.denom])
|
||||
|
||||
const handleBuyClick = useCallback(async () => {
|
||||
if (account?.id) {
|
||||
const isSucceeded = await swap({
|
||||
fee: hardcodedFee,
|
||||
accountId: account.id,
|
||||
coinIn: BNCoin.fromDenomAndBigNumber(sellAsset.denom, sellAssetAmount.integerValue()),
|
||||
denomOut: buyAsset.denom,
|
||||
slippage,
|
||||
})
|
||||
if (isSucceeded) {
|
||||
setSellAssetAmount(ZERO)
|
||||
}
|
||||
}
|
||||
}, [account?.id, buyAsset.denom, sellAsset.denom, sellAssetAmount, slippage, swap])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<MarginToggle checked={isMarginChecked} onChange={setMarginChecked} disabled />
|
||||
|
||||
<Divider />
|
||||
<OrderTypeSelector selected={selectedOrderType} onChange={setSelectedOrderType} />
|
||||
|
||||
<AssetAmountInput
|
||||
label='Buy'
|
||||
max={maxBuyableAmountEstimation}
|
||||
amount={buyAssetAmount}
|
||||
setAmount={setBuyAssetAmount}
|
||||
asset={buyAsset}
|
||||
assetUSDValue={buyAssetValue}
|
||||
maxButtonLabel='Max Amount:'
|
||||
containerClassName='mx-3 my-6'
|
||||
onFocus={() => setFocusedInput('buy')}
|
||||
/>
|
||||
|
||||
<RangeInput
|
||||
max={accountSellAssetDeposit.shiftedBy(-sellAsset.decimals).toNumber()}
|
||||
value={sellAssetAmount.shiftedBy(-sellAsset.decimals).toNumber()}
|
||||
onChange={(value) => {
|
||||
setFocusedInput('sell')
|
||||
setSellAssetAmount(BN(value).shiftedBy(sellAsset.decimals).integerValue())
|
||||
}}
|
||||
wrapperClassName='p-4'
|
||||
/>
|
||||
|
||||
<AssetAmountInput
|
||||
label='Sell'
|
||||
max={accountSellAssetDeposit}
|
||||
amount={sellAssetAmount}
|
||||
setAmount={setSellAssetAmount}
|
||||
assetUSDValue={sellAssetValue}
|
||||
asset={sellAsset}
|
||||
maxButtonLabel='Balance:'
|
||||
containerClassName='mx-3'
|
||||
onFocus={() => setFocusedInput('sell')}
|
||||
/>
|
||||
|
||||
<TradeSummary
|
||||
containerClassName='m-3 mt-10'
|
||||
buyAsset={buyAsset}
|
||||
sellAsset={sellAsset}
|
||||
buyAction={handleBuyClick}
|
||||
buyButtonDisabled={sellAssetAmount.isZero()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import { useState } from 'react'
|
||||
|
||||
import Divider from 'components/Divider'
|
||||
import RangeInput from 'components/RangeInput'
|
||||
import AssetSelector from 'components/Trade/TradeModule/AssetSelector'
|
||||
import SwapForm from 'components/Trade/TradeModule/SwapForm'
|
||||
|
||||
interface Props {
|
||||
buyAsset: Asset
|
||||
@ -13,7 +11,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function TradeModule(props: Props) {
|
||||
const [value, setValue] = useState(0)
|
||||
const { buyAsset, sellAsset, onChangeBuyAsset, onChangeSellAsset } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -24,19 +22,13 @@ export default function TradeModule(props: Props) {
|
||||
)}
|
||||
>
|
||||
<AssetSelector
|
||||
buyAsset={props.buyAsset}
|
||||
sellAsset={props.sellAsset}
|
||||
onChangeBuyAsset={props.onChangeBuyAsset}
|
||||
onChangeSellAsset={props.onChangeSellAsset}
|
||||
/>
|
||||
<Divider />
|
||||
<RangeInput
|
||||
max={4000}
|
||||
marginThreshold={2222}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
wrapperClassName='p-4'
|
||||
buyAsset={buyAsset}
|
||||
sellAsset={sellAsset}
|
||||
onChangeBuyAsset={onChangeBuyAsset}
|
||||
onChangeSellAsset={onChangeSellAsset}
|
||||
/>
|
||||
|
||||
<SwapForm buyAsset={buyAsset} sellAsset={sellAsset} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
3
src/constants/math.ts
Normal file
3
src/constants/math.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { BN } from 'utils/helpers'
|
||||
|
||||
export const ZERO = BN(0)
|
9
src/hooks/useSwapRoute.ts
Normal file
9
src/hooks/useSwapRoute.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import useSWR from 'swr'
|
||||
|
||||
import getSwapRoute from 'api/swap/getSwapRoute'
|
||||
|
||||
export default function useSwapRoute(denomIn: string, denomOut: string) {
|
||||
return useSWR(`swapRoute-${denomIn}-${denomOut}`, () => getSwapRoute(denomIn, denomOut), {
|
||||
fallbackData: [],
|
||||
})
|
||||
}
|
@ -13,6 +13,7 @@ import {
|
||||
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
|
||||
import { getSingleValueFromBroadcastResult } from 'utils/broadcast'
|
||||
import { formatAmountWithSymbol } from 'utils/formatters'
|
||||
import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse'
|
||||
|
||||
export default function createBroadcastSlice(
|
||||
set: SetState<Store>,
|
||||
@ -324,5 +325,36 @@ export default function createBroadcastSlice(
|
||||
)
|
||||
return !!response.result
|
||||
},
|
||||
swap: async (options: {
|
||||
fee: StdFee
|
||||
accountId: string
|
||||
coinIn: BNCoin
|
||||
denomOut: string
|
||||
slippage: number
|
||||
}) => {
|
||||
const msg: CreditManagerExecuteMsg = {
|
||||
update_credit_account: {
|
||||
account_id: options.accountId,
|
||||
actions: [
|
||||
{
|
||||
swap_exact_in: {
|
||||
coin_in: options.coinIn.toActionCoin(),
|
||||
denom_out: options.denomOut,
|
||||
slippage: options.slippage.toString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const response = await get().executeMsg({ msg, fee: options.fee })
|
||||
const coinOut = getTokenOutFromSwapResponse(response, options.denomOut)
|
||||
const successMessage = `Swapped ${formatAmountWithSymbol(
|
||||
options.coinIn.toCoin(),
|
||||
)} for ${formatAmountWithSymbol(coinOut)}`
|
||||
|
||||
handleResponseMessages(response, successMessage)
|
||||
return !!response.result
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -28,3 +28,14 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
input[type='number']::-webkit-outer-spin-button,
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number'] {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
-moz-appearance: textfield !important;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
7
src/types/interfaces/store/broadcast.d.ts
vendored
7
src/types/interfaces/store/broadcast.d.ts
vendored
@ -56,4 +56,11 @@ interface BroadcastSlice {
|
||||
coin: BNCoin
|
||||
accountBalance?: boolean
|
||||
}) => Promise<boolean>
|
||||
swap: (options: {
|
||||
fee: StdFee
|
||||
accountId: string
|
||||
coinIn: BNCoin
|
||||
denomOut: string
|
||||
slippage: number
|
||||
}) => Promise<boolean>
|
||||
}
|
||||
|
18
src/utils/getTokenOutFromSwapResponse.ts
Normal file
18
src/utils/getTokenOutFromSwapResponse.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export default function getTokenOutFromSwapResponse(
|
||||
response: BroadcastResult,
|
||||
denom: string,
|
||||
): Coin {
|
||||
if (response.result) {
|
||||
const rawLogs = JSON.parse(response.result.rawLogs)
|
||||
const events = rawLogs[0].events
|
||||
const tokenSwappedEvent = events.find((e: { type: string }) => e.type === 'token_swapped')
|
||||
const tokensOutValue = tokenSwappedEvent.attributes.find(
|
||||
(a: { key: string }) => a.key === 'tokens_out',
|
||||
).value
|
||||
const amount = tokensOutValue.split(denom)[0]
|
||||
|
||||
return { denom, amount }
|
||||
}
|
||||
|
||||
return { denom: '', amount: '' }
|
||||
}
|
108
yarn.lock
108
yarn.lock
@ -2640,10 +2640,10 @@
|
||||
tweetnacl "^1.0.3"
|
||||
tweetnacl-util "^0.15.1"
|
||||
|
||||
"@next/env@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.10.tgz#8b17783d2c09be126bbde9ff1164566517131bff"
|
||||
integrity sha512-3G1yD/XKTSLdihyDSa8JEsaWOELY+OWe08o0LUYzfuHp1zHDA8SObQlzKt+v+wrkkPcnPweoLH1ImZeUa0A1NQ==
|
||||
"@next/env@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e"
|
||||
integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw==
|
||||
|
||||
"@next/eslint-plugin-next@13.4.12":
|
||||
version "13.4.12"
|
||||
@ -2652,50 +2652,50 @@
|
||||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-darwin-arm64@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.10.tgz#962ac55559970d1725163ff9d62d008bc1c33503"
|
||||
integrity sha512-4bsdfKmmg7mgFGph0UorD1xWfZ5jZEw4kKRHYEeTK9bT1QnMbPVPlVXQRIiFPrhoDQnZUoa6duuPUJIEGLV1Jg==
|
||||
"@next/swc-darwin-arm64@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677"
|
||||
integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ==
|
||||
|
||||
"@next/swc-darwin-x64@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.10.tgz#90c01fdce5101953df0039eef48e4074055cc5aa"
|
||||
integrity sha512-ngXhUBbcZIWZWqNbQSNxQrB9T1V+wgfCzAor2olYuo/YpaL6mUYNUEgeBMhr8qwV0ARSgKaOp35lRvB7EmCRBg==
|
||||
"@next/swc-darwin-x64@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8"
|
||||
integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.10.tgz#8fc25052c345ffc8f6c51f61d1bb6c359b80ab2b"
|
||||
integrity sha512-SjCZZCOmHD4uyM75MVArSAmF5Y+IJSGroPRj2v9/jnBT36SYFTORN8Ag/lhw81W9EeexKY/CUg2e9mdebZOwsg==
|
||||
"@next/swc-linux-arm64-gnu@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1"
|
||||
integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg==
|
||||
|
||||
"@next/swc-linux-arm64-musl@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.10.tgz#25e6b0dbb87c89c44c3e3680227172862bc7072c"
|
||||
integrity sha512-F+VlcWijX5qteoYIOxNiBbNE8ruaWuRlcYyIRK10CugqI/BIeCDzEDyrHIHY8AWwbkTwe6GRHabMdE688Rqq4Q==
|
||||
"@next/swc-linux-arm64-musl@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0"
|
||||
integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.10.tgz#24fa8070ea0855c0aa020832ce7d1b84d3413fc1"
|
||||
integrity sha512-WDv1YtAV07nhfy3i1visr5p/tjiH6CeXp4wX78lzP1jI07t4PnHHG1WEDFOduXh3WT4hG6yN82EQBQHDi7hBrQ==
|
||||
"@next/swc-linux-x64-gnu@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a"
|
||||
integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg==
|
||||
|
||||
"@next/swc-linux-x64-musl@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.10.tgz#ae55914d50589a4f8b91c8eeebdd713f0c1b1675"
|
||||
integrity sha512-zFkzqc737xr6qoBgDa3AwC7jPQzGLjDlkNmt/ljvQJ/Veri5ECdHjZCUuiTUfVjshNIIpki6FuP0RaQYK9iCRg==
|
||||
"@next/swc-linux-x64-musl@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a"
|
||||
integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.10.tgz#ab3098b2305f3c0e46dfb2e318a9988bff884047"
|
||||
integrity sha512-IboRS8IWz5mWfnjAdCekkl8s0B7ijpWeDwK2O8CdgZkoCDY0ZQHBSGiJ2KViAG6+BJVfLvcP+a2fh6cdyBr9QQ==
|
||||
"@next/swc-win32-arm64-msvc@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a"
|
||||
integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.10.tgz#a1c5980538641ca656012c00d05b08882cf0ec9f"
|
||||
integrity sha512-bSA+4j8jY4EEiwD/M2bol4uVEu1lBlgsGdvM+mmBm/BbqofNBfaZ2qwSbwE2OwbAmzNdVJRFRXQZ0dkjopTRaQ==
|
||||
"@next/swc-win32-ia32-msvc@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190"
|
||||
integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA==
|
||||
|
||||
"@next/swc-win32-x64-msvc@13.4.10":
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.10.tgz#44dd9eea943ed14a1012edd5011b8e905f5e6fc4"
|
||||
integrity sha512-g2+tU63yTWmcVQKDGY0MV1PjjqgZtwM4rB1oVVi/v0brdZAcrcTV+04agKzWtvWroyFz6IqtT0MoZJA7PNyLVw==
|
||||
"@next/swc-win32-x64-msvc@13.4.9":
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b"
|
||||
integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw==
|
||||
|
||||
"@noble/curves@1.1.0", "@noble/curves@~1.1.0":
|
||||
version "1.1.0"
|
||||
@ -7597,12 +7597,12 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next@^13.4.10:
|
||||
version "13.4.10"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-13.4.10.tgz#a5b50696759c61663d5a1dd726995fa0576a382e"
|
||||
integrity sha512-4ep6aKxVTQ7rkUW2fBLhpBr/5oceCuf4KmlUpvG/aXuDTIf9mexNSpabUD6RWPspu6wiJJvozZREhXhueYO36A==
|
||||
next@13.4.9:
|
||||
version "13.4.9"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba"
|
||||
integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA==
|
||||
dependencies:
|
||||
"@next/env" "13.4.10"
|
||||
"@next/env" "13.4.9"
|
||||
"@swc/helpers" "0.5.1"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001406"
|
||||
@ -7611,15 +7611,15 @@ next@^13.4.10:
|
||||
watchpack "2.4.0"
|
||||
zod "3.21.4"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "13.4.10"
|
||||
"@next/swc-darwin-x64" "13.4.10"
|
||||
"@next/swc-linux-arm64-gnu" "13.4.10"
|
||||
"@next/swc-linux-arm64-musl" "13.4.10"
|
||||
"@next/swc-linux-x64-gnu" "13.4.10"
|
||||
"@next/swc-linux-x64-musl" "13.4.10"
|
||||
"@next/swc-win32-arm64-msvc" "13.4.10"
|
||||
"@next/swc-win32-ia32-msvc" "13.4.10"
|
||||
"@next/swc-win32-x64-msvc" "13.4.10"
|
||||
"@next/swc-darwin-arm64" "13.4.9"
|
||||
"@next/swc-darwin-x64" "13.4.9"
|
||||
"@next/swc-linux-arm64-gnu" "13.4.9"
|
||||
"@next/swc-linux-arm64-musl" "13.4.9"
|
||||
"@next/swc-linux-x64-gnu" "13.4.9"
|
||||
"@next/swc-linux-x64-musl" "13.4.9"
|
||||
"@next/swc-win32-arm64-msvc" "13.4.9"
|
||||
"@next/swc-win32-ia32-msvc" "13.4.9"
|
||||
"@next/swc-win32-x64-msvc" "13.4.9"
|
||||
|
||||
no-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
|
Loading…
Reference in New Issue
Block a user