allow setting the amount out in jupiter swap
This commit is contained in:
parent
c5c88ce3c2
commit
41223e846c
|
@ -6,7 +6,7 @@ import MultiSelectDropdown from '@components/forms/MultiSelectDropdown'
|
|||
import { EXPLORERS } from '@components/settings/PreferredExplorerSettings'
|
||||
import Button, { IconButton } from '@components/shared/Button'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { Disclosure } from '@headlessui/react'
|
||||
import {
|
||||
AdjustmentsVerticalIcon,
|
||||
ArrowLeftIcon,
|
||||
|
@ -244,37 +244,26 @@ const ActivityFilters = ({
|
|||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={showMobileFilters}
|
||||
enter="transition-all ease-in duration-300"
|
||||
enterFrom="opacity-100 max-h-0"
|
||||
enterTo="opacity-100 max-h-full"
|
||||
leave="transition-all ease-out duration-300"
|
||||
leaveFrom="opacity-100 max-h-full"
|
||||
leaveTo="opacity-0 max-h-0"
|
||||
>
|
||||
<Disclosure.Panel className="bg-th-bkg-2 px-6 pb-6">
|
||||
<div className="py-4">
|
||||
<Label text={t('activity:activity-type')} />
|
||||
<ActivityTypeFiltersForm
|
||||
filters={filters}
|
||||
updateFilters={updateFilters}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedFiltersForm
|
||||
advancedFilters={advancedFilters}
|
||||
setAdvancedFilters={setAdvancedFilters}
|
||||
<Disclosure.Panel className="bg-th-bkg-2 px-6 pb-6">
|
||||
<div className="py-4">
|
||||
<Label text={t('activity:activity-type')} />
|
||||
<ActivityTypeFiltersForm
|
||||
filters={filters}
|
||||
updateFilters={updateFilters}
|
||||
/>
|
||||
<Button
|
||||
className="w-full md:w-auto"
|
||||
size="large"
|
||||
onClick={handleUpdateMobileResults}
|
||||
>
|
||||
{t('activity:update')}
|
||||
</Button>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
<AdvancedFiltersForm
|
||||
advancedFilters={advancedFilters}
|
||||
setAdvancedFilters={setAdvancedFilters}
|
||||
/>
|
||||
<Button
|
||||
className="w-full md:w-auto"
|
||||
size="large"
|
||||
onClick={handleUpdateMobileResults}
|
||||
>
|
||||
{t('activity:update')}
|
||||
</Button>
|
||||
</Disclosure.Panel>
|
||||
</Disclosure>
|
||||
) : null
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useViewport } from 'hooks/useViewport'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
import Image from 'next/legacy/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
floorToDecimal,
|
||||
formatDecimal,
|
||||
|
@ -203,61 +203,87 @@ const Balance = ({ bank }: { bank: Bank }) => {
|
|||
const { selectedMarket } = useSelectedMarket()
|
||||
const { asPath } = useRouter()
|
||||
|
||||
const handleBalanceClick = (balance: number, type: 'base' | 'quote') => {
|
||||
const handleTradeFormBalanceClick = useCallback(
|
||||
(balance: number, type: 'base' | 'quote') => {
|
||||
const set = mangoStore.getState().set
|
||||
const group = mangoStore.getState().group
|
||||
const tradeForm = mangoStore.getState().tradeForm
|
||||
|
||||
if (!group || !selectedMarket) return
|
||||
|
||||
let price: number
|
||||
if (tradeForm.tradeType === 'Market') {
|
||||
const orderbook = mangoStore.getState().selectedMarket.orderbook
|
||||
const side =
|
||||
(balance > 0 && type === 'quote') || (balance < 0 && type === 'base')
|
||||
? 'buy'
|
||||
: 'sell'
|
||||
price = calculateMarketPrice(orderbook, balance, side)
|
||||
} else {
|
||||
price = new Decimal(tradeForm.price).toNumber()
|
||||
}
|
||||
|
||||
let minOrderDecimals: number
|
||||
let tickSize: number
|
||||
if (selectedMarket instanceof Serum3Market) {
|
||||
const market = group.getSerum3ExternalMarket(
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
minOrderDecimals = getDecimalCount(market.minOrderSize)
|
||||
tickSize = getDecimalCount(market.tickSize)
|
||||
} else {
|
||||
minOrderDecimals = getDecimalCount(selectedMarket.minOrderSize)
|
||||
tickSize = getDecimalCount(selectedMarket.tickSize)
|
||||
}
|
||||
|
||||
if (type === 'quote') {
|
||||
const trimmedBalance = trimDecimals(balance, tickSize)
|
||||
const baseSize = trimDecimals(trimmedBalance / price, minOrderDecimals)
|
||||
const quoteSize = trimDecimals(baseSize * price, tickSize)
|
||||
set((s) => {
|
||||
s.tradeForm.baseSize = baseSize.toString()
|
||||
s.tradeForm.quoteSize = quoteSize.toString()
|
||||
})
|
||||
} else {
|
||||
const baseSize = trimDecimals(balance, minOrderDecimals)
|
||||
const quoteSize = trimDecimals(baseSize * price, tickSize)
|
||||
set((s) => {
|
||||
s.tradeForm.baseSize = baseSize.toString()
|
||||
s.tradeForm.quoteSize = quoteSize.toString()
|
||||
})
|
||||
}
|
||||
},
|
||||
[selectedMarket]
|
||||
)
|
||||
|
||||
const handleSwapFormBalanceClick = useCallback((balance: number) => {
|
||||
const set = mangoStore.getState().set
|
||||
const group = mangoStore.getState().group
|
||||
const tradeForm = mangoStore.getState().tradeForm
|
||||
|
||||
if (!group || !selectedMarket) return
|
||||
|
||||
let price: number
|
||||
if (tradeForm.tradeType === 'Market') {
|
||||
const orderbook = mangoStore.getState().selectedMarket.orderbook
|
||||
const side =
|
||||
(balance > 0 && type === 'quote') || (balance < 0 && type === 'base')
|
||||
? 'buy'
|
||||
: 'sell'
|
||||
price = calculateMarketPrice(orderbook, balance, side)
|
||||
} else {
|
||||
price = new Decimal(tradeForm.price).toNumber()
|
||||
}
|
||||
let minOrderDecimals: number
|
||||
let tickSize: number
|
||||
if (selectedMarket instanceof Serum3Market) {
|
||||
const market = group.getSerum3ExternalMarket(
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
minOrderDecimals = getDecimalCount(market.minOrderSize)
|
||||
tickSize = getDecimalCount(market.tickSize)
|
||||
} else {
|
||||
minOrderDecimals = getDecimalCount(selectedMarket.minOrderSize)
|
||||
tickSize = getDecimalCount(selectedMarket.tickSize)
|
||||
}
|
||||
|
||||
if (type === 'quote') {
|
||||
const trimmedBalance = trimDecimals(balance, tickSize)
|
||||
const baseSize = trimDecimals(trimmedBalance / price, minOrderDecimals)
|
||||
const quoteSize = trimDecimals(baseSize * price, tickSize)
|
||||
if (balance >= 0) {
|
||||
set((s) => {
|
||||
s.tradeForm.baseSize = baseSize.toString()
|
||||
s.tradeForm.quoteSize = quoteSize.toString()
|
||||
s.swap.inputBank = bank
|
||||
s.swap.amountIn = balance.toString()
|
||||
s.swap.swapMode = 'ExactIn'
|
||||
})
|
||||
} else {
|
||||
const baseSize = trimDecimals(balance, minOrderDecimals)
|
||||
const quoteSize = trimDecimals(baseSize * price, tickSize)
|
||||
console.log('else')
|
||||
|
||||
set((s) => {
|
||||
s.tradeForm.baseSize = baseSize.toString()
|
||||
s.tradeForm.quoteSize = quoteSize.toString()
|
||||
s.swap.outputBank = bank
|
||||
s.swap.amountOut = Math.abs(balance).toString()
|
||||
s.swap.swapMode = 'ExactOut'
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const balance = useMemo(() => {
|
||||
return mangoAccount ? mangoAccount.getTokenBalanceUi(bank) : 0
|
||||
}, [mangoAccount])
|
||||
}, [bank, mangoAccount])
|
||||
|
||||
const isBaseOrQuote = useMemo(() => {
|
||||
if (selectedMarket instanceof Serum3Market && asPath.includes('/trade')) {
|
||||
if (
|
||||
selectedMarket instanceof Serum3Market &&
|
||||
(asPath.includes('/trade') || asPath.includes('/swap'))
|
||||
) {
|
||||
if (bank.tokenIndex === selectedMarket.baseTokenIndex) {
|
||||
return 'base'
|
||||
} else if (bank.tokenIndex === selectedMarket.quoteTokenIndex) {
|
||||
|
@ -266,18 +292,26 @@ const Balance = ({ bank }: { bank: Bank }) => {
|
|||
}
|
||||
}, [bank, selectedMarket])
|
||||
|
||||
const handleClick = (balance: number, type: 'base' | 'quote') => {
|
||||
if (asPath.includes('/trade')) {
|
||||
handleTradeFormBalanceClick(
|
||||
parseFloat(formatDecimal(balance, bank.mintDecimals)),
|
||||
type
|
||||
)
|
||||
} else {
|
||||
handleSwapFormBalanceClick(
|
||||
parseFloat(formatDecimal(balance, bank.mintDecimals))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="flex justify-end">
|
||||
{balance ? (
|
||||
isBaseOrQuote ? (
|
||||
<LinkButton
|
||||
className="font-normal"
|
||||
onClick={() =>
|
||||
handleBalanceClick(
|
||||
parseFloat(formatDecimal(balance, bank.mintDecimals)),
|
||||
isBaseOrQuote
|
||||
)
|
||||
}
|
||||
onClick={() => handleClick(balance, isBaseOrQuote)}
|
||||
>
|
||||
{formatDecimal(balance, bank.mintDecimals)}
|
||||
</LinkButton>
|
||||
|
|
|
@ -32,7 +32,7 @@ const HealthImpact = ({
|
|||
</p>
|
||||
</Tooltip>
|
||||
<div className="flex items-center space-x-1.5 font-mono">
|
||||
<p className={`text-th-fgd-1 ${small ? 'text-xs' : 'text-sm'}`}>
|
||||
<p className={`text-th-fgd-2 ${small ? 'text-xs' : 'text-sm'}`}>
|
||||
{currentMaintHealth}%
|
||||
</p>
|
||||
<ArrowRightIcon className="h-4 w-4 text-th-fgd-4" />
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import MaxAmountButton from '@components/shared/MaxAmountButton'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useTokenMax } from './useTokenMax'
|
||||
|
||||
const MaxSwapAmount = ({
|
||||
setAmountIn,
|
||||
useMargin,
|
||||
}: {
|
||||
setAmountIn: (x: string) => void
|
||||
useMargin: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad)
|
||||
const {
|
||||
amount: tokenMax,
|
||||
amountWithBorrow,
|
||||
decimals,
|
||||
} = useTokenMax(useMargin)
|
||||
|
||||
if (mangoAccountLoading) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-end pl-6 text-xs">
|
||||
<MaxAmountButton
|
||||
className="mb-0.5"
|
||||
label="Bal"
|
||||
onClick={() => setAmountIn(tokenMax.toFixed(decimals))}
|
||||
value={tokenMax.toFixed()}
|
||||
/>
|
||||
{useMargin ? (
|
||||
<MaxAmountButton
|
||||
className="mb-0.5 ml-2"
|
||||
label={t('max')}
|
||||
onClick={() => setAmountIn(amountWithBorrow.toFixed(decimals))}
|
||||
value={amountWithBorrow.toFixed()}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MaxSwapAmount
|
|
@ -0,0 +1,59 @@
|
|||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import Decimal from 'decimal.js'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { floorToDecimal } from 'utils/numbers'
|
||||
import { useTokenMax } from './useTokenMax'
|
||||
|
||||
const PercentageSelectButtons = ({
|
||||
amountIn,
|
||||
setAmountIn,
|
||||
useMargin,
|
||||
}: {
|
||||
amountIn: string
|
||||
setAmountIn: (x: string) => void
|
||||
useMargin: boolean
|
||||
}) => {
|
||||
const [sizePercentage, setSizePercentage] = useState('')
|
||||
const {
|
||||
amount: tokenMax,
|
||||
amountWithBorrow,
|
||||
decimals,
|
||||
} = useTokenMax(useMargin)
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
if (!tokenMax && !amountWithBorrow) return new Decimal(0)
|
||||
return useMargin ? amountWithBorrow : tokenMax
|
||||
}, [tokenMax, amountWithBorrow, useMargin])
|
||||
|
||||
useEffect(() => {
|
||||
if (maxAmount.gt(0) && amountIn && maxAmount.eq(amountIn)) {
|
||||
setSizePercentage('100')
|
||||
}
|
||||
}, [amountIn, maxAmount])
|
||||
|
||||
const handleSizePercentage = (percentage: string) => {
|
||||
setSizePercentage(percentage)
|
||||
if (maxAmount.gt(0)) {
|
||||
let amount = maxAmount.mul(percentage).div(100)
|
||||
if (percentage !== '100') {
|
||||
amount = floorToDecimal(amount, decimals)
|
||||
}
|
||||
setAmountIn(amount.toFixed())
|
||||
} else {
|
||||
setAmountIn('0')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-span-2 mt-2">
|
||||
<ButtonGroup
|
||||
activeValue={sizePercentage}
|
||||
onChange={(p) => handleSizePercentage(p)}
|
||||
values={['10', '25', '50', '75', '100']}
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PercentageSelectButtons
|
|
@ -7,19 +7,20 @@ import {
|
|||
ExclamationCircleIcon,
|
||||
LinkIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import NumberFormat, { NumberFormatValues } from 'react-number-format'
|
||||
import NumberFormat, {
|
||||
NumberFormatValues,
|
||||
SourceInfo,
|
||||
} from 'react-number-format'
|
||||
import Decimal from 'decimal.js'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import ContentBox from '../shared/ContentBox'
|
||||
import JupiterRouteInfo from './JupiterRouteInfo'
|
||||
import SwapReviewRouteInfo from './SwapReviewRouteInfo'
|
||||
import TokenSelect from './TokenSelect'
|
||||
import useDebounce from '../shared/useDebounce'
|
||||
import { floorToDecimal, numberFormat } from '../../utils/numbers'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import SwapFormTokenList from './SwapFormTokenList'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import Button, { IconButton } from '../shared/Button'
|
||||
import ButtonGroup from '../forms/ButtonGroup'
|
||||
import Loading from '../shared/Loading'
|
||||
import { EnterBottomExitBottom } from '../shared/Transitions'
|
||||
import useJupiterRoutes from './useJupiterRoutes'
|
||||
|
@ -34,7 +35,6 @@ import {
|
|||
USDC_MINT,
|
||||
} from '../../utils/constants'
|
||||
import { useTokenMax } from './useTokenMax'
|
||||
import MaxAmountButton from '@components/shared/MaxAmountButton'
|
||||
import HealthImpact from '@components/shared/HealthImpact'
|
||||
import { useWallet } from '@solana/wallet-adapter-react'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
|
@ -43,6 +43,8 @@ import useMangoGroup from 'hooks/useMangoGroup'
|
|||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import SwapSlider from './SwapSlider'
|
||||
import TokenVaultWarnings from '@components/shared/TokenVaultWarnings'
|
||||
import MaxSwapAmount from './MaxSwapAmount'
|
||||
import PercentageSelectButtons from './PercentageSelectButtons'
|
||||
|
||||
const MAX_DIGITS = 11
|
||||
export const withValueLimit = (values: NumberFormatValues): boolean => {
|
||||
|
@ -54,7 +56,6 @@ export const withValueLimit = (values: NumberFormatValues): boolean => {
|
|||
const SwapForm = () => {
|
||||
const { t } = useTranslation(['common', 'swap'])
|
||||
const [selectedRoute, setSelectedRoute] = useState<RouteInfo>()
|
||||
const [amountInFormValue, setAmountInFormValue] = useState('')
|
||||
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
|
||||
const [showTokenSelect, setShowTokenSelect] = useState('')
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
|
@ -68,77 +69,131 @@ const SwapForm = () => {
|
|||
slippage,
|
||||
inputBank,
|
||||
outputBank,
|
||||
amountIn: amountInFormValue,
|
||||
amountOut: amountOutFormValue,
|
||||
swapMode,
|
||||
} = mangoStore((s) => s.swap)
|
||||
const [debouncedAmountIn] = useDebounce(amountInFormValue, 300)
|
||||
const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300)
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const { connected } = useWallet()
|
||||
|
||||
const amountIn: Decimal | null = useMemo(() => {
|
||||
const amountInAsDecimal: Decimal | null = useMemo(() => {
|
||||
return Number(debouncedAmountIn)
|
||||
? new Decimal(debouncedAmountIn)
|
||||
: new Decimal(0)
|
||||
}, [debouncedAmountIn])
|
||||
|
||||
const amountOutAsDecimal: Decimal | null = useMemo(() => {
|
||||
return Number(debouncedAmountOut)
|
||||
? new Decimal(debouncedAmountOut)
|
||||
: new Decimal(0)
|
||||
}, [debouncedAmountOut])
|
||||
|
||||
const { bestRoute, routes } = useJupiterRoutes({
|
||||
inputMint: inputBank?.mint.toString() || USDC_MINT,
|
||||
outputMint: outputBank?.mint.toString() || MANGO_MINT,
|
||||
inputAmount: debouncedAmountIn,
|
||||
amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut,
|
||||
slippage,
|
||||
swapMode,
|
||||
})
|
||||
|
||||
const outAmount: number = useMemo(() => {
|
||||
return selectedRoute?.outAmount.toString()
|
||||
? new Decimal(selectedRoute.outAmount.toString())
|
||||
.div(10 ** outputBank!.mintDecimals)
|
||||
.toNumber()
|
||||
: 0
|
||||
}, [selectedRoute, outputBank])
|
||||
const setAmountInFormValue = useCallback((amountIn: string) => {
|
||||
set((s) => {
|
||||
s.swap.amountIn = amountIn
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setAmountOutFormValue = useCallback((amountOut: string) => {
|
||||
set((s) => {
|
||||
s.swap.amountOut = amountOut
|
||||
})
|
||||
}, [])
|
||||
|
||||
/*
|
||||
Once a route is returned from the Jupiter API, use the inAmount or outAmount
|
||||
depending on the swapMode and set those values in state
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (bestRoute) {
|
||||
setSelectedRoute(bestRoute)
|
||||
}
|
||||
}, [bestRoute])
|
||||
|
||||
if (inputBank && swapMode === 'ExactOut') {
|
||||
const inAmount = new Decimal(bestRoute.inAmount)
|
||||
.div(10 ** inputBank.mintDecimals)
|
||||
.toString()
|
||||
setAmountInFormValue(inAmount)
|
||||
} else if (outputBank && swapMode === 'ExactIn') {
|
||||
const outAmount = new Decimal(bestRoute.outAmount)
|
||||
.div(10 ** outputBank.mintDecimals)
|
||||
.toString()
|
||||
setAmountOutFormValue(outAmount)
|
||||
}
|
||||
}
|
||||
}, [bestRoute, swapMode, inputBank, outputBank])
|
||||
|
||||
/*
|
||||
If the use margin setting is toggled, clear the form values
|
||||
*/
|
||||
useEffect(() => {
|
||||
setAmountInFormValue('')
|
||||
setAmountOutFormValue('')
|
||||
}, [useMargin])
|
||||
|
||||
const handleAmountInChange = useCallback((e: NumberFormatValues) => {
|
||||
setAmountInFormValue(e.value)
|
||||
const handleAmountInChange = useCallback(
|
||||
(e: NumberFormatValues, info: SourceInfo) => {
|
||||
if (info.source !== 'event') return
|
||||
if (swapMode === 'ExactOut') {
|
||||
set((s) => {
|
||||
s.swap.swapMode = 'ExactIn'
|
||||
})
|
||||
}
|
||||
setAmountInFormValue(e.value)
|
||||
},
|
||||
[swapMode]
|
||||
)
|
||||
|
||||
const handleAmountOutChange = useCallback(
|
||||
(e: NumberFormatValues, info: SourceInfo) => {
|
||||
if (info.source !== 'event') return
|
||||
if (swapMode === 'ExactIn') {
|
||||
set((s) => {
|
||||
s.swap.swapMode = 'ExactOut'
|
||||
})
|
||||
}
|
||||
setAmountOutFormValue(e.value)
|
||||
},
|
||||
[swapMode]
|
||||
)
|
||||
|
||||
const handleTokenInSelect = useCallback((mintAddress: string) => {
|
||||
const group = mangoStore.getState().group
|
||||
if (group) {
|
||||
const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
|
||||
set((s) => {
|
||||
s.swap.inputBank = bank
|
||||
})
|
||||
}
|
||||
setShowTokenSelect('')
|
||||
}, [])
|
||||
|
||||
const handleTokenInSelect = useCallback(
|
||||
(mintAddress: string) => {
|
||||
const group = mangoStore.getState().group
|
||||
if (group) {
|
||||
const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
|
||||
set((s) => {
|
||||
s.swap.inputBank = bank
|
||||
})
|
||||
}
|
||||
setShowTokenSelect('')
|
||||
},
|
||||
[set]
|
||||
)
|
||||
|
||||
const handleTokenOutSelect = useCallback(
|
||||
(mintAddress: string) => {
|
||||
const group = mangoStore.getState().group
|
||||
if (group) {
|
||||
const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
|
||||
set((s) => {
|
||||
s.swap.outputBank = bank
|
||||
})
|
||||
}
|
||||
setShowTokenSelect('')
|
||||
},
|
||||
[set]
|
||||
)
|
||||
const handleTokenOutSelect = useCallback((mintAddress: string) => {
|
||||
const group = mangoStore.getState().group
|
||||
if (group) {
|
||||
const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
|
||||
set((s) => {
|
||||
s.swap.outputBank = bank
|
||||
})
|
||||
}
|
||||
setShowTokenSelect('')
|
||||
}, [])
|
||||
|
||||
const handleSwitchTokens = useCallback(() => {
|
||||
if (amountIn?.gt(0) && outAmount) {
|
||||
setAmountInFormValue(outAmount.toString())
|
||||
if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) {
|
||||
setAmountInFormValue(amountOutAsDecimal.toString())
|
||||
set((s) => {
|
||||
s.swap.swapMode = 'ExactIn'
|
||||
})
|
||||
}
|
||||
const inputBank = mangoStore.getState().swap.inputBank
|
||||
const outputBank = mangoStore.getState().swap.outputBank
|
||||
|
@ -149,11 +204,17 @@ const SwapForm = () => {
|
|||
setAnimateSwitchArrow(
|
||||
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1
|
||||
)
|
||||
}, [set, outAmount, amountIn])
|
||||
}, [set, amountOutAsDecimal, amountInAsDecimal])
|
||||
|
||||
const maintProjectedHealth = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!inputBank || !mangoAccount || !outputBank || !outAmount || !group)
|
||||
if (
|
||||
!inputBank ||
|
||||
!mangoAccount ||
|
||||
!outputBank ||
|
||||
!amountOutAsDecimal ||
|
||||
!group
|
||||
)
|
||||
return 0
|
||||
|
||||
const simulatedHealthRatio =
|
||||
|
@ -162,25 +223,31 @@ const SwapForm = () => {
|
|||
[
|
||||
{
|
||||
mintPk: inputBank.mint,
|
||||
uiTokenAmount: amountIn.toNumber() * -1,
|
||||
uiTokenAmount: amountInAsDecimal.toNumber() * -1,
|
||||
},
|
||||
{
|
||||
mintPk: outputBank.mint,
|
||||
uiTokenAmount: outAmount,
|
||||
uiTokenAmount: amountInAsDecimal.toNumber(),
|
||||
},
|
||||
],
|
||||
HealthType.maint
|
||||
)
|
||||
return simulatedHealthRatio! > 100
|
||||
return simulatedHealthRatio > 100
|
||||
? 100
|
||||
: simulatedHealthRatio! < 0
|
||||
: simulatedHealthRatio < 0
|
||||
? 0
|
||||
: Math.trunc(simulatedHealthRatio!)
|
||||
}, [mangoAccount, inputBank, outputBank, amountIn, outAmount])
|
||||
: Math.trunc(simulatedHealthRatio)
|
||||
}, [
|
||||
mangoAccount,
|
||||
inputBank,
|
||||
outputBank,
|
||||
amountInAsDecimal,
|
||||
amountOutAsDecimal,
|
||||
])
|
||||
|
||||
const loadingSwapDetails: boolean = useMemo(() => {
|
||||
return !!amountIn.toNumber() && connected && !selectedRoute
|
||||
}, [amountIn, connected, selectedRoute])
|
||||
return !!amountInAsDecimal.toNumber() && connected && !selectedRoute
|
||||
}, [amountInAsDecimal, connected, selectedRoute])
|
||||
|
||||
return (
|
||||
<ContentBox
|
||||
|
@ -199,9 +266,9 @@ const SwapForm = () => {
|
|||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<JupiterRouteInfo
|
||||
<SwapReviewRouteInfo
|
||||
onClose={() => setShowConfirm(false)}
|
||||
amountIn={amountIn}
|
||||
amountIn={amountInAsDecimal}
|
||||
slippage={slippage}
|
||||
routes={routes}
|
||||
selectedRoute={selectedRoute}
|
||||
|
@ -252,7 +319,7 @@ const SwapForm = () => {
|
|||
<div className="col-span-1 rounded-lg rounded-r-none border border-r-0 border-th-bkg-4 bg-th-bkg-1">
|
||||
<TokenSelect
|
||||
bank={
|
||||
inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)![0]
|
||||
inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)?.[0]
|
||||
}
|
||||
showTokenList={setShowTokenSelect}
|
||||
type="input"
|
||||
|
@ -296,13 +363,13 @@ const SwapForm = () => {
|
|||
<TokenSelect
|
||||
bank={
|
||||
outputBank ||
|
||||
group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)![0]
|
||||
group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)?.[0]
|
||||
}
|
||||
showTokenList={setShowTokenSelect}
|
||||
type="output"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-[54px] w-full items-center justify-end rounded-r-lg border border-th-bkg-4 bg-th-bkg-3 text-right text-lg font-bold text-th-fgd-3 xl:text-xl">
|
||||
<div className="flex h-[54px] w-full items-center justify-end rounded-r-lg border border-th-bkg-4 text-right text-lg font-bold text-th-fgd-3 xl:text-xl">
|
||||
{loadingSwapDetails ? (
|
||||
<div className="w-full">
|
||||
<SheenLoader className="flex flex-1 rounded-l-none">
|
||||
|
@ -318,10 +385,10 @@ const SwapForm = () => {
|
|||
decimalScale={outputBank?.mintDecimals || 6}
|
||||
name="amountOut"
|
||||
id="amountOut"
|
||||
className="w-full rounded-r-lg bg-th-bkg-3 p-3 text-right font-mono text-base font-bold text-th-fgd-3 focus:outline-none lg:text-lg xl:text-xl"
|
||||
className="w-full rounded-r-lg bg-th-bkg-1 p-3 text-right font-mono text-base font-bold text-th-fgd-3 focus:outline-none lg:text-lg xl:text-xl"
|
||||
placeholder="0.00"
|
||||
disabled
|
||||
value={outAmount ? numberFormat.format(outAmount) : ''}
|
||||
value={amountOutFormValue}
|
||||
onValueChange={handleAmountOutChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -329,13 +396,13 @@ const SwapForm = () => {
|
|||
{swapFormSizeUi === 'Slider' ? (
|
||||
<SwapSlider
|
||||
useMargin={useMargin}
|
||||
amount={amountIn.toNumber()}
|
||||
amount={amountInAsDecimal.toNumber()}
|
||||
onChange={setAmountInFormValue}
|
||||
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
|
||||
/>
|
||||
) : (
|
||||
<PercentageSelectButtons
|
||||
amountIn={amountIn.toString()}
|
||||
amountIn={amountInAsDecimal.toString()}
|
||||
setAmountIn={setAmountInFormValue}
|
||||
useMargin={useMargin}
|
||||
/>
|
||||
|
@ -344,25 +411,30 @@ const SwapForm = () => {
|
|||
loadingSwapDetails={loadingSwapDetails}
|
||||
useMargin={useMargin}
|
||||
setShowConfirm={setShowConfirm}
|
||||
amountIn={amountIn}
|
||||
amountIn={amountInAsDecimal}
|
||||
inputSymbol={inputBank?.name}
|
||||
amountOut={selectedRoute ? outAmount : undefined}
|
||||
amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
|
||||
/>
|
||||
{group ? (
|
||||
{group && inputBank ? (
|
||||
<div className="pt-4">
|
||||
<TokenVaultWarnings
|
||||
bank={
|
||||
inputBank || group.banksMapByName.get(INPUT_TOKEN_DEFAULT)![0]
|
||||
}
|
||||
/>
|
||||
<TokenVaultWarnings bank={inputBank} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
id="swap-step-four"
|
||||
className={`border-t border-th-bkg-3 px-6 py-4 transition-all`}
|
||||
>
|
||||
<HealthImpact maintProjectedHealth={maintProjectedHealth} />
|
||||
<div id="swap-step-four" className={`px-2 py-4`}>
|
||||
<HealthImpact maintProjectedHealth={maintProjectedHealth} />
|
||||
</div>
|
||||
<div className={`px-2`}>
|
||||
<div className="flex justify-between">
|
||||
<p className="text-sm text-th-fgd-3">Est. {t('swap:slippage')}</p>
|
||||
<p className="text-right font-mono text-sm text-th-fgd-3">
|
||||
{selectedRoute?.priceImpactPct
|
||||
? selectedRoute?.priceImpactPct * 100 < 0.1
|
||||
? '< 0.1%'
|
||||
: `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%`
|
||||
: '0.00%'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContentBox>
|
||||
)
|
||||
|
@ -382,7 +454,7 @@ const SwapFormSubmitButton = ({
|
|||
amountOut: number | undefined
|
||||
inputSymbol: string | undefined
|
||||
loadingSwapDetails: boolean
|
||||
setShowConfirm: (x: any) => any
|
||||
setShowConfirm: (x: boolean) => void
|
||||
useMargin: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
|
@ -433,92 +505,3 @@ const SwapFormSubmitButton = ({
|
|||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const MaxSwapAmount = ({
|
||||
setAmountIn,
|
||||
useMargin,
|
||||
}: {
|
||||
setAmountIn: (x: string) => void
|
||||
useMargin: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad)
|
||||
const {
|
||||
amount: tokenMax,
|
||||
amountWithBorrow,
|
||||
decimals,
|
||||
} = useTokenMax(useMargin)
|
||||
|
||||
if (mangoAccountLoading) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-end pl-6 text-xs">
|
||||
<MaxAmountButton
|
||||
className="mb-0.5"
|
||||
label="Bal"
|
||||
onClick={() => setAmountIn(tokenMax.toFixed(decimals))}
|
||||
value={tokenMax.toFixed()}
|
||||
/>
|
||||
{useMargin ? (
|
||||
<MaxAmountButton
|
||||
className="mb-0.5 ml-2"
|
||||
label={t('max')}
|
||||
onClick={() => setAmountIn(amountWithBorrow.toFixed(decimals))}
|
||||
value={amountWithBorrow.toFixed()}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PercentageSelectButtons = ({
|
||||
amountIn,
|
||||
setAmountIn,
|
||||
useMargin,
|
||||
}: {
|
||||
amountIn: string
|
||||
setAmountIn: (x: string) => any
|
||||
useMargin: boolean
|
||||
}) => {
|
||||
const [sizePercentage, setSizePercentage] = useState('')
|
||||
const {
|
||||
amount: tokenMax,
|
||||
amountWithBorrow,
|
||||
decimals,
|
||||
} = useTokenMax(useMargin)
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
if (!tokenMax && !amountWithBorrow) return new Decimal(0)
|
||||
return useMargin ? amountWithBorrow : tokenMax
|
||||
}, [tokenMax, amountWithBorrow, useMargin])
|
||||
|
||||
useEffect(() => {
|
||||
if (maxAmount.gt(0) && amountIn && maxAmount.eq(amountIn)) {
|
||||
setSizePercentage('100')
|
||||
}
|
||||
}, [amountIn, maxAmount])
|
||||
|
||||
const handleSizePercentage = (percentage: string) => {
|
||||
setSizePercentage(percentage)
|
||||
if (maxAmount.gt(0)) {
|
||||
let amount = maxAmount.mul(percentage).div(100)
|
||||
if (percentage !== '100') {
|
||||
amount = floorToDecimal(amount, decimals)
|
||||
}
|
||||
setAmountIn(amount.toFixed())
|
||||
} else {
|
||||
setAmountIn('0')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-span-2 mt-2">
|
||||
<ButtonGroup
|
||||
activeValue={sizePercentage}
|
||||
onChange={(p) => handleSizePercentage(p)}
|
||||
values={['10', '25', '50', '75', '100']}
|
||||
unit="%"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ const EMPTY_COINGECKO_PRICES = {
|
|||
outputCoingeckoPrice: 0,
|
||||
}
|
||||
|
||||
const JupiterRouteInfo = ({
|
||||
const SwapReviewRouteInfo = ({
|
||||
amountIn,
|
||||
onClose,
|
||||
routes,
|
||||
|
@ -570,4 +570,4 @@ const JupiterRouteInfo = ({
|
|||
) : null
|
||||
}
|
||||
|
||||
export default React.memo(JupiterRouteInfo)
|
||||
export default React.memo(SwapReviewRouteInfo)
|
|
@ -38,7 +38,7 @@ const SwapSettings = ({ onClose }: { onClose: () => void }) => {
|
|||
</IconButton>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="mb-2 text-th-fgd-1">{t('swap:slippage')}</p>
|
||||
<p className="mb-2 text-th-fgd-1">Max {t('swap:slippage')}</p>
|
||||
{showCustomSlippageForm ? (
|
||||
<Input
|
||||
type="text"
|
||||
|
|
|
@ -6,8 +6,9 @@ import useJupiterSwapData from './useJupiterSwapData'
|
|||
type useJupiterPropTypes = {
|
||||
inputMint: string
|
||||
outputMint: string
|
||||
inputAmount: string
|
||||
amount: string
|
||||
slippage: number
|
||||
swapMode: string
|
||||
}
|
||||
|
||||
const fetchJupiterRoutes = async (
|
||||
|
@ -15,19 +16,20 @@ const fetchJupiterRoutes = async (
|
|||
outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
amount = 0,
|
||||
slippage = 50,
|
||||
swapMode = 'ExactIn',
|
||||
feeBps = 0
|
||||
) => {
|
||||
{
|
||||
const params: any = {
|
||||
const paramsString = new URLSearchParams({
|
||||
inputMint: inputMint.toString(),
|
||||
outputMint: outputMint.toString(),
|
||||
amount,
|
||||
slippageBps: Math.ceil(slippage * 100),
|
||||
amount: amount.toString(),
|
||||
slippageBps: Math.ceil(slippage * 100).toString(),
|
||||
onlyDirectRoutes: 'true',
|
||||
feeBps,
|
||||
}
|
||||
feeBps: feeBps.toString(),
|
||||
swapMode,
|
||||
}).toString()
|
||||
|
||||
const paramsString = new URLSearchParams(params).toString()
|
||||
const response = await fetch(
|
||||
`https://quote-api.jup.ag/v3/quote?${paramsString}`
|
||||
)
|
||||
|
@ -45,25 +47,39 @@ const fetchJupiterRoutes = async (
|
|||
const useJupiterRoutes = ({
|
||||
inputMint,
|
||||
outputMint,
|
||||
inputAmount,
|
||||
amount,
|
||||
slippage,
|
||||
swapMode,
|
||||
}: useJupiterPropTypes) => {
|
||||
const { inputTokenInfo } = useJupiterSwapData()
|
||||
console.log('amount: ', amount)
|
||||
|
||||
const amount = inputAmount
|
||||
? new Decimal(inputAmount).mul(10 ** (inputTokenInfo?.decimals || 6))
|
||||
const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
|
||||
|
||||
const decimals =
|
||||
swapMode === 'ExactIn'
|
||||
? inputTokenInfo?.decimals || 6
|
||||
: outputTokenInfo?.decimals || 6
|
||||
|
||||
const nativeAmount = amount
|
||||
? new Decimal(amount).mul(10 ** decimals)
|
||||
: new Decimal(0)
|
||||
|
||||
const res = useQuery<{ routes: RouteInfo[]; bestRoute: RouteInfo }, Error>(
|
||||
['swap-routes', inputMint, outputMint, inputAmount, slippage],
|
||||
['swap-routes', inputMint, outputMint, amount, slippage, swapMode],
|
||||
async () =>
|
||||
fetchJupiterRoutes(inputMint, outputMint, amount.toNumber(), slippage),
|
||||
fetchJupiterRoutes(
|
||||
inputMint,
|
||||
outputMint,
|
||||
nativeAmount.toNumber(),
|
||||
slippage,
|
||||
swapMode
|
||||
),
|
||||
{
|
||||
enabled: inputAmount ? true : false,
|
||||
enabled: amount ? true : false,
|
||||
}
|
||||
)
|
||||
|
||||
return inputAmount
|
||||
return amount
|
||||
? {
|
||||
...(res.data ?? {
|
||||
routes: [],
|
||||
|
|
|
@ -54,6 +54,13 @@ const PerpFundingRate = () => {
|
|||
: '-'}
|
||||
%
|
||||
</div>
|
||||
{/* <div className="font-mono text-xs text-th-fgd-2">
|
||||
{selectedMarket instanceof PerpMarket &&
|
||||
bids instanceof BookSide &&
|
||||
asks instanceof BookSide
|
||||
? selectedMarket.getCurrentFundingRate(bids, asks)
|
||||
: '-'}
|
||||
</div> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-v4": "https://tylersssss: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@github.com/blockworks-foundation/mango-v4.git#main",
|
||||
"@blockworks-foundation/mango-v4": "https://tylersssss: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@github.com/blockworks-foundation/mango-v4.git#ts/test",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@heroicons/react": "^2.0.10",
|
||||
"@project-serum/anchor": "0.25.0",
|
||||
|
|
|
@ -37,7 +37,6 @@ import { Orderbook, SpotBalances } from 'types'
|
|||
import spotBalancesUpdater from './spotBalancesUpdater'
|
||||
import { PerpMarket } from '@blockworks-foundation/mango-v4/'
|
||||
import perpPositionsUpdater from './perpPositionsUpdater'
|
||||
import { token } from '@project-serum/anchor/dist/cjs/utils'
|
||||
|
||||
const GROUP = new PublicKey('DLdcpC6AsAJ9xeKMR3WhHrN5sM5o7GVVXQhQ5vwisTtz')
|
||||
|
||||
|
@ -232,6 +231,9 @@ export type MangoStore = {
|
|||
margin: boolean
|
||||
slippage: number
|
||||
success: boolean
|
||||
swapMode: 'ExactIn' | 'ExactOut'
|
||||
amountIn: string
|
||||
amountOut: string
|
||||
}
|
||||
set: (x: (x: MangoStore) => void) => void
|
||||
tokenStats: {
|
||||
|
@ -342,6 +344,18 @@ const mangoStore = create<MangoStore>()(
|
|||
},
|
||||
uiLocked: true,
|
||||
},
|
||||
swap: {
|
||||
inputBank: undefined,
|
||||
outputBank: undefined,
|
||||
inputTokenInfo: undefined,
|
||||
outputTokenInfo: undefined,
|
||||
margin: true,
|
||||
slippage: 0.5,
|
||||
success: false,
|
||||
swapMode: 'ExactIn',
|
||||
amountIn: '',
|
||||
amountOut: '',
|
||||
},
|
||||
tokenStats: {
|
||||
loading: false,
|
||||
data: [],
|
||||
|
@ -355,15 +369,6 @@ const mangoStore = create<MangoStore>()(
|
|||
postOnly: false,
|
||||
ioc: false,
|
||||
},
|
||||
swap: {
|
||||
inputBank: undefined,
|
||||
outputBank: undefined,
|
||||
inputTokenInfo: undefined,
|
||||
outputTokenInfo: undefined,
|
||||
margin: true,
|
||||
slippage: 0.5,
|
||||
success: false,
|
||||
},
|
||||
wallet: {
|
||||
tokens: [],
|
||||
nfts: {
|
||||
|
|
|
@ -50,9 +50,9 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@blockworks-foundation/mango-v4@https://tylersssss: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@github.com/blockworks-foundation/mango-v4.git#main":
|
||||
"@blockworks-foundation/mango-v4@https://tylersssss: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@github.com/blockworks-foundation/mango-v4.git#ts/test":
|
||||
version "0.0.1-beta.6"
|
||||
resolved "https://tylersssss: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@github.com/blockworks-foundation/mango-v4.git#0609adbe702ba68c11f02d317d79eae356dc47b9"
|
||||
resolved "https://tylersssss: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@github.com/blockworks-foundation/mango-v4.git#c54fdbee5cc7be5e9d5367996b7986e370e609ff"
|
||||
dependencies:
|
||||
"@project-serum/anchor" "^0.25.0"
|
||||
"@project-serum/serum" "^0.13.65"
|
||||
|
|
Loading…
Reference in New Issue