Merge branch 'main' into feature/kline-charts-spot

This commit is contained in:
Adrian Brzeziński 2022-12-01 12:47:35 +01:00
commit cc67329be9
23 changed files with 525 additions and 331 deletions

View File

@ -1,7 +1,7 @@
export const fetchChartData = async (
baseTokenId: string | undefined,
quoteTokenId: string | undefined,
daysToShow: number
daysToShow: string
) => {
console.log('fetching chart:', baseTokenId, quoteTokenId)

View File

@ -26,13 +26,13 @@ const AccountChart = ({
}) => {
const { t } = useTranslation('common')
const actions = mangoStore((s) => s.actions)
const [daysToShow, setDaysToShow] = useState<number>(1)
const [daysToShow, setDaysToShow] = useState<string>('1')
const loading = mangoStore((s) => s.mangoAccount.stats.performance.loading)
const handleDaysToShow = async (days: number) => {
const handleDaysToShow = async (days: string) => {
await actions.fetchAccountPerformance(
mangoAccount.publicKey.toString(),
days
parseInt(days)
)
setDaysToShow(days)
}

View File

@ -4,11 +4,11 @@ import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
import MultiSelectDropdown from '@components/forms/MultiSelectDropdown'
import { EXPLORERS } from '@components/settings/PreferredExplorerSettings'
import Button, { IconButton, LinkButton } from '@components/shared/Button'
import Modal from '@components/shared/Modal'
import Button, { IconButton } from '@components/shared/Button'
import Tooltip from '@components/shared/Tooltip'
import { Disclosure, Transition } from '@headlessui/react'
import {
AdjustmentsVerticalIcon,
ArrowLeftIcon,
ArrowPathIcon,
ChevronDownIcon,
@ -18,18 +18,17 @@ import dayjs from 'dayjs'
import useLocalStorageState from 'hooks/useLocalStorageState'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup'
import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'next-i18next'
import Image from 'next/legacy/image'
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'
import { PREFERRED_EXPLORER_KEY } from 'utils/constants'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import { breakpoints } from 'utils/theme'
import ActivityFeedTable from './ActivityFeedTable'
interface Filters {
deposit: boolean
liquidate_token_with_token: boolean
perp_trade: boolean
swap: boolean
withdraw: boolean
}
@ -45,6 +44,7 @@ interface AdvancedFilters {
const DEFAULT_FILTERS = {
deposit: true,
liquidate_token_with_token: true,
perp_trade: true,
swap: true,
withdraw: true,
}
@ -61,6 +61,7 @@ const DEFAULT_PARAMS = [
'deposit',
'liquidate_token_with_token',
'swap',
'perp_trade',
'withdraw',
]
@ -111,7 +112,7 @@ const ActivityFeed = () => {
}, [advancedFilters])
const queryParams = useMemo(() => {
return params.length === 3
return params.length === 5
? advancedParamsString
: `&activity-type=${params.toString()}${advancedParamsString}`
}, [advancedParamsString, params])
@ -123,6 +124,7 @@ const ActivityFeed = () => {
setFilters={setFilters}
updateFilters={updateFilters}
params={queryParams}
setParams={setParams}
advancedFilters={advancedFilters}
setAdvancedFilters={setAdvancedFilters}
/>
@ -147,6 +149,7 @@ const ActivityFilters = ({
setFilters,
updateFilters,
params,
setParams,
advancedFilters,
setAdvancedFilters,
}: {
@ -154,6 +157,7 @@ const ActivityFilters = ({
setFilters: (x: Filters) => void
updateFilters: (e: ChangeEvent<HTMLInputElement>, filter: string) => void
params: string
setParams: (x: string[]) => void
advancedFilters: AdvancedFilters
setAdvancedFilters: (x: AdvancedFilters) => void
}) => {
@ -161,10 +165,6 @@ const ActivityFilters = ({
const actions = mangoStore((s) => s.actions)
const loadActivityFeed = mangoStore((s) => s.activityFeed.loading)
const { mangoAccount } = useMangoAccount()
const [showAdvancedFiltersModal, setShowAdvancedFiltersModal] =
useState(false)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false
const [showMobileFilters, setShowMobileFilters] = useState(false)
const [hasFilters, setHasFilters] = useState(false)
@ -197,80 +197,16 @@ const ActivityFilters = ({
await actions.fetchActivityFeed(mangoAccount.publicKey.toString())
setAdvancedFilters(DEFAULT_ADVANCED_FILTERS)
setFilters(DEFAULT_FILTERS)
setParams(DEFAULT_PARAMS)
}
}, [actions])
const handleUpdateModalResults = () => {
handleUpdateResults()
setShowAdvancedFiltersModal(false)
}
const handleUpdateMobileResults = () => {
handleUpdateResults()
setShowMobileFilters(false)
}
return mangoAccount ? (
!isMobile ? (
<>
<div className="flex items-center justify-between border-b border-th-bkg-3 bg-th-bkg-2 pl-6">
<h3 className="flex items-center whitespace-nowrap pr-6 text-sm">
{t('activity:filter-results')}
</h3>
<ActivityTypeFiltersForm
filters={filters}
updateFilters={updateFilters}
/>
<div className="flex h-12 items-center justify-between border-l border-th-bkg-4 p-6">
<LinkButton
className="whitespace-nowrap text-sm"
onClick={() => setShowAdvancedFiltersModal(true)}
>
{t('activity:advanced-filters')}
</LinkButton>
{hasFilters ? (
<Tooltip content={t('activity:reset-filters')}>
<IconButton
className={`ml-4 ${loadActivityFeed ? 'animate-spin' : ''}`}
onClick={() => handleResetFilters()}
size="small"
>
<ArrowPathIcon className="h-5 w-5" />
</IconButton>
</Tooltip>
) : null}
</div>
<Button
className="rounded-none"
size="large"
onClick={handleUpdateResults}
>
{t('activity:update')}
</Button>
</div>
{showAdvancedFiltersModal ? (
<Modal
isOpen={showAdvancedFiltersModal}
onClose={() => setShowAdvancedFiltersModal(false)}
>
<h2 className="mb-2 text-center">
{t('activity:advanced-filters')}
</h2>
<AdvancedFiltersForm
advancedFilters={advancedFilters}
setAdvancedFilters={setAdvancedFilters}
/>
<Button
className="w-full"
size="large"
onClick={handleUpdateModalResults}
>
{t('activity:update')}
</Button>
</Modal>
) : null}
</>
) : (
<Disclosure>
<div className="relative">
{hasFilters ? (
@ -289,15 +225,17 @@ const ActivityFilters = ({
<div
onClick={() => setShowMobileFilters(!showMobileFilters)}
role="button"
className={`default-transition w-full border-b border-th-bkg-3 bg-th-bkg-2 p-4 hover:bg-th-bkg-3`}
className={`default-transition w-full border-b border-th-bkg-3 bg-th-bkg-2 p-4 hover:bg-th-bkg-3 md:px-6`}
>
<Disclosure.Button
className={`flex h-full w-full items-center justify-between rounded-none`}
>
<div className="flex items-center space-x-2">
<AdjustmentsVerticalIcon className="h-5 w-5 text-th-fgd-4" />
<span className="font-bold text-th-fgd-1">
{t('activity:filter-results')}
</span>
</div>
<ChevronDownIcon
className={`${
showMobileFilters ? 'rotate-180' : 'rotate-360'
@ -329,7 +267,7 @@ const ActivityFilters = ({
setAdvancedFilters={setAdvancedFilters}
/>
<Button
className="w-full"
className="w-full md:w-auto"
size="large"
onClick={handleUpdateMobileResults}
>
@ -338,7 +276,6 @@ const ActivityFilters = ({
</Disclosure.Panel>
</Transition>
</Disclosure>
)
) : null
}
@ -351,7 +288,7 @@ const ActivityTypeFiltersForm = ({
}) => {
const { t } = useTranslation('activity')
return (
<div className="flex w-full flex-col space-y-2 md:flex-row md:space-y-0">
<div className="flex w-full flex-col space-y-3 md:flex-row md:space-y-0">
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.deposit}
@ -376,6 +313,14 @@ const ActivityTypeFiltersForm = ({
<span className="text-sm">{t('swaps')}</span>
</Checkbox>
</div>
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.perp_trade}
onChange={(e) => updateFilters(e, 'perp_trade')}
>
<span className="text-sm">{t('perps')}</span>
</Checkbox>
</div>
<div className="flex h-8 flex-1 items-center lg:h-12 lg:border-l lg:border-th-bkg-4 lg:p-4">
<Checkbox
checked={filters.liquidate_token_with_token}

View File

@ -102,6 +102,11 @@ const ActivityFeedTable = ({
symbol: swap_in_symbol,
}
}
if (activity_type === 'perp_trade') {
const { perp_market, price, quantity } = activity.activity_details
credit = { value: quantity, symbol: perp_market }
debit = { value: formatDecimal(quantity * price * -1), symbol: 'USDC' }
}
return { credit, debit }
}
@ -134,6 +139,11 @@ const ActivityFeedTable = ({
(swap_in_amount + loan_origination_fee) * swap_in_price_usd -
swap_out_amount * swap_out_price_usd
}
if (activity_type === 'perp_trade') {
const { maker_fee, price, quantity, taker_fee } =
activity.activity_details
value = (quantity * price + maker_fee + taker_fee) * -1
}
return value
}
@ -309,6 +319,7 @@ const MobileActivityFeedItem = ({
const { signature } = activity.activity_details
const isLiquidation = activity_type === 'liquidate_token_with_token'
const isSwap = activity_type === 'swap'
const isPerp = activity_type === 'perp_trade'
const activityName = isLiquidation ? 'liquidation' : activity_type
const value = getValue(activity)
return (
@ -352,6 +363,26 @@ const MobileActivityFeedItem = ({
{activity.activity_details.swap_out_symbol}
</span>
</>
) : isPerp ? (
<>
<span
className={`mr-1 font-body ${
activity.activity_details.taker_side === 'bid'
? 'text-th-green'
: 'text-th-red'
}`}
>
{activity.activity_details.taker_side === 'bid'
? 'BUY'
: 'SELL'}
</span>
<span className="mr-1">
{activity.activity_details.quantity}
</span>
<span className="font-body text-th-fgd-3">
{activity.activity_details.perp_market}
</span>
</>
) : (
<>
<span className="mr-1">

View File

@ -1,10 +1,10 @@
import { FunctionComponent } from 'react'
interface ChartRangeButtonsProps {
activeValue: number
activeValue: string
className?: string
onChange: (x: number) => void
values: Array<number>
onChange: (x: string) => void
values: Array<string>
names?: Array<string>
}
@ -20,7 +20,7 @@ const ChartRangeButtons: FunctionComponent<ChartRangeButtonsProps> = ({
<div className="relative flex">
{activeValue && values.includes(activeValue) ? (
<div
className={`default-transition absolute left-0 top-0 h-full transform rounded-md bg-th-bkg-2`}
className={`default-transition absolute left-0 top-0 h-full transform rounded-md bg-th-bkg-3`}
style={{
transform: `translateX(${
values.findIndex((v) => v === activeValue) * 100

View File

@ -30,11 +30,11 @@ dayjs.extend(relativeTime)
interface DetailedAreaChartProps {
data: any[]
daysToShow: number
daysToShow: string
hideChange?: boolean
hideChart?: () => void
loading?: boolean
setDaysToShow: (x: number) => void
setDaysToShow: (x: string) => void
tickFormat?: (x: any) => string
title?: string
xKey: string
@ -51,7 +51,7 @@ export const formatDateAxis = (date: string, days: number) => {
const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
data,
daysToShow = 1,
daysToShow = '1',
hideChange,
hideChart,
loading,
@ -183,7 +183,7 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}
values={[1, 7, 30]}
values={['1', '7', '30']}
onChange={(v) => setDaysToShow(v)}
/>
</div>
@ -252,7 +252,9 @@ const DetailedAreaChart: FunctionComponent<DetailedAreaChartProps> = ({
fontSize: 10,
}}
tickLine={false}
tickFormatter={(d) => formatDateAxis(d, daysToShow)}
tickFormatter={(d) =>
formatDateAxis(d, parseInt(daysToShow))
}
/>
<YAxis
axisLine={false}

View File

@ -0,0 +1,45 @@
import { StarIcon } from '@heroicons/react/24/outline'
import { StarIcon as FilledStarIcon } from '@heroicons/react/20/solid'
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { FAVORITE_MARKETS_KEY } from 'utils/constants'
const FavoriteMarketButton = ({
market,
}: {
market: PerpMarket | Serum3Market
}) => {
const [favoriteMarkets, setFavoriteMarkets] = useLocalStorageState(
FAVORITE_MARKETS_KEY,
[]
)
const addToFavorites = (marketName: string) => {
const newFavorites: any = [...favoriteMarkets, marketName]
setFavoriteMarkets(newFavorites)
}
const removeFromFavorites = (marketName: string) => {
setFavoriteMarkets(favoriteMarkets.filter((m: string) => m !== marketName))
}
return favoriteMarkets.find(
(marketName: string) => marketName === market.name
) ? (
<button
className="default-transition flex items-center justify-center text-th-primary md:hover:text-th-fgd-3"
onClick={() => removeFromFavorites(market.name)}
>
<FilledStarIcon className="h-5 w-5" />
</button>
) : (
<button
className="default-transition flex items-center justify-center text-th-fgd-4 md:hover:text-th-primary"
onClick={() => addToFavorites(market.name)}
>
<StarIcon className="h-5 w-5" />
</button>
)
}
export default FavoriteMarketButton

View File

@ -76,7 +76,7 @@ const SwapTokenChart = () => {
const [baseTokenId, setBaseTokenId] = useState(inputCoingeckoId)
const [quoteTokenId, setQuoteTokenId] = useState(outputCoingeckoId)
const [mouseData, setMouseData] = useState<any>(null)
const [daysToShow, setDaysToShow] = useState(1)
const [daysToShow, setDaysToShow] = useState('1')
const { theme } = useTheme()
const [animationSettings] = useLocalStorageState(
ANIMATION_SETTINGS_KEY,
@ -245,7 +245,7 @@ const SwapTokenChart = () => {
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}
values={[1, 7, 30]}
values={['1', '7', '30']}
onChange={(v) => setDaysToShow(v)}
/>
</div>

View File

@ -1,115 +1,13 @@
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import Change from '@components/shared/Change'
import TabUnderline from '@components/shared/TabUnderline'
import { Popover } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import { useCoingecko } from 'hooks/useCoingecko'
import useOraclePrice from 'hooks/useOraclePrice'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import { DEFAULT_MARKET_NAME } from 'utils/constants'
import { useMemo } from 'react'
import { formatFixedDecimals } from 'utils/numbers'
import MarketLogos from './MarketLogos'
const MarketSelectDropdown = () => {
const { selectedMarket } = useSelectedMarket()
const serumMarkets = mangoStore((s) => s.serumMarkets)
const perpMarkets = mangoStore((s) => s.perpMarkets)
const [activeTab, setActiveTab] = useState('perp')
return (
<Popover>
{({ open }) => (
<div
className="relative flex flex-col overflow-visible"
id="trade-step-one"
>
<Popover.Button className="default-transition flex w-full items-center justify-between hover:text-th-primary">
<>
{selectedMarket ? <MarketLogos market={selectedMarket} /> : null}
</>
<div className="text-xl font-bold text-th-fgd-1 md:text-base">
{selectedMarket?.name || DEFAULT_MARKET_NAME}
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</Popover.Button>
<Popover.Panel className="absolute -left-5 top-[46px] z-50 mr-4 w-screen bg-th-bkg-2 pb-2 pt-4 sm:w-56 md:top-[37px]">
<TabUnderline
activeValue={activeTab}
onChange={(v) => setActiveTab(v)}
small
values={['perp', 'spot']}
/>
{activeTab === 'spot'
? serumMarkets?.length
? serumMarkets.map((m) => {
return (
<Link
href={{
pathname: '/trade',
query: { name: m.name },
}}
key={m.publicKey.toString()}
shallow={true}
>
<div className="default-transition flex items-center py-2 px-4 hover:cursor-pointer hover:bg-th-bkg-2">
<MarketLogos market={m} />
<span
className={
m.name === selectedMarket?.name
? 'text-th-primary'
: ''
}
>
{m.name}
</span>
</div>
</Link>
)
})
: null
: null}
{activeTab === 'perp'
? perpMarkets?.length
? perpMarkets.map((m) => {
return (
<Link
href={{
pathname: '/trade',
query: { name: m.name },
}}
key={m.publicKey.toString()}
shallow={true}
>
<div className="default-transition flex items-center py-2 px-4 hover:cursor-pointer hover:bg-th-bkg-2">
<MarketLogos market={m} />
<span
className={
m.name === selectedMarket?.name
? 'text-th-primary'
: ''
}
>
{m.name}
</span>
</div>
</Link>
)
})
: null
: null}
</Popover.Panel>
</div>
)}
</Popover>
)
}
import MarketSelectDropdown from './MarketSelectDropdown'
import PerpFundingRate from './PerpFundingRate'
const AdvancedMarketHeader = () => {
const { t } = useTranslation(['common', 'trade'])
@ -154,14 +52,13 @@ const AdvancedMarketHeader = () => {
<div className="ml-6 flex-col">
<div className="text-xs text-th-fgd-4">{t('rolling-change')}</div>
<Change change={change} size="small" />
{/* <div
className={`font-mono text-xs ${
change < 0 ? 'text-th-red' : 'text-th-gree'
}`}
>
{isNaN(change) ? '0.00' : change.toFixed(2)}%
</div> */}
</div>
{selectedMarket instanceof PerpMarket ? (
<div className="ml-6 flex-col">
<div className="text-xs text-th-fgd-4">Funding Rate</div>
<PerpFundingRate />
</div>
) : null}
</div>
)
}

View File

@ -0,0 +1,77 @@
import { Transition } from '@headlessui/react'
import { StarIcon } from '@heroicons/react/20/solid'
import useLocalStorageState from 'hooks/useLocalStorageState'
import useMangoGroup from 'hooks/useMangoGroup'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useViewport } from 'hooks/useViewport'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FAVORITE_MARKETS_KEY } from 'utils/constants'
import { breakpoints } from 'utils/theme'
import MarketLogos from './MarketLogos'
const FavoriteMarketsBar = () => {
const [favoriteMarkets] = useLocalStorageState(FAVORITE_MARKETS_KEY, [])
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { asPath } = useRouter()
const { selectedMarket } = useSelectedMarket()
const { group } = useMangoGroup()
return !isMobile ? (
<Transition
className="flex items-center space-x-4 overflow-hidden border-b border-th-bkg-3 px-6"
show={!!favoriteMarkets.length}
enter="transition-all ease-in duration-200"
enterFrom="opacity-0 h-0"
enterTo="opacity-100 h-8"
leave="transition-all ease-out duration-200"
leaveFrom="opacity-100 h-8"
leaveTo="opacity-0 h-0"
>
<StarIcon className="h-4 w-4 text-th-fgd-4" />
{favoriteMarkets.map((mkt: string) => {
// const change24h = marketsInfo?.find((m) => m.name === mkt)?.change24h
const isPerp = mkt.includes('PERP')
let market
if (isPerp) {
market = group?.getPerpMarketByName(mkt)
} else {
market = group?.getSerum3MarketByName(mkt)
}
return (
<Link href={`/trade?name=${mkt}`} key={mkt} shallow={true}>
<div
className={`default-transition flex items-center whitespace-nowrap py-1 text-xs hover:text-th-primary hover:opacity-100 ${
asPath.includes(mkt) ||
(asPath === '/trade' &&
selectedMarket &&
selectedMarket.name === mkt)
? 'text-th-primary'
: 'text-th-fgd-1 opacity-60'
}`}
>
{market ? <MarketLogos market={market} small /> : null}
<span className="mb-0 mr-1.5 text-xs">{mkt}</span>
{/* {change24h ? (
<div
className={`text-xs ${
change24h
? change24h >= 0
? 'text-th-green'
: 'text-th-red'
: 'text-th-fgd-4'
}`}
>
{`${(change24h * 100).toFixed(1)}%`}
</div>
) : null} */}
</div>
</Link>
)
})}
</Transition>
) : null
}
export default FavoriteMarketsBar

View File

@ -5,7 +5,13 @@ import Image from 'next/legacy/image'
import { useMemo } from 'react'
import useMangoGroup from 'hooks/useMangoGroup'
const MarketLogos = ({ market }: { market: Serum3Market | PerpMarket }) => {
const MarketLogos = ({
market,
small,
}: {
market: Serum3Market | PerpMarket
small?: boolean
}) => {
const { group } = useMangoGroup()
const { mangoTokens } = useJupiterMints()
@ -39,8 +45,14 @@ const MarketLogos = ({ market }: { market: Serum3Market | PerpMarket }) => {
return (
<div
className={`relative mr-1.5 h-5 ${
market instanceof Serum3Market ? 'w-[34px]' : 'w-[20px]'
className={`relative mr-1.5 ${small ? 'h-4' : 'h-5'} ${
market instanceof Serum3Market
? small
? 'w-[27px]'
: 'w-[34px]'
: small
? 'w-[16px]'
: 'w-[20px]'
}`}
>
<div className="absolute left-0 top-0">
@ -48,12 +60,14 @@ const MarketLogos = ({ market }: { market: Serum3Market | PerpMarket }) => {
<Image
alt=""
className="z-10 rounded-full drop-shadow-md"
width="20"
height="20"
width={small ? '16' : '20'}
height={small ? '16' : '20'}
src={logos.baseLogoURI}
/>
) : (
<QuestionMarkCircleIcon className="h-5 w-5 text-th-fgd-3" />
<QuestionMarkCircleIcon
className={`${small ? 'h-4 w-4' : 'h-5 w-5'} text-th-fgd-3`}
/>
)}
</div>
<div className="absolute right-0 top-0">
@ -61,12 +75,14 @@ const MarketLogos = ({ market }: { market: Serum3Market | PerpMarket }) => {
<Image
alt=""
className="rounded-full opacity-60"
width="20"
height="20"
width={small ? '16' : '20'}
height={small ? '16' : '20'}
src={logos.quoteLogoURI}
/>
) : market instanceof PerpMarket ? null : (
<QuestionMarkCircleIcon className="h-5 w-5 text-th-fgd-3" />
<QuestionMarkCircleIcon
className={`${small ? 'h-4 w-4' : 'h-5 w-5'} text-th-fgd-3`}
/>
)}
</div>
</div>

View File

@ -0,0 +1,153 @@
import ChartRangeButtons from '@components/shared/ChartRangeButtons'
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
import TabUnderline from '@components/shared/TabUnderline'
import { Popover } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import { DEFAULT_MARKET_NAME } from 'utils/constants'
import MarketLogos from './MarketLogos'
const MarketSelectDropdown = () => {
const { selectedMarket } = useSelectedMarket()
const serumMarkets = mangoStore((s) => s.serumMarkets)
const perpMarkets = mangoStore((s) => s.perpMarkets)
const [activeTab, setActiveTab] = useState('perp')
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
const spotBaseTokens: string[] = useMemo(() => {
if (serumMarkets.length) {
const baseTokens: string[] = []
serumMarkets.map((m) => {
const base = m.name.split('/')[1]
if (!baseTokens.includes(base)) {
baseTokens.push(base)
}
})
return baseTokens
}
return []
}, [serumMarkets])
return (
<Popover>
{({ open }) => (
<div
className="relative flex flex-col overflow-visible"
id="trade-step-one"
>
<Popover.Button className="default-transition flex w-full items-center justify-between hover:text-th-primary">
<>
{selectedMarket ? <MarketLogos market={selectedMarket} /> : null}
</>
<div className="text-xl font-bold text-th-fgd-1 md:text-base">
{selectedMarket?.name || DEFAULT_MARKET_NAME}
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</Popover.Button>
<Popover.Panel className="absolute -left-5 top-[46px] z-50 mr-4 w-screen bg-th-bkg-2 pb-2 pt-4 sm:w-72 md:top-[37px]">
<TabUnderline
activeValue={activeTab}
onChange={(v) => setActiveTab(v)}
small
values={['perp', 'spot']}
/>
{activeTab === 'spot' ? (
serumMarkets?.length ? (
<>
<div className="mb-2 w-56 px-4">
<ChartRangeButtons
activeValue={spotBaseFilter}
values={['All', ...spotBaseTokens]}
onChange={(v) => setSpotBaseFilter(v)}
/>
</div>
{serumMarkets
.filter((mkt) => {
if (spotBaseFilter === 'All') {
return mkt
} else {
return mkt.name.split('/')[1] === spotBaseFilter
}
})
.map((m) => {
return (
<div
className="flex items-center justify-between py-2 px-4"
key={m.publicKey.toString()}
>
<Link
href={{
pathname: '/trade',
query: { name: m.name },
}}
shallow={true}
>
<div className="default-transition flex items-center hover:cursor-pointer hover:text-th-primary">
<MarketLogos market={m} />
<span
className={
m.name === selectedMarket?.name
? 'text-th-primary'
: ''
}
>
{m.name}
</span>
</div>
</Link>
<FavoriteMarketButton market={m} />
</div>
)
})}
</>
) : null
) : null}
{activeTab === 'perp'
? perpMarkets?.length
? perpMarkets.map((m) => {
return (
<div
className="flex items-center justify-between py-2 px-4"
key={m.publicKey.toString()}
>
<Link
href={{
pathname: '/trade',
query: { name: m.name },
}}
shallow={true}
>
<div className="default-transition flex items-center hover:cursor-pointer hover:bg-th-bkg-2">
<MarketLogos market={m} />
<span
className={
m.name === selectedMarket?.name
? 'text-th-primary'
: ''
}
>
{m.name}
</span>
</div>
</Link>
<FavoriteMarketButton market={m} />
</div>
)
})
: null
: null}
</Popover.Panel>
</div>
)}
</Popover>
)
}
export default MarketSelectDropdown

View File

@ -25,19 +25,26 @@ import {
import useSelectedMarket from 'hooks/useSelectedMarket'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
function decodeBookL2(
export const decodeBookL2 = (book: SpotOrderBook | BookSide): number[][] => {
const depth = 40
if (book instanceof SpotOrderBook) {
book.getL2(depth).map(([price, size]) => [price, size])
} else if (book instanceof BookSide) {
return book.getL2Ui(depth)
}
return []
}
function decodeBook(
client: MangoClient,
market: Market | PerpMarket,
accInfo: AccountInfo<Buffer>,
side: 'bids' | 'asks'
): number[][] {
if (market && accInfo?.data) {
const depth = 40
): SpotOrderBook | BookSide {
if (market instanceof Market) {
const book = SpotOrderBook.decode(market, accInfo.data)
return book.getL2(depth).map(([price, size]) => [price, size])
} else if (market instanceof PerpMarket) {
// FIXME: Review the null being passed here
return book
} else {
const decodedAcc = client.program.coder.accounts.decode(
'bookSide',
accInfo.data
@ -48,11 +55,9 @@ function decodeBookL2(
side === 'bids' ? BookSideType.bids : BookSideType.asks,
decodedAcc
)
return book.getL2Ui(depth)
return book
}
}
return []
}
// export function decodeBook(
// market: Market,
@ -327,14 +332,10 @@ const Orderbook = () => {
if (bidsPk) {
connection.getAccountInfo(bidsPk).then((info) => {
if (!info) return
const decodedBook = decodeBook(client, market, info, 'bids')
set((state) => {
// state.accountInfos[bidsPk.toString()] = info
state.selectedMarket.orderbook.bids = decodeBookL2(
client,
market,
info,
'bids'
)
state.selectedMarket.bidsAccount = decodedBook
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
})
})
console.log('bidsPk', bidsPk)
@ -347,15 +348,10 @@ const Orderbook = () => {
previousBidInfo.lamports !== info.lamports
) {
previousBidInfo = info
// info['parsed'] = decodeBook(serum3MarketExternal, info)
const decodedBook = decodeBook(client, market, info, 'bids')
set((state) => {
// state.accountInfos[bidsPk.toString()] = info
state.selectedMarket.orderbook.bids = decodeBookL2(
client,
market,
info,
'bids'
)
state.selectedMarket.bidsAccount = decodedBook
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
})
}
}
@ -367,14 +363,10 @@ const Orderbook = () => {
if (asksPk) {
connection.getAccountInfo(asksPk).then((info) => {
if (!info) return
const decodedBook = decodeBook(client, market, info, 'asks')
set((state) => {
// state.accountInfos[bidsPk.toString()] = info
state.selectedMarket.orderbook.asks = decodeBookL2(
client,
market,
info,
'bids'
)
state.selectedMarket.asksAccount = decodedBook
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
})
})
askSubscriptionId = connection.onAccountChange(
@ -386,15 +378,10 @@ const Orderbook = () => {
previousAskInfo.lamports !== info.lamports
) {
previousAskInfo = info
// info['parsed'] = decodeBook(serum3MarketExternal, info)
const decodedBook = decodeBook(client, market, info, 'asks')
set((state) => {
// state.accountInfos[asksPk.toString()] = info
state.selectedMarket.orderbook.asks = decodeBookL2(
client,
market,
info,
'asks'
)
state.selectedMarket.asksAccount = decodedBook
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
})
}
}

View File

@ -0,0 +1,21 @@
import { BookSide, PerpMarket } from '@blockworks-foundation/mango-v4'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
const PerpFundingRate = () => {
const { selectedMarket } = useSelectedMarket()
const bids = mangoStore((s) => s.selectedMarket.bidsAccount)
const asks = mangoStore((s) => s.selectedMarket.asksAccount)
return (
<div className="font-mono text-xs text-th-fgd-2">
{selectedMarket instanceof PerpMarket &&
bids instanceof BookSide &&
asks instanceof BookSide
? selectedMarket.getCurrentFundingRate(bids, asks)
: '-'}
</div>
)
}
export default PerpFundingRate

View File

@ -14,6 +14,7 @@ import MobileTradeAdvancedPage from './MobileTradeAdvancedPage'
import OrderbookAndTrades from './OrderbookAndTrades'
import { useWallet } from '@solana/wallet-adapter-react'
import TradeOnboardingTour from '@components/tours/TradeOnboardingTour'
import FavoriteMarketsBar from './FavoriteMarketsBar'
const TradingViewChart = dynamic(() => import('./TradingViewChart'), {
ssr: false,
@ -149,6 +150,7 @@ const TradeAdvancedPage = () => {
<MobileTradeAdvancedPage />
) : (
<>
<FavoriteMarketsBar />
<ResponsiveGridLayout
// layouts={savedLayouts ? savedLayouts : defaultLayouts}
layouts={defaultLayouts}

View File

@ -86,7 +86,7 @@ const Token: NextPage = () => {
const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko()
const [chartData, setChartData] = useState<{ prices: any[] } | null>(null)
const [loadChartData, setLoadChartData] = useState(true)
const [daysToShow, setDaysToShow] = useState<number>(1)
const [daysToShow, setDaysToShow] = useState<string>('1')
const [animationSettings] = useLocalStorageState(
ANIMATION_SETTINGS_KEY,
INITIAL_ANIMATION_SETTINGS
@ -177,11 +177,11 @@ const Token: NextPage = () => {
} = coingeckoData ? coingeckoData.market_data : DEFAULT_COINGECKO_VALUES
const loadingChart = useMemo(() => {
return daysToShow == 1 ? loadingPrices : loadChartData
return daysToShow == '1' ? loadingPrices : loadChartData
}, [loadChartData, loadingPrices])
const coingeckoTokenPrices = useMemo(() => {
if (daysToShow === 1 && coingeckoPrices.length && bank) {
if (daysToShow === '1' && coingeckoPrices.length && bank) {
const tokenPriceData = coingeckoPrices.find(
(asset) => asset.symbol === bank.name
)
@ -197,8 +197,8 @@ const Token: NextPage = () => {
return []
}, [coingeckoPrices, bank, daysToShow, chartData, loadingChart])
const handleDaysToShow = async (days: number) => {
if (days !== 1) {
const handleDaysToShow = async (days: string) => {
if (days !== '1') {
try {
const response = await fetch(
`https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`
@ -406,12 +406,12 @@ const Token: NextPage = () => {
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}
values={[1, 7, 30]}
values={['1', '7', '30']}
onChange={(v) => handleDaysToShow(v)}
/>
</div>
<PriceChart
daysToShow={daysToShow}
daysToShow={parseInt(daysToShow)}
prices={coingeckoTokenPrices}
/>
</>

View File

@ -13,6 +13,8 @@
"liquidation": "Liquidation",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"perps": "Perps",
"perp_trade": "Perp",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"swap": "Swap",

View File

@ -13,6 +13,8 @@
"liquidation": "Liquidation",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"perps": "Perps",
"perp_trade": "Perp",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"swap": "Swap",

View File

@ -13,6 +13,8 @@
"liquidation": "Liquidation",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"perps": "Perps",
"perp_trade": "Perp",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"swap": "Swap",

View File

@ -13,6 +13,8 @@
"liquidation": "Liquidation",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"perps": "Perps",
"perp_trade": "Perp",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"swap": "Swap",

View File

@ -13,6 +13,8 @@
"liquidation": "Liquidation",
"liquidations": "Liquidations",
"liquidation-details": "Liquidation Details",
"perps": "Perps",
"perp_trade": "Perp",
"reset-filters": "Reset Filters",
"select-tokens": "Select Tokens",
"swap": "Swap",

View File

@ -5,6 +5,7 @@ import { subscribeWithSelector } from 'zustand/middleware'
import { AnchorProvider, Wallet, web3 } from '@project-serum/anchor'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import { OpenOrders, Order } from '@project-serum/serum/lib/market'
import { Orderbook as SpotOrderBook } from '@project-serum/serum'
import { Wallet as WalletAdapter } from '@solana/wallet-adapter-react'
import {
MangoClient,
@ -15,6 +16,7 @@ import {
Bank,
PerpOrder,
PerpPosition,
BookSide,
} from '@blockworks-foundation/mango-v4'
import EmptyWallet from '../utils/wallet'
@ -194,6 +196,8 @@ export type MangoStore = {
name: string
current: Serum3Market | PerpMarket | undefined
fills: any
bidsAccount: BookSide | SpotOrderBook | undefined
asksAccount: BookSide | SpotOrderBook | undefined
orderbook: Orderbook
}
serumMarkets: Serum3Market[]
@ -295,6 +299,8 @@ const mangoStore = create<MangoStore>()(
name: DEFAULT_MARKET_NAME,
current: undefined,
fills: [],
bidsAccount: undefined,
asksAccount: undefined,
orderbook: {
bids: [],
asks: [],

View File

@ -48,3 +48,5 @@ export const GRID_LAYOUT_KEY = 'savedLayouts-0.1'
export const NOTIFICATION_POSITION_KEY = 'notificationPosition'
export const MIN_SOL_BALANCE = 0.04
export const FAVORITE_MARKETS_KEY = 'favoriteMarkets'