merge main

This commit is contained in:
saml33 2023-10-30 16:11:41 +11:00
commit 686c654c48
27 changed files with 345 additions and 1069 deletions

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,7 @@ const NotificationsButton = () => {
<BellIcon className="h-5 w-5" />
<span className="sr-only">Notifications</span>
{notificationCount !== 0 ? (
<div className="absolute left-8 top-4">
<div className="absolute left-5 top-4 sm:left-8">
<span className="relative flex h-3.5 w-max items-center justify-center rounded-full bg-th-down px-1 text-xxs font-bold text-white">
{formatNumericValue(notificationCount)}
</span>

View File

@ -3,7 +3,7 @@ import Button from './Button'
import { useWallet } from '@solana/wallet-adapter-react'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { WalletName, WalletReadyState } from '@solana/wallet-adapter-base'
import { AUTO_CONNECT_WALLET, LAST_WALLET_NAME } from 'utils/constants'
import { IS_ONBOARDED_KEY, LAST_WALLET_NAME } from 'utils/constants'
import { notify } from 'utils/notifications'
import { LinkIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
@ -24,10 +24,10 @@ const SecondaryConnectButton = ({
LAST_WALLET_NAME,
'',
)
const [autoConnect] = useLocalStorageState(AUTO_CONNECT_WALLET, true)
const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY)
const handleConnect = useCallback(() => {
if (!autoConnect) {
if (!isOnboarded) {
set((s) => {
s.showUserSetup = true
})
@ -48,7 +48,7 @@ const SecondaryConnectButton = ({
})
}
}
}, [autoConnect, lastWalletName, select, wallets])
}, [isOnboarded, lastWalletName, select, wallets])
return (
<Button

View File

@ -3,6 +3,7 @@ import { PerpStatsItem } from 'types'
import { formatNumericValue } from 'utils/numbers'
import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import { useTranslation } from 'next-i18next'
import Switch from '@components/forms/Switch'
interface GroupedDataItem extends PerpStatsItem {
intervalStartMillis: number
@ -37,6 +38,8 @@ const groupByHourlyInterval = (
return groupedData
}
const YEARLY_HOURS = 8760
const AverageFundingChart = ({
loading,
marketStats,
@ -46,6 +49,7 @@ const AverageFundingChart = ({
}) => {
const { t } = useTranslation(['common', 'stats', 'trade'])
const [daysToShow, setDaysToShow] = useState('30')
const [showAsApr, setShowAsApr] = useState(false)
const [interval, intervalString] = useMemo(() => {
if (daysToShow === '30') {
@ -60,26 +64,49 @@ const AverageFundingChart = ({
const chartData = useMemo(() => {
if (!marketStats) return []
const groupedData = groupByHourlyInterval(marketStats, interval)
if (showAsApr) {
const multiplier = YEARLY_HOURS / interval
for (const data of groupedData) {
data.funding_rate_hourly = data.funding_rate_hourly * multiplier
}
}
return groupedData
}, [daysToShow, interval, marketStats])
}, [daysToShow, interval, marketStats, showAsApr])
return (
<DetailedAreaOrBarChart
data={chartData}
daysToShow={daysToShow}
setDaysToShow={setDaysToShow}
heightClass="h-64"
loading={loading}
loaderHeightClass="h-[350px]"
suffix="%"
tickFormat={(x) => formatNumericValue(x, 4)}
title={t('trade:average-funding', { interval: t(intervalString) })}
xKey="date_hour"
yKey="funding_rate_hourly"
yDecimals={5}
chartType="bar"
tooltipDateFormat={daysToShow === '30' ? 'DD MMM YY' : ''}
/>
<div className="flex h-full flex-col justify-between">
<div className="px-4 md:px-6">
<DetailedAreaOrBarChart
data={chartData}
daysToShow={daysToShow}
setDaysToShow={setDaysToShow}
heightClass="h-64"
loading={loading}
loaderHeightClass="h-[350px]"
suffix="%"
tickFormat={(x) => formatNumericValue(x, 4)}
title={
showAsApr
? `${t('trade:average-funding', { interval: '' })} APR`
: t('trade:average-funding', { interval: t(intervalString) })
}
xKey="date_hour"
yKey="funding_rate_hourly"
yDecimals={showAsApr ? 2 : 5}
chartType="bar"
tooltipDateFormat={daysToShow === '30' ? 'DD MMM YY' : ''}
/>
</div>
<div className="mt-2 flex justify-end border-t border-th-bkg-3 px-4 py-2 md:px-6">
<Switch
checked={showAsApr}
onChange={() => setShowAsApr(!showAsApr)}
small
>
{t('stats:show-as-apr')}
</Switch>
</div>
</div>
)
}

View File

@ -9,7 +9,7 @@ import DetailedAreaOrBarChart from '@components/shared/DetailedAreaOrBarChart'
import AverageFundingChart from './AverageFundingChart'
const CHART_WRAPPER_CLASSES =
'col-span-2 lg:col-span-1 border-b border-th-bkg-3 py-4 px-6'
'col-span-2 lg:col-span-1 border-b border-th-bkg-3 py-4 px-4 md:px-6'
import PerpMarketParams from './PerpMarketParams'
import PerpVolumeChart from './PerpVolumeChart'
@ -74,7 +74,9 @@ const PerpMarketDetails = ({
yKey={'open_interest'}
/>
</div>
<div className={`${CHART_WRAPPER_CLASSES} lg:border-r`}>
<div
className={`col-span-2 border-b border-th-bkg-3 pt-4 lg:col-span-1 lg:border-r`}
>
<AverageFundingChart
loading={loadingPerpStats}
marketStats={marketStats}
@ -92,9 +94,9 @@ const PerpMarketDetails = ({
])}
daysToShow={priceDaysToShow}
setDaysToShow={setPriceDaysToShow}
heightClass="h-64"
heightClass="h-72"
loading={loadingPerpStats}
loaderHeightClass="h-[350px]"
loaderHeightClass="h-[400px]"
prefix="$"
tickFormat={(x) => formatYAxis(x)}
title={t('price')}

View File

@ -18,24 +18,14 @@ import SwapSlider from './SwapSlider'
import PercentageSelectButtons from './PercentageSelectButtons'
import BuyTokenInput from './BuyTokenInput'
import SellTokenInput from './SellTokenInput'
import Button from '@components/shared/Button'
import SwapReviewRouteInfo from './SwapReviewRouteInfo'
import useIpAddress from 'hooks/useIpAddress'
import { useTranslation } from 'react-i18next'
import useQuoteRoutes from './useQuoteRoutes'
import { useTokenMax } from './useTokenMax'
import Loading from '@components/shared/Loading'
import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import Link from 'next/link'
import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { formatCurrencyValue } from 'utils/numbers'
import { SwapFormTokenListType } from './SwapFormTokenList'
import useTokenPositionsFull from 'hooks/useTokenPositionsFull'
import TopBarStore from '@store/topBarStore'
import SwapFormSubmitButton from './SwapFormSubmitButton'
dayjs.extend(relativeTime)
@ -60,7 +50,6 @@ const MarketSwapForm = ({
setShowTokenSelect,
onSuccess,
}: MarketSwapFormProps) => {
const { t } = useTranslation(['common', 'swap', 'trade'])
//initial state is undefined null is returned on error
const [selectedRoute, setSelectedRoute] =
useState<JupiterV6RouteInfo | null>()
@ -101,7 +90,6 @@ const MarketSwapForm = ({
!isDraggingSlider
),
})
const { ipAllowed, ipCountry } = useIpAddress()
const amountInAsDecimal: Decimal | null = useMemo(() => {
return Number(amountInFormValue)
@ -358,131 +346,15 @@ const MarketSwapForm = ({
setShowTokenSelect={setShowTokenSelect}
handleRepay={handleRepay}
/>
{ipAllowed ? (
<SwapFormSubmitButton
loadingSwapDetails={loadingExactIn || loadingExactOut}
useMargin={useMargin}
selectedRoute={selectedRoute}
setShowConfirm={setShowConfirm}
amountIn={amountInAsDecimal}
inputSymbol={inputBank?.name}
amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
/>
) : (
<Button
disabled
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
size="large"
>
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
<SwapFormSubmitButton
loadingSwapDetails={loadingExactIn || loadingExactOut}
selectedRoute={selectedRoute}
setShowConfirm={setShowConfirm}
amountIn={amountInAsDecimal}
amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
/>
</>
)
}
export default MarketSwapForm
const SwapFormSubmitButton = ({
amountIn,
amountOut,
loadingSwapDetails,
selectedRoute,
setShowConfirm,
}: {
amountIn: Decimal
amountOut: number | undefined
inputSymbol: string | undefined
loadingSwapDetails: boolean
selectedRoute: JupiterV6RouteInfo | undefined | null
setShowConfirm: (x: boolean) => void
useMargin: boolean
}) => {
const { t } = useTranslation('common')
const { mangoAccountAddress } = useMangoAccount()
const { connected } = useWallet()
const { setShowSettingsModal } = TopBarStore()
// const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin)
const { inputBank, outputBank } = mangoStore((s) => s.swap)
const { remainingBorrowsInPeriod, timeToNextPeriod } =
useRemainingBorrowsInPeriod(true)
const tokenPositionsFull = useTokenPositionsFull([outputBank, inputBank])
// check if the borrowed amount exceeds the net borrow limit in the current period
const borrowExceedsLimitInPeriod = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount || !inputBank || !remainingBorrowsInPeriod) return false
const balance = mangoAccount.getTokenDepositsUi(inputBank)
const remainingBalance = balance - amountIn.toNumber()
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
const borrowAmountNotional = borrowAmount * inputBank.uiPrice
return borrowAmountNotional > remainingBorrowsInPeriod
}, [amountIn, inputBank, mangoAccountAddress, remainingBorrowsInPeriod])
const disabled =
!amountIn.toNumber() ||
!amountOut ||
!selectedRoute ||
!!selectedRoute.error
return (
<>
{connected ? (
<Button
onClick={() => setShowConfirm(true)}
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
disabled={disabled}
size="large"
>
{loadingSwapDetails ? (
<Loading />
) : (
<span>{t('swap:review-swap')}</span>
)}
</Button>
) : (
<SecondaryConnectButton
className="mb-4 mt-6 flex w-full items-center justify-center"
isLarge
/>
)}
{tokenPositionsFull ? (
<div className="pb-4">
<InlineNotification
type="error"
desc={
<>
{t('error-token-positions-full')}{' '}
<Link href={''} onClick={() => setShowSettingsModal(true)}>
{t('manage')}
</Link>
</>
}
/>
</div>
) : null}
{borrowExceedsLimitInPeriod &&
remainingBorrowsInPeriod &&
timeToNextPeriod ? (
<div className="mb-4">
<InlineNotification
type="error"
desc={t('error-borrow-exceeds-limit', {
remaining: formatCurrencyValue(remainingBorrowsInPeriod),
resetTime: dayjs().to(dayjs().add(timeToNextPeriod, 'second')),
})}
/>
</div>
) : null}
{(selectedRoute === null && amountIn.gt(0)) ||
(selectedRoute && !!selectedRoute.error) ? (
<div className="mb-4">
<InlineNotification type="error" desc={t('swap:no-swap-found')} />
</div>
) : null}
</>
)
}

View File

@ -0,0 +1,28 @@
const POPULAR_TOKENS = [
{ name: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' },
{ name: 'SOL', mint: 'So11111111111111111111111111111111111111112' },
{ name: 'mSOL', mint: 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So' },
{ name: 'wBTC', mint: '3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh' },
]
const PopularSwapTokens = ({
setSwapToken,
}: {
setSwapToken: (token: string) => void
}) => {
return (
<div className="flex flex-wrap">
{POPULAR_TOKENS.map((token) => (
<button
className="m-1 rounded-md border border-th-fgd-4 px-2 py-0.5 text-sm text-th-fgd-3 focus:outline-none md:hover:border-th-fgd-2 md:hover:text-th-fgd-2"
onClick={() => setSwapToken(token.mint)}
key={token.mint}
>
{token.name}
</button>
))}
</div>
)
}
export default PopularSwapTokens

View File

@ -162,7 +162,7 @@ const SwapForm = () => {
{swapOrTrigger === 'swap' ? (
<>
<div className="flex justify-end pb-3 pt-4">
<div className="flex justify-between px-6">
<div className="flex justify-between px-4 md:px-6">
<Switch
checked={walletSwap}
onChange={() => setWalletSwap(!walletSwap)}
@ -177,24 +177,24 @@ const SwapForm = () => {
</div>
</div>
{walletSwap ? (
<div className="px-6">
<div className="px-4 md:px-6">
<WalletSwapForm setShowTokenSelect={setShowTokenSelect} />
</div>
) : (
<div className="px-6">
<div className="px-4 md:px-6">
<MarketSwapForm setShowTokenSelect={setShowTokenSelect} />
</div>
)}
</>
) : (
<div className="px-6 pt-4">
<div className="px-4 pt-4 md:px-6">
<TriggerSwapForm
showTokenSelect={showTokenSelect}
setShowTokenSelect={setShowTokenSelect}
/>
</div>
)}
<div className="px-6 pb-6">
<div className="px-4 pb-6 md:px-6">
{inputBank && !walletSwap ? (
<TokenVaultWarnings bank={inputBank} type="swap" />
) : null}

View File

@ -0,0 +1,132 @@
import InlineNotification from '@components/shared/InlineNotification'
import { formatCurrencyValue } from 'utils/numbers'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import Decimal from 'decimal.js'
import { JupiterV6RouteInfo } from 'types/jupiter'
import { useTranslation } from 'react-i18next'
import useMangoAccount from 'hooks/useMangoAccount'
import { useWallet } from '@solana/wallet-adapter-react'
import TopBarStore from '@store/topBarStore'
import mangoStore from '@store/mangoStore'
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
import useTokenPositionsFull from 'hooks/useTokenPositionsFull'
import { useMemo } from 'react'
import Button from '@components/shared/Button'
import Loading from '@components/shared/Loading'
import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
import Link from 'next/link'
import useIpAddress from 'hooks/useIpAddress'
dayjs.extend(relativeTime)
const SwapFormSubmitButton = ({
amountIn,
amountOut,
loadingSwapDetails,
selectedRoute,
setShowConfirm,
}: {
amountIn: Decimal
amountOut: number | undefined
loadingSwapDetails: boolean
selectedRoute: JupiterV6RouteInfo | undefined | null
setShowConfirm: (x: boolean) => void
}) => {
const { t } = useTranslation('common')
const { mangoAccountAddress } = useMangoAccount()
const { connected } = useWallet()
const { ipAllowed, ipCountry } = useIpAddress()
const { setShowSettingsModal } = TopBarStore()
const { inputBank, outputBank } = mangoStore((s) => s.swap)
const { remainingBorrowsInPeriod, timeToNextPeriod } =
useRemainingBorrowsInPeriod(true)
const tokenPositionsFull = useTokenPositionsFull([outputBank, inputBank])
// check if the borrowed amount exceeds the net borrow limit in the current period
const borrowExceedsLimitInPeriod = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount || !inputBank || !remainingBorrowsInPeriod) return false
const balance = mangoAccount.getTokenDepositsUi(inputBank)
const remainingBalance = balance - amountIn.toNumber()
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
const borrowAmountNotional = borrowAmount * inputBank.uiPrice
return borrowAmountNotional > remainingBorrowsInPeriod
}, [amountIn, inputBank, mangoAccountAddress, remainingBorrowsInPeriod])
const disabled =
!amountIn.toNumber() ||
!amountOut ||
!selectedRoute ||
!!selectedRoute.error
return ipAllowed ? (
<>
{connected ? (
<Button
onClick={() => setShowConfirm(true)}
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
disabled={disabled}
size="large"
>
{loadingSwapDetails ? (
<Loading />
) : (
<span>{t('swap:review-swap')}</span>
)}
</Button>
) : (
<SecondaryConnectButton
className="mb-4 mt-6 flex w-full items-center justify-center"
isLarge
/>
)}
{tokenPositionsFull ? (
<div className="pb-4">
<InlineNotification
type="error"
desc={
<>
{t('error-token-positions-full')}{' '}
<Link href={''} onClick={() => setShowSettingsModal(true)}>
{t('manage')}
</Link>
</>
}
/>
</div>
) : null}
{borrowExceedsLimitInPeriod &&
remainingBorrowsInPeriod &&
timeToNextPeriod ? (
<div className="mb-4">
<InlineNotification
type="error"
desc={t('error-borrow-exceeds-limit', {
remaining: formatCurrencyValue(remainingBorrowsInPeriod),
resetTime: dayjs().to(dayjs().add(timeToNextPeriod, 'second')),
})}
/>
</div>
) : null}
{(selectedRoute === null && amountIn.gt(0)) ||
(selectedRoute && !!selectedRoute.error) ? (
<div className="mb-4">
<InlineNotification type="error" desc={t('swap:no-swap-found')} />
</div>
) : null}
</>
) : (
<Button
disabled
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
size="large"
>
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)
}
export default SwapFormSubmitButton

View File

@ -17,6 +17,7 @@ import Input from '@components/forms/Input'
import { getInputTokenBalance } from './TriggerSwapForm'
import { walletBalanceForToken } from '@components/DepositForm'
import TokenReduceOnlyDesc from '@components/shared/TokenReduceOnlyDesc'
import PopularSwapTokens from './PopularSwapTokens'
export type SwapFormTokenListType =
| 'input'
@ -349,7 +350,7 @@ const SwapFormTokenList = ({
>
<XMarkIcon className="h-6 w-6" />
</IconButton>
<div className="relative mb-4">
<div className="relative">
<Input
className="pl-10"
type="text"
@ -361,13 +362,24 @@ const SwapFormTokenList = ({
/>
<MagnifyingGlassIcon className="absolute left-3 top-3.5 h-5 w-5 text-th-fgd-3" />
</div>
<div className="flex justify-between rounded bg-th-bkg-2 p-2">
{!type?.includes('reduce') ? (
<div className="pt-2">
<PopularSwapTokens setSwapToken={handleTokenSelect} />
</div>
) : null}
<div className="mt-4 flex justify-between rounded bg-th-bkg-2 p-2">
<p className="text-xs text-th-fgd-4">{t('token')}</p>
{!type?.includes('output') ? (
<p className="text-xs text-th-fgd-4">{t('max')}</p>
) : null}
</div>
<div className="thin-scroll h-[calc(100%-128px)] overflow-auto py-2">
<div
className={`thin-scroll ${
!type?.includes('reduce')
? 'h-[calc(100%-170px)]'
: 'h-[calc(100%-128px)]'
} overflow-auto py-2`}
>
{sortedTokens?.length ? (
sortedTokens.map((token) => (
<TokenItem

View File

@ -63,18 +63,6 @@ const SwapSummaryInfo = ({
: Math.trunc(simulatedHealthRatio)
}, [inputBank, outputBank, amountInFormValue, amountOutFormValue])
const estSlippage = useMemo(() => {
const { group } = mangoStore.getState()
const amountIn = parseFloat(amountInFormValue) || 0
if (!group || !inputBank || amountIn <= 0) return 0
const value = amountIn * inputBank.uiPrice
const slippage = group.getPriceImpactByTokenIndex(
inputBank.tokenIndex,
value,
)
return slippage
}, [amountInFormValue, inputBank])
const handleSetMargin = () => {
set((s) => {
s.swap.margin = !s.swap.margin
@ -117,16 +105,6 @@ const SwapSummaryInfo = ({
</div>
</>
) : null}
{estSlippage ? (
<>
<div className="flex items-center justify-between">
<p className="text-sm text-th-fgd-3">{t('trade:est-slippage')}</p>
<span className="font-mono text-th-fgd-2">
{estSlippage.toFixed(2)}%
</span>
</div>
</>
) : null}
</div>
)
}

View File

@ -188,6 +188,7 @@ const SwapTokenChart = () => {
swapOrTrigger,
} = mangoStore((s) => s.swap)
const { inputCoingeckoId, outputCoingeckoId } = useJupiterSwapData()
const { isDesktop } = useViewport()
const [baseTokenId, setBaseTokenId] = useState(inputCoingeckoId)
const [quoteTokenId, setQuoteTokenId] = useState(outputCoingeckoId)
const [mouseData, setMouseData] = useState<SwapChartDataItem>()
@ -498,8 +499,11 @@ const SwapTokenChart = () => {
)
}, [amountIn, amountOut, swapMode])
const chartNumberHeight = isDesktop ? 48 : 40
const chartNumberWidth = isDesktop ? 35 : 27
return (
<ContentBox hideBorder hidePadding className="h-full px-6 py-3">
<ContentBox hideBorder hidePadding className="h-full px-4 py-3 md:px-6">
{isLoading || isFetching ? (
<>
<SheenLoader className="w-[148px] rounded-md">
@ -539,11 +543,11 @@ const SwapTokenChart = () => {
) : null}
{mouseData ? (
<>
<div className="mb-1 flex flex-col font-display text-5xl text-th-fgd-1 md:flex-row md:items-end">
<div className="mb-1 flex flex-col font-display text-4xl text-th-fgd-1 md:flex-row md:items-end md:text-5xl">
{animationSettings['number-scroll'] ? (
<FlipNumbers
height={48}
width={35}
height={chartNumberHeight}
width={chartNumberWidth}
play
numbers={formatNumericValue(mouseData.price)}
/>
@ -552,27 +556,25 @@ const SwapTokenChart = () => {
<FormatNumericValue value={mouseData.price} />
</span>
)}
<span
className={`ml-0 mt-2 flex items-center text-sm md:ml-3 md:mt-0`}
>
<Change change={calculateChartChange()} suffix="%" />
</span>
</div>
<p className="text-sm text-th-fgd-4">
{dayjs(mouseData.time).format('DD MMM YY, h:mma')}
</p>
<div className="flex space-x-3">
<Change change={calculateChartChange()} suffix="%" />
<p className="text-sm text-th-fgd-4">
{dayjs(mouseData.time).format('DD MMM YY, h:mma')}
</p>
</div>
</>
) : (
<>
<div className="mb-1 flex flex-col font-display text-5xl text-th-fgd-1 md:flex-row md:items-end">
<div className="mb-1 flex flex-col font-display text-4xl text-th-fgd-1 md:flex-row md:items-end md:text-5xl">
{loadingSwapPrice ? (
<SheenLoader className="mt-2 w-[180px] rounded-md">
<div className="h-[40px] bg-th-bkg-2" />
<div className="h-9 bg-th-bkg-2 md:h-10" />
</SheenLoader>
) : animationSettings['number-scroll'] ? (
<FlipNumbers
height={48}
width={35}
height={chartNumberHeight}
width={chartNumberWidth}
play
numbers={formatNumericValue(
chartData[chartData.length - 1].price,
@ -585,22 +587,20 @@ const SwapTokenChart = () => {
/>
</span>
)}
<span
className={`ml-0 mt-2 flex items-center text-sm md:ml-3 md:mt-0`}
>
<Change change={calculateChartChange()} suffix="%" />
</span>
</div>
<p className="text-sm text-th-fgd-4">
{dayjs(chartData[chartData.length - 1].time).format(
'DD MMM YY, h:mma',
)}
</p>
<div className="flex space-x-3">
<Change change={calculateChartChange()} suffix="%" />
<p className="text-sm text-th-fgd-4">
{dayjs(chartData[chartData.length - 1].time).format(
'DD MMM YY, h:mma',
)}
</p>
</div>
</>
)}
</div>
</div>
<div className="mt-2 h-40 w-auto md:h-96">
<div className="mt-2 h-44 w-auto sm:h-52 md:h-96">
<div className="absolute right-0 top-[2px] -mb-2 flex items-center justify-end space-x-4">
<FavoriteSwapButton
inputToken={inputBank!.name}

View File

@ -16,14 +16,8 @@ import { JupiterV6RouteInfo } from 'types/jupiter'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { DEFAULT_PERCENTAGE_VALUES } from './PercentageSelectButtons'
import BuyTokenInput from './BuyTokenInput'
import Button from '@components/shared/Button'
import SwapReviewRouteInfo from './SwapReviewRouteInfo'
import useIpAddress from 'hooks/useIpAddress'
import { useTranslation } from 'react-i18next'
import useQuoteRoutes from './useQuoteRoutes'
import Loading from '@components/shared/Loading'
import InlineNotification from '@components/shared/InlineNotification'
import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { floorToDecimal } from 'utils/numbers'
@ -32,6 +26,7 @@ import WalletSellTokenInput from './WalletSellTokenInput'
import { walletBalanceForToken } from '@components/DepositForm'
import WalletSwapSlider from './WalletSwapSlider'
import ButtonGroup from '@components/forms/ButtonGroup'
import SwapFormSubmitButton from './SwapFormSubmitButton'
dayjs.extend(relativeTime)
@ -42,7 +37,6 @@ type WalletSwapFormProps = {
const set = mangoStore.getState().set
const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => {
const { t } = useTranslation(['common', 'swap', 'trade'])
//initial state is undefined null is returned on error
const [selectedRoute, setSelectedRoute] =
useState<JupiterV6RouteInfo | null>()
@ -52,7 +46,6 @@ const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => {
const [isDraggingSlider, setIsDraggingSlider] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const {
margin: useMargin,
slippage,
inputBank,
outputBank,
@ -84,7 +77,6 @@ const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => {
!isDraggingSlider
),
})
const { ipAllowed, ipCountry } = useIpAddress()
const walletTokens = mangoStore((s) => s.wallet.tokens)
@ -360,81 +352,15 @@ const WalletSwapForm = ({ setShowTokenSelect }: WalletSwapFormProps) => {
loading={loadingExactIn}
setShowTokenSelect={setShowTokenSelect}
/>
{ipAllowed ? (
<SwapFormSubmitButton
loadingSwapDetails={loadingExactIn || loadingExactOut}
useMargin={useMargin}
selectedRoute={selectedRoute}
setShowConfirm={setShowConfirm}
amountIn={amountInAsDecimal}
inputSymbol={inputBank?.name}
amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
/>
) : (
<Button
disabled
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
size="large"
>
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
<SwapFormSubmitButton
loadingSwapDetails={loadingExactIn || loadingExactOut}
selectedRoute={selectedRoute}
setShowConfirm={setShowConfirm}
amountIn={amountInAsDecimal}
amountOut={selectedRoute ? amountOutAsDecimal.toNumber() : undefined}
/>
</>
)
}
export default WalletSwapForm
const SwapFormSubmitButton = ({
amountIn,
amountOut,
loadingSwapDetails,
selectedRoute,
setShowConfirm,
}: {
amountIn: Decimal
amountOut: number | undefined
inputSymbol: string | undefined
loadingSwapDetails: boolean
selectedRoute: JupiterV6RouteInfo | undefined | null
setShowConfirm: (x: boolean) => void
useMargin: boolean
}) => {
const { t } = useTranslation('common')
const { connected } = useWallet()
const disabled =
(connected && !amountIn.toNumber()) || !amountOut || !selectedRoute
return (
<>
{connected ? (
<Button
onClick={() => setShowConfirm(true)}
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
disabled={disabled}
size="large"
>
{loadingSwapDetails ? (
<Loading />
) : (
<span>{t('swap:review-swap')}</span>
)}
</Button>
) : (
<SecondaryConnectButton
className="mb-4 mt-6 flex w-full items-center justify-center"
isLarge
/>
)}
{(selectedRoute === null && amountIn.gt(0)) ||
(selectedRoute && !!selectedRoute.error) ? (
<div className="mb-4">
<InlineNotification type="error" desc={t('swap:no-swap-found')} />
</div>
) : null}
</>
)
}

View File

@ -46,8 +46,8 @@ const fetchIpGeolocation = async () => {
}
export default function useIpAddress() {
const [ipAllowed, setIpAllowed] = useState(false)
const [spotAllowed, setSpotAllowed] = useState(false)
const [ipAllowed, setIpAllowed] = useState(true)
const [spotAllowed, setSpotAllowed] = useState(true)
const [ipCountry, setIpCountry] = useState('')
const ipCountryCode = useQuery<string, Error>(

14
public/icons/guac.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -9,6 +9,7 @@
"perp-volume": "Perp Volume",
"pnl-liquidation-fee": "Positive PnL Liquidation Fee",
"settle-pnl-factor": "Settle PnL Factor",
"show-as-apr": "Show as APR",
"six-hourly": "6-Hourly",
"tooltip-base-liquidation-fee": "The liqee pays this liquidation fee when a liquidator has to take over a perp base position.",
"tooltip-funding-limits": "The minimum and maximum funding rates (in percent per day).",

View File

@ -17,7 +17,7 @@
"insufficient-balance": "Insufficient {{symbol}} Balance",
"insufficient-collateral": "Insufficient Collateral",
"margin": "Margin",
"margin-swap": "Margin Swap",
"mango-swap": "Mango Swap",
"max-slippage": "Max Slippage",
"maximum-cost": "Maximum Cost",
"minimum-received": "Minimum Received",
@ -41,9 +41,10 @@
"show-swap-history": "Show Swap History Prices shown are from CoinGecko and may not match your swap price",
"slippage": "Slippage",
"swap-history": "Swap History",
"swap-into-1": "Borrow against your collateral and swap with up to 5x leverage.",
"swap-into-2": "Swap your Mango assets via the top DEXs on Solana and get the best possible price.",
"swap-into-3": "Use the slider to set your swap size. Margin can be switched off in swap settings.",
"swap-into-1": "Swap with up to 5x leverage",
"swap-into-2": "Powered by Jupiter",
"swap-into-3": "Trigger orders for stop-loss and take-profit",
"swap-into-4": "Wallet Swap to swap tokens in your wallet",
"swap-route": "Swap Route",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",

View File

@ -9,6 +9,7 @@
"perp-volume": "Perp Volume",
"pnl-liquidation-fee": "Positive PnL Liquidation Fee",
"settle-pnl-factor": "Settle PnL Factor",
"show-as-apr": "Show as APR",
"six-hourly": "6-Hourly",
"tooltip-base-liquidation-fee": "The liqee pays this liquidation fee when a liquidator has to take over a perp base position.",
"tooltip-funding-limits": "The minimum and maximum funding rates (in percent per day).",

View File

@ -17,7 +17,7 @@
"insufficient-balance": "Insufficient {{symbol}} Balance",
"insufficient-collateral": "Insufficient Collateral",
"margin": "Margin",
"margin-swap": "Margin Swap",
"mango-swap": "Mango Swap",
"max-slippage": "Max Slippage",
"maximum-cost": "Maximum Cost",
"minimum-received": "Minimum Received",
@ -41,9 +41,10 @@
"show-swap-history": "Show Swap History Prices shown are from CoinGecko and may not match your swap price",
"slippage": "Slippage",
"swap-history": "Swap History",
"swap-into-1": "Borrow against your collateral and swap with up to 5x leverage.",
"swap-into-2": "Swap your Mango assets via the top DEXs on Solana and get the best possible price.",
"swap-into-3": "Use the slider to set your swap size. Margin can be switched off in swap settings.",
"swap-into-1": "Swap with up to 5x leverage",
"swap-into-2": "Powered by Jupiter",
"swap-into-3": "Trigger orders for stop-loss and take-profit",
"swap-into-4": "Wallet Swap to swap tokens in your wallet",
"swap-route": "Swap Route",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",

View File

@ -9,6 +9,7 @@
"perp-volume": "Perp Volume",
"pnl-liquidation-fee": "Positive PnL Liquidation Fee",
"settle-pnl-factor": "Settle PnL Factor",
"show-as-apr": "Show as APR",
"six-hourly": "6-Hourly",
"tooltip-base-liquidation-fee": "The liqee pays this liquidation fee when a liquidator has to take over a perp base position.",
"tooltip-funding-limits": "The minimum and maximum funding rates (in percent per day).",

View File

@ -17,7 +17,7 @@
"insufficient-balance": "Insufficient {{symbol}} Balance",
"insufficient-collateral": "Insufficient Collateral",
"margin": "Margin",
"margin-swap": "Margin Swap",
"mango-swap": "Mango Swap",
"max-slippage": "Max Slippage",
"maximum-cost": "Maximum Cost",
"minimum-received": "Minimum Received",
@ -41,9 +41,10 @@
"show-swap-history": "Show Swap History Prices shown are from CoinGecko and may not match your swap price",
"slippage": "Slippage",
"swap-history": "Swap History",
"swap-into-1": "Borrow against your collateral and swap with up to 5x leverage.",
"swap-into-2": "Swap your Mango assets via the top DEXs on Solana and get the best possible price.",
"swap-into-3": "Use the slider to set your swap size. Margin can be switched off in swap settings.",
"swap-into-1": "Swap with up to 5x leverage",
"swap-into-2": "Powered by Jupiter",
"swap-into-3": "Trigger orders for stop-loss and take-profit",
"swap-into-4": "Wallet Swap to swap tokens in your wallet",
"swap-route": "Swap Route",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%",

View File

@ -9,6 +9,7 @@
"perp-volume": "合约交易量",
"pnl-liquidation-fee": "正数盈亏清算费用",
"settle-pnl-factor": "结清盈亏因素",
"show-as-apr": "Show as APR",
"six-hourly": "6-小时",
"tooltip-base-liquidation-fee": "当清算者接管持仓时,被清算者将支付此费用。",
"tooltip-funding-limits": "资金费用的最高与最低(一天)。",

View File

@ -17,7 +17,7 @@
"insufficient-balance": "{{symbol}}余额不够",
"insufficient-collateral": "质押品不够",
"margin": "保证金",
"margin-swap": "杠杆换币",
"mango-swap": "Mango Swap",
"max-slippage": "最多下滑",
"maximum-cost": "最大成本",
"minimum-received": "最小获取",
@ -43,9 +43,10 @@
"show-swap-history": "显示换币纪录价格来自CoinGecko而也许跟换币价格不同",
"slippage": "下滑",
"swap-history": "换币纪录",
"swap-into-1": "以你的质押品来借贷并进行高达5倍杠杆换币。",
"swap-into-2": "通过 Solana 上的顶级 DEX 交换您的 Mango 资产,并获得最优惠的价格。",
"swap-into-3": "使用滑块设置交换大小。在换币设置中可以关闭杠杆功能。",
"swap-into-1": "Swap with up to 5x leverage",
"swap-into-2": "Powered by Jupiter",
"swap-into-3": "Trigger orders for stop-loss and take-profit",
"swap-into-4": "Wallet Swap to swap tokens in your wallet",
"swap-route": "换币路线",
"tooltip-borrow-balance": "您将使用您的{{balance}} {{token}} 余额并借入{{borrowAmount}} {{token}} 来执行此交换。当前的{{token}} 可变借贷利率为{{rate}}%",
"tooltip-borrow-no-balance": "您将借入 {{borrowAmount}} {{token}} 来执行此交换。当前的 {{token}} 可变借贷利率为 {{rate}}%",

View File

@ -9,6 +9,7 @@
"perp-volume": "合約交易量",
"pnl-liquidation-fee": "正數盈虧清算費用",
"settle-pnl-factor": "結清盈虧因素",
"show-as-apr": "Show as APR",
"six-hourly": "6-小時",
"tooltip-base-liquidation-fee": "當清算者接管持倉時,被清算者將支付此費用。",
"tooltip-funding-limits": "資金費用的最高與最低(一天)。",

View File

@ -17,7 +17,7 @@
"insufficient-balance": "{{symbol}}餘額不夠",
"insufficient-collateral": "質押品不夠",
"margin": "保證金",
"margin-swap": "槓桿換幣",
"mango-swap": "Mango Swap",
"max-slippage": "最多下滑",
"maximum-cost": "最大成本",
"minimum-received": "最小獲取",
@ -43,9 +43,10 @@
"show-swap-history": "顯示換幣紀錄價格來自CoinGecko而也許跟換幣價格不同",
"slippage": "下滑",
"swap-history": "換幣紀錄",
"swap-into-1": "以你的質押品來借貸並進行高達5倍槓桿換幣。",
"swap-into-2": "通過 Solana 上的頂級 DEX 交換您的 Mango 資產,並獲得最優惠的價格。",
"swap-into-3": "使用滑塊設置交換大小。在換幣設置中可以關閉槓桿功能。",
"swap-into-1": "Swap with up to 5x leverage",
"swap-into-2": "Powered by Jupiter",
"swap-into-3": "Trigger orders for stop-loss and take-profit",
"swap-into-4": "Wallet Swap to swap tokens in your wallet",
"swap-route": "換幣路線",
"tooltip-borrow-balance": "您將使用您的 {{balance}} {{token}} 餘額並借入 {{borrowAmount}} {{token}} 來執行此交換。當前的 {{token}} 可變借貸利率為 {{rate}}%",
"tooltip-borrow-no-balance": "您將借入 {{borrowAmount}} {{token}} 來執行此交換。當前的 {{token}} 可變借貸利率為 {{rate}}%",

View File

@ -144,6 +144,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
eth: true,
ethpo: true,
'eth (portal)': true,
guac: true,
hnt: true,
jitosol: true,
kin: true,

View File

@ -79,7 +79,10 @@ const roundValue = (
})
}
const digits2 = new Intl.NumberFormat('en', { maximumFractionDigits: 2 })
const digits2 = new Intl.NumberFormat('en', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
const digits3 = new Intl.NumberFormat('en', { maximumFractionDigits: 3 })
const digits4 = new Intl.NumberFormat('en', { maximumFractionDigits: 4 })
const digits5 = new Intl.NumberFormat('en', { maximumFractionDigits: 5 })