allow setting the amount out in jupiter swap

This commit is contained in:
tjs 2022-12-07 16:33:35 -05:00
parent c5c88ce3c2
commit 41223e846c
13 changed files with 421 additions and 285 deletions

View File

@ -6,7 +6,7 @@ import MultiSelectDropdown from '@components/forms/MultiSelectDropdown'
import { EXPLORERS } from '@components/settings/PreferredExplorerSettings' import { EXPLORERS } from '@components/settings/PreferredExplorerSettings'
import Button, { IconButton } from '@components/shared/Button' import Button, { IconButton } from '@components/shared/Button'
import Tooltip from '@components/shared/Tooltip' import Tooltip from '@components/shared/Tooltip'
import { Disclosure, Transition } from '@headlessui/react' import { Disclosure } from '@headlessui/react'
import { import {
AdjustmentsVerticalIcon, AdjustmentsVerticalIcon,
ArrowLeftIcon, ArrowLeftIcon,
@ -244,37 +244,26 @@ const ActivityFilters = ({
</Disclosure.Button> </Disclosure.Button>
</div> </div>
</div> </div>
<Transition <Disclosure.Panel className="bg-th-bkg-2 px-6 pb-6">
appear={true} <div className="py-4">
show={showMobileFilters} <Label text={t('activity:activity-type')} />
enter="transition-all ease-in duration-300" <ActivityTypeFiltersForm
enterFrom="opacity-100 max-h-0" filters={filters}
enterTo="opacity-100 max-h-full" updateFilters={updateFilters}
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}
/> />
<Button </div>
className="w-full md:w-auto" <AdvancedFiltersForm
size="large" advancedFilters={advancedFilters}
onClick={handleUpdateMobileResults} setAdvancedFilters={setAdvancedFilters}
> />
{t('activity:update')} <Button
</Button> className="w-full md:w-auto"
</Disclosure.Panel> size="large"
</Transition> onClick={handleUpdateMobileResults}
>
{t('activity:update')}
</Button>
</Disclosure.Panel>
</Disclosure> </Disclosure>
) : null ) : null
} }

View File

@ -8,7 +8,7 @@ import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import Image from 'next/legacy/image' import Image from 'next/legacy/image'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { import {
floorToDecimal, floorToDecimal,
formatDecimal, formatDecimal,
@ -203,61 +203,87 @@ const Balance = ({ bank }: { bank: Bank }) => {
const { selectedMarket } = useSelectedMarket() const { selectedMarket } = useSelectedMarket()
const { asPath } = useRouter() 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 set = mangoStore.getState().set
const group = mangoStore.getState().group if (balance >= 0) {
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) => { set((s) => {
s.tradeForm.baseSize = baseSize.toString() s.swap.inputBank = bank
s.tradeForm.quoteSize = quoteSize.toString() s.swap.amountIn = balance.toString()
s.swap.swapMode = 'ExactIn'
}) })
} else { } else {
const baseSize = trimDecimals(balance, minOrderDecimals) console.log('else')
const quoteSize = trimDecimals(baseSize * price, tickSize)
set((s) => { set((s) => {
s.tradeForm.baseSize = baseSize.toString() s.swap.outputBank = bank
s.tradeForm.quoteSize = quoteSize.toString() s.swap.amountOut = Math.abs(balance).toString()
s.swap.swapMode = 'ExactOut'
}) })
} }
} }, [])
const balance = useMemo(() => { const balance = useMemo(() => {
return mangoAccount ? mangoAccount.getTokenBalanceUi(bank) : 0 return mangoAccount ? mangoAccount.getTokenBalanceUi(bank) : 0
}, [mangoAccount]) }, [bank, mangoAccount])
const isBaseOrQuote = useMemo(() => { 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) { if (bank.tokenIndex === selectedMarket.baseTokenIndex) {
return 'base' return 'base'
} else if (bank.tokenIndex === selectedMarket.quoteTokenIndex) { } else if (bank.tokenIndex === selectedMarket.quoteTokenIndex) {
@ -266,18 +292,26 @@ const Balance = ({ bank }: { bank: Bank }) => {
} }
}, [bank, selectedMarket]) }, [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 ( return (
<p className="flex justify-end"> <p className="flex justify-end">
{balance ? ( {balance ? (
isBaseOrQuote ? ( isBaseOrQuote ? (
<LinkButton <LinkButton
className="font-normal" className="font-normal"
onClick={() => onClick={() => handleClick(balance, isBaseOrQuote)}
handleBalanceClick(
parseFloat(formatDecimal(balance, bank.mintDecimals)),
isBaseOrQuote
)
}
> >
{formatDecimal(balance, bank.mintDecimals)} {formatDecimal(balance, bank.mintDecimals)}
</LinkButton> </LinkButton>

View File

@ -32,7 +32,7 @@ const HealthImpact = ({
</p> </p>
</Tooltip> </Tooltip>
<div className="flex items-center space-x-1.5 font-mono"> <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}% {currentMaintHealth}%
</p> </p>
<ArrowRightIcon className="h-4 w-4 text-th-fgd-4" /> <ArrowRightIcon className="h-4 w-4 text-th-fgd-4" />

View File

@ -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

View File

@ -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

View File

@ -7,19 +7,20 @@ import {
ExclamationCircleIcon, ExclamationCircleIcon,
LinkIcon, LinkIcon,
} from '@heroicons/react/20/solid' } 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 Decimal from 'decimal.js'
import mangoStore from '@store/mangoStore' import mangoStore from '@store/mangoStore'
import ContentBox from '../shared/ContentBox' import ContentBox from '../shared/ContentBox'
import JupiterRouteInfo from './JupiterRouteInfo' import SwapReviewRouteInfo from './SwapReviewRouteInfo'
import TokenSelect from './TokenSelect' import TokenSelect from './TokenSelect'
import useDebounce from '../shared/useDebounce' import useDebounce from '../shared/useDebounce'
import { floorToDecimal, numberFormat } from '../../utils/numbers'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import SwapFormTokenList from './SwapFormTokenList' import SwapFormTokenList from './SwapFormTokenList'
import { Transition } from '@headlessui/react' import { Transition } from '@headlessui/react'
import Button, { IconButton } from '../shared/Button' import Button, { IconButton } from '../shared/Button'
import ButtonGroup from '../forms/ButtonGroup'
import Loading from '../shared/Loading' import Loading from '../shared/Loading'
import { EnterBottomExitBottom } from '../shared/Transitions' import { EnterBottomExitBottom } from '../shared/Transitions'
import useJupiterRoutes from './useJupiterRoutes' import useJupiterRoutes from './useJupiterRoutes'
@ -34,7 +35,6 @@ import {
USDC_MINT, USDC_MINT,
} from '../../utils/constants' } from '../../utils/constants'
import { useTokenMax } from './useTokenMax' import { useTokenMax } from './useTokenMax'
import MaxAmountButton from '@components/shared/MaxAmountButton'
import HealthImpact from '@components/shared/HealthImpact' import HealthImpact from '@components/shared/HealthImpact'
import { useWallet } from '@solana/wallet-adapter-react' import { useWallet } from '@solana/wallet-adapter-react'
import useMangoAccount from 'hooks/useMangoAccount' import useMangoAccount from 'hooks/useMangoAccount'
@ -43,6 +43,8 @@ import useMangoGroup from 'hooks/useMangoGroup'
import useLocalStorageState from 'hooks/useLocalStorageState' import useLocalStorageState from 'hooks/useLocalStorageState'
import SwapSlider from './SwapSlider' import SwapSlider from './SwapSlider'
import TokenVaultWarnings from '@components/shared/TokenVaultWarnings' import TokenVaultWarnings from '@components/shared/TokenVaultWarnings'
import MaxSwapAmount from './MaxSwapAmount'
import PercentageSelectButtons from './PercentageSelectButtons'
const MAX_DIGITS = 11 const MAX_DIGITS = 11
export const withValueLimit = (values: NumberFormatValues): boolean => { export const withValueLimit = (values: NumberFormatValues): boolean => {
@ -54,7 +56,6 @@ export const withValueLimit = (values: NumberFormatValues): boolean => {
const SwapForm = () => { const SwapForm = () => {
const { t } = useTranslation(['common', 'swap']) const { t } = useTranslation(['common', 'swap'])
const [selectedRoute, setSelectedRoute] = useState<RouteInfo>() const [selectedRoute, setSelectedRoute] = useState<RouteInfo>()
const [amountInFormValue, setAmountInFormValue] = useState('')
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
const [showTokenSelect, setShowTokenSelect] = useState('') const [showTokenSelect, setShowTokenSelect] = useState('')
const [showSettings, setShowSettings] = useState(false) const [showSettings, setShowSettings] = useState(false)
@ -68,77 +69,131 @@ const SwapForm = () => {
slippage, slippage,
inputBank, inputBank,
outputBank, outputBank,
amountIn: amountInFormValue,
amountOut: amountOutFormValue,
swapMode,
} = mangoStore((s) => s.swap) } = mangoStore((s) => s.swap)
const [debouncedAmountIn] = useDebounce(amountInFormValue, 300) const [debouncedAmountIn] = useDebounce(amountInFormValue, 300)
const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300)
const { mangoAccount } = useMangoAccount() const { mangoAccount } = useMangoAccount()
const { connected } = useWallet() const { connected } = useWallet()
const amountIn: Decimal | null = useMemo(() => { const amountInAsDecimal: Decimal | null = useMemo(() => {
return Number(debouncedAmountIn) return Number(debouncedAmountIn)
? new Decimal(debouncedAmountIn) ? new Decimal(debouncedAmountIn)
: new Decimal(0) : new Decimal(0)
}, [debouncedAmountIn]) }, [debouncedAmountIn])
const amountOutAsDecimal: Decimal | null = useMemo(() => {
return Number(debouncedAmountOut)
? new Decimal(debouncedAmountOut)
: new Decimal(0)
}, [debouncedAmountOut])
const { bestRoute, routes } = useJupiterRoutes({ const { bestRoute, routes } = useJupiterRoutes({
inputMint: inputBank?.mint.toString() || USDC_MINT, inputMint: inputBank?.mint.toString() || USDC_MINT,
outputMint: outputBank?.mint.toString() || MANGO_MINT, outputMint: outputBank?.mint.toString() || MANGO_MINT,
inputAmount: debouncedAmountIn, amount: swapMode === 'ExactIn' ? debouncedAmountIn : debouncedAmountOut,
slippage, slippage,
swapMode,
}) })
const outAmount: number = useMemo(() => { const setAmountInFormValue = useCallback((amountIn: string) => {
return selectedRoute?.outAmount.toString() set((s) => {
? new Decimal(selectedRoute.outAmount.toString()) s.swap.amountIn = amountIn
.div(10 ** outputBank!.mintDecimals) })
.toNumber() }, [])
: 0
}, [selectedRoute, outputBank])
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(() => { useEffect(() => {
if (bestRoute) { if (bestRoute) {
setSelectedRoute(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(() => { useEffect(() => {
setAmountInFormValue('') setAmountInFormValue('')
setAmountOutFormValue('')
}, [useMargin]) }, [useMargin])
const handleAmountInChange = useCallback((e: NumberFormatValues) => { const handleAmountInChange = useCallback(
setAmountInFormValue(e.value) (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( const handleTokenOutSelect = useCallback((mintAddress: string) => {
(mintAddress: string) => { const group = mangoStore.getState().group
const group = mangoStore.getState().group if (group) {
if (group) { const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
const bank = group.getFirstBankByMint(new PublicKey(mintAddress)) set((s) => {
set((s) => { s.swap.outputBank = bank
s.swap.inputBank = bank })
}) }
} setShowTokenSelect('')
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 handleSwitchTokens = useCallback(() => { const handleSwitchTokens = useCallback(() => {
if (amountIn?.gt(0) && outAmount) { if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) {
setAmountInFormValue(outAmount.toString()) setAmountInFormValue(amountOutAsDecimal.toString())
set((s) => {
s.swap.swapMode = 'ExactIn'
})
} }
const inputBank = mangoStore.getState().swap.inputBank const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank const outputBank = mangoStore.getState().swap.outputBank
@ -149,11 +204,17 @@ const SwapForm = () => {
setAnimateSwitchArrow( setAnimateSwitchArrow(
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1 (prevanimateSwitchArrow) => prevanimateSwitchArrow + 1
) )
}, [set, outAmount, amountIn]) }, [set, amountOutAsDecimal, amountInAsDecimal])
const maintProjectedHealth = useMemo(() => { const maintProjectedHealth = useMemo(() => {
const group = mangoStore.getState().group const group = mangoStore.getState().group
if (!inputBank || !mangoAccount || !outputBank || !outAmount || !group) if (
!inputBank ||
!mangoAccount ||
!outputBank ||
!amountOutAsDecimal ||
!group
)
return 0 return 0
const simulatedHealthRatio = const simulatedHealthRatio =
@ -162,25 +223,31 @@ const SwapForm = () => {
[ [
{ {
mintPk: inputBank.mint, mintPk: inputBank.mint,
uiTokenAmount: amountIn.toNumber() * -1, uiTokenAmount: amountInAsDecimal.toNumber() * -1,
}, },
{ {
mintPk: outputBank.mint, mintPk: outputBank.mint,
uiTokenAmount: outAmount, uiTokenAmount: amountInAsDecimal.toNumber(),
}, },
], ],
HealthType.maint HealthType.maint
) )
return simulatedHealthRatio! > 100 return simulatedHealthRatio > 100
? 100 ? 100
: simulatedHealthRatio! < 0 : simulatedHealthRatio < 0
? 0 ? 0
: Math.trunc(simulatedHealthRatio!) : Math.trunc(simulatedHealthRatio)
}, [mangoAccount, inputBank, outputBank, amountIn, outAmount]) }, [
mangoAccount,
inputBank,
outputBank,
amountInAsDecimal,
amountOutAsDecimal,
])
const loadingSwapDetails: boolean = useMemo(() => { const loadingSwapDetails: boolean = useMemo(() => {
return !!amountIn.toNumber() && connected && !selectedRoute return !!amountInAsDecimal.toNumber() && connected && !selectedRoute
}, [amountIn, connected, selectedRoute]) }, [amountInAsDecimal, connected, selectedRoute])
return ( return (
<ContentBox <ContentBox
@ -199,9 +266,9 @@ const SwapForm = () => {
leaveFrom="translate-x-0" leaveFrom="translate-x-0"
leaveTo="translate-x-full" leaveTo="translate-x-full"
> >
<JupiterRouteInfo <SwapReviewRouteInfo
onClose={() => setShowConfirm(false)} onClose={() => setShowConfirm(false)}
amountIn={amountIn} amountIn={amountInAsDecimal}
slippage={slippage} slippage={slippage}
routes={routes} routes={routes}
selectedRoute={selectedRoute} 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"> <div className="col-span-1 rounded-lg rounded-r-none border border-r-0 border-th-bkg-4 bg-th-bkg-1">
<TokenSelect <TokenSelect
bank={ bank={
inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)![0] inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)?.[0]
} }
showTokenList={setShowTokenSelect} showTokenList={setShowTokenSelect}
type="input" type="input"
@ -296,13 +363,13 @@ const SwapForm = () => {
<TokenSelect <TokenSelect
bank={ bank={
outputBank || outputBank ||
group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)![0] group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)?.[0]
} }
showTokenList={setShowTokenSelect} showTokenList={setShowTokenSelect}
type="output" type="output"
/> />
</div> </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 ? ( {loadingSwapDetails ? (
<div className="w-full"> <div className="w-full">
<SheenLoader className="flex flex-1 rounded-l-none"> <SheenLoader className="flex flex-1 rounded-l-none">
@ -318,10 +385,10 @@ const SwapForm = () => {
decimalScale={outputBank?.mintDecimals || 6} decimalScale={outputBank?.mintDecimals || 6}
name="amountOut" name="amountOut"
id="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" placeholder="0.00"
disabled value={amountOutFormValue}
value={outAmount ? numberFormat.format(outAmount) : ''} onValueChange={handleAmountOutChange}
/> />
)} )}
</div> </div>
@ -329,13 +396,13 @@ const SwapForm = () => {
{swapFormSizeUi === 'Slider' ? ( {swapFormSizeUi === 'Slider' ? (
<SwapSlider <SwapSlider
useMargin={useMargin} useMargin={useMargin}
amount={amountIn.toNumber()} amount={amountInAsDecimal.toNumber()}
onChange={setAmountInFormValue} onChange={setAmountInFormValue}
step={1 / 10 ** (inputBank?.mintDecimals || 6)} step={1 / 10 ** (inputBank?.mintDecimals || 6)}
/> />
) : ( ) : (
<PercentageSelectButtons <PercentageSelectButtons
amountIn={amountIn.toString()} amountIn={amountInAsDecimal.toString()}
setAmountIn={setAmountInFormValue} setAmountIn={setAmountInFormValue}
useMargin={useMargin} useMargin={useMargin}
/> />
@ -344,25 +411,30 @@ const SwapForm = () => {
loadingSwapDetails={loadingSwapDetails} loadingSwapDetails={loadingSwapDetails}
useMargin={useMargin} useMargin={useMargin}
setShowConfirm={setShowConfirm} setShowConfirm={setShowConfirm}
amountIn={amountIn} amountIn={amountInAsDecimal}
inputSymbol={inputBank?.name} inputSymbol={inputBank?.name}
amountOut={selectedRoute ? outAmount : undefined} amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
/> />
{group ? ( {group && inputBank ? (
<div className="pt-4"> <div className="pt-4">
<TokenVaultWarnings <TokenVaultWarnings bank={inputBank} />
bank={
inputBank || group.banksMapByName.get(INPUT_TOKEN_DEFAULT)![0]
}
/>
</div> </div>
) : null} ) : null}
</div> <div id="swap-step-four" className={`px-2 py-4`}>
<div <HealthImpact maintProjectedHealth={maintProjectedHealth} />
id="swap-step-four" </div>
className={`border-t border-th-bkg-3 px-6 py-4 transition-all`} <div className={`px-2`}>
> <div className="flex justify-between">
<HealthImpact maintProjectedHealth={maintProjectedHealth} /> <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> </div>
</ContentBox> </ContentBox>
) )
@ -382,7 +454,7 @@ const SwapFormSubmitButton = ({
amountOut: number | undefined amountOut: number | undefined
inputSymbol: string | undefined inputSymbol: string | undefined
loadingSwapDetails: boolean loadingSwapDetails: boolean
setShowConfirm: (x: any) => any setShowConfirm: (x: boolean) => void
useMargin: boolean useMargin: boolean
}) => { }) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
@ -433,92 +505,3 @@ const SwapFormSubmitButton = ({
</Button> </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>
)
}

View File

@ -93,7 +93,7 @@ const EMPTY_COINGECKO_PRICES = {
outputCoingeckoPrice: 0, outputCoingeckoPrice: 0,
} }
const JupiterRouteInfo = ({ const SwapReviewRouteInfo = ({
amountIn, amountIn,
onClose, onClose,
routes, routes,
@ -570,4 +570,4 @@ const JupiterRouteInfo = ({
) : null ) : null
} }
export default React.memo(JupiterRouteInfo) export default React.memo(SwapReviewRouteInfo)

View File

@ -38,7 +38,7 @@ const SwapSettings = ({ onClose }: { onClose: () => void }) => {
</IconButton> </IconButton>
<div className="mt-4"> <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 ? ( {showCustomSlippageForm ? (
<Input <Input
type="text" type="text"

View File

@ -6,8 +6,9 @@ import useJupiterSwapData from './useJupiterSwapData'
type useJupiterPropTypes = { type useJupiterPropTypes = {
inputMint: string inputMint: string
outputMint: string outputMint: string
inputAmount: string amount: string
slippage: number slippage: number
swapMode: string
} }
const fetchJupiterRoutes = async ( const fetchJupiterRoutes = async (
@ -15,19 +16,20 @@ const fetchJupiterRoutes = async (
outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
amount = 0, amount = 0,
slippage = 50, slippage = 50,
swapMode = 'ExactIn',
feeBps = 0 feeBps = 0
) => { ) => {
{ {
const params: any = { const paramsString = new URLSearchParams({
inputMint: inputMint.toString(), inputMint: inputMint.toString(),
outputMint: outputMint.toString(), outputMint: outputMint.toString(),
amount, amount: amount.toString(),
slippageBps: Math.ceil(slippage * 100), slippageBps: Math.ceil(slippage * 100).toString(),
onlyDirectRoutes: 'true', onlyDirectRoutes: 'true',
feeBps, feeBps: feeBps.toString(),
} swapMode,
}).toString()
const paramsString = new URLSearchParams(params).toString()
const response = await fetch( const response = await fetch(
`https://quote-api.jup.ag/v3/quote?${paramsString}` `https://quote-api.jup.ag/v3/quote?${paramsString}`
) )
@ -45,25 +47,39 @@ const fetchJupiterRoutes = async (
const useJupiterRoutes = ({ const useJupiterRoutes = ({
inputMint, inputMint,
outputMint, outputMint,
inputAmount, amount,
slippage, slippage,
swapMode,
}: useJupiterPropTypes) => { }: useJupiterPropTypes) => {
const { inputTokenInfo } = useJupiterSwapData() console.log('amount: ', amount)
const amount = inputAmount const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
? new Decimal(inputAmount).mul(10 ** (inputTokenInfo?.decimals || 6))
const decimals =
swapMode === 'ExactIn'
? inputTokenInfo?.decimals || 6
: outputTokenInfo?.decimals || 6
const nativeAmount = amount
? new Decimal(amount).mul(10 ** decimals)
: new Decimal(0) : new Decimal(0)
const res = useQuery<{ routes: RouteInfo[]; bestRoute: RouteInfo }, Error>( const res = useQuery<{ routes: RouteInfo[]; bestRoute: RouteInfo }, Error>(
['swap-routes', inputMint, outputMint, inputAmount, slippage], ['swap-routes', inputMint, outputMint, amount, slippage, swapMode],
async () => 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 ?? { ...(res.data ?? {
routes: [], routes: [],

View File

@ -54,6 +54,13 @@ const PerpFundingRate = () => {
: '-'} : '-'}
% %
</div> </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> */}
</> </>
) )
} }

View File

@ -12,7 +12,7 @@
"postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz" "postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz"
}, },
"dependencies": { "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", "@headlessui/react": "^1.6.6",
"@heroicons/react": "^2.0.10", "@heroicons/react": "^2.0.10",
"@project-serum/anchor": "0.25.0", "@project-serum/anchor": "0.25.0",

View File

@ -37,7 +37,6 @@ import { Orderbook, SpotBalances } from 'types'
import spotBalancesUpdater from './spotBalancesUpdater' import spotBalancesUpdater from './spotBalancesUpdater'
import { PerpMarket } from '@blockworks-foundation/mango-v4/' import { PerpMarket } from '@blockworks-foundation/mango-v4/'
import perpPositionsUpdater from './perpPositionsUpdater' import perpPositionsUpdater from './perpPositionsUpdater'
import { token } from '@project-serum/anchor/dist/cjs/utils'
const GROUP = new PublicKey('DLdcpC6AsAJ9xeKMR3WhHrN5sM5o7GVVXQhQ5vwisTtz') const GROUP = new PublicKey('DLdcpC6AsAJ9xeKMR3WhHrN5sM5o7GVVXQhQ5vwisTtz')
@ -232,6 +231,9 @@ export type MangoStore = {
margin: boolean margin: boolean
slippage: number slippage: number
success: boolean success: boolean
swapMode: 'ExactIn' | 'ExactOut'
amountIn: string
amountOut: string
} }
set: (x: (x: MangoStore) => void) => void set: (x: (x: MangoStore) => void) => void
tokenStats: { tokenStats: {
@ -342,6 +344,18 @@ const mangoStore = create<MangoStore>()(
}, },
uiLocked: true, uiLocked: true,
}, },
swap: {
inputBank: undefined,
outputBank: undefined,
inputTokenInfo: undefined,
outputTokenInfo: undefined,
margin: true,
slippage: 0.5,
success: false,
swapMode: 'ExactIn',
amountIn: '',
amountOut: '',
},
tokenStats: { tokenStats: {
loading: false, loading: false,
data: [], data: [],
@ -355,15 +369,6 @@ const mangoStore = create<MangoStore>()(
postOnly: false, postOnly: false,
ioc: false, ioc: false,
}, },
swap: {
inputBank: undefined,
outputBank: undefined,
inputTokenInfo: undefined,
outputTokenInfo: undefined,
margin: true,
slippage: 0.5,
success: false,
},
wallet: { wallet: {
tokens: [], tokens: [],
nfts: { nfts: {

View File

@ -50,9 +50,9 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" 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" 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: dependencies:
"@project-serum/anchor" "^0.25.0" "@project-serum/anchor" "^0.25.0"
"@project-serum/serum" "^0.13.65" "@project-serum/serum" "^0.13.65"