mrege main

This commit is contained in:
saml33 2022-09-30 22:39:38 +10:00
commit bc55ef9204
31 changed files with 464 additions and 278 deletions

View File

@ -35,7 +35,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
}, [width])
useEffect(() => {
const animationFrames = 5
const animationFrames = 15
for (let x = 1; x <= animationFrames; x++) {
setTimeout(() => {
@ -55,7 +55,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<BounceLoader />
</div>
) : null}
<div className="flex-grow bg-th-bkg-1 text-th-fgd-1 transition-all">
<div className="flex-grow bg-th-bkg-1 text-th-fgd-2 transition-all">
<div className="flex">
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<BottomBar />

View File

@ -79,7 +79,7 @@ const MangoAccountsList = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Panel className="absolute top-[13.5px] -right-5 z-10 mr-4 w-56 rounded-md rounded-t-none border border-th-bkg-3 bg-th-bkg-1 p-4">
<Popover.Panel className="absolute top-[13.5px] -right-5 z-10 mr-4 w-56 rounded-md rounded-t-none border border-th-bkg-2 bg-th-bkg-3 p-4 text-th-fgd-3">
{loading ? (
<Loading />
) : mangoAccounts.length ? (
@ -87,7 +87,7 @@ const MangoAccountsList = ({
<div key={acc.publicKey.toString()}>
<button
onClick={() => handleSelectMangoAccount(acc)}
className="mb-3 flex w-full items-center justify-between border-b border-th-bkg-3 pb-3"
className="mb-3 flex w-full items-center justify-between border-b border-th-bkg-4 pb-3 hover:text-th-fgd-1"
>
{acc.name}
{acc.publicKey.toString() ===

View File

@ -28,7 +28,7 @@ const OnboardingTour = () => {
onClick={handleClose}
className={`absolute right-4 top-4 z-50 text-th-bkg-3 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
>
<XMarkIcon className={`h-5 w-5`} />
<XMarkIcon className={`h-6 w-6`} />
</button>
<h3 className="text-th-bkg-1">{title}</h3>
<p className="text-sm text-th-bkg-1">{description}</p>

View File

@ -42,7 +42,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
return (
<div
className={`transition-all duration-500 ${
className={`transition-all duration-300 ${
collapsed ? 'w-[64px]' : 'w-44 lg:w-48 xl:w-52'
} border-r border-th-bkg-3 bg-th-bkg-1`}
>
@ -50,7 +50,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
<div className="my-2">
<Link href={'/'} shallow={true} passHref>
<div
className={`h-14 items-center transition-all duration-500 ease-in-out ${
className={`h-14 items-center transition-all duration-300 ease-in-out ${
collapsed ? '' : 'justify-start'
} pb-1 pt-2 pl-4`}
>
@ -323,26 +323,16 @@ export const ExpandableMenuItem = ({
{icon}
</div>
</Popover.Button>
<Transition
show={showMenu}
as={Fragment}
enter="transition ease-in duration-300"
enterFrom="opacity-0 scale-90"
enterTo="opacity-100 scale-100"
leave="transition ease-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
<Popover.Panel
className={`absolute z-20 w-56 rounded-md rounded-l-none border border-th-bkg-3 bg-th-bkg-1 py-2 ${
alignBottom
? 'bottom-0 left-[63px] rounded-b-none border-b-0 p-0'
: 'top-1/2 left-[63px] -translate-y-1/2'
}`}
>
<Popover.Panel
className={`absolute z-20 w-56 rounded-md rounded-l-none border border-th-bkg-3 bg-th-bkg-1 py-2 ${
alignBottom
? 'bottom-0 left-[63px] rounded-b-none border-b-0 p-0'
: 'top-1/2 left-[63px] -translate-y-1/2'
}`}
>
{children}
</Popover.Panel>
</Transition>
{children}
</Popover.Panel>
</div>
</Popover>
) : (

View File

@ -1,16 +1,20 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import mangoStore from '@store/mangoStore'
import TabButtons from '../shared/TabButtons'
import TokenList from '../TokenList'
import SwapHistoryTable from '../swap/SwapHistoryTable'
import { useRouter } from 'next/router'
import ActivityFeed from './ActivityFeed'
const TABS = ['balances', 'activity:activity', 'swap:swap-history']
const AccountTabs = () => {
const [activeTab, setActiveTab] = useState('balances')
const actions = mangoStore((s) => s.actions)
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const { pathname } = useRouter()
const tabsWithCount: [string, number][] = useMemo(() => {
return TABS.map((t) => [t, 0])
}, [])
useEffect(() => {
if (mangoAccount) {
@ -23,7 +27,7 @@ const AccountTabs = () => {
<TabButtons
activeValue={activeTab}
onChange={(v) => setActiveTab(v)}
values={['balances', 'activity:activity', 'swap:swap-history']}
values={tabsWithCount}
showBorders
/>
<TabContent activeTab={activeTab} />

View File

@ -12,6 +12,7 @@ import {
ChevronRightIcon,
CurrencyDollarIcon as FeesIcon,
LightBulbIcon,
ArrowsRightLeftIcon,
} from '@heroicons/react/20/solid'
const StyledBarItemLabel = ({
@ -58,18 +59,18 @@ const BottomBar = () => {
asPath === '/swap' ? 'text-th-primary' : 'text-th-fgd-3'
} col-span-1 flex cursor-pointer flex-col items-center`}
>
<TradeIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('trade')}</StyledBarItemLabel>
<ArrowsRightLeftIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('swap')}</StyledBarItemLabel>
</a>
</Link>
<Link href="/stats" shallow={true}>
<Link href="/trade" shallow={true}>
<a
className={`${
asPath === '/stats' ? 'text-th-primary' : 'text-th-fgd-3'
asPath === '/trade' ? 'text-th-primary' : 'text-th-fgd-3'
} col-span-1 flex cursor-pointer flex-col items-center`}
>
<ChartBarIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('stats')}</StyledBarItemLabel>
<TradeIcon className="mb-1 h-4 w-4" />
<StyledBarItemLabel>{t('trade')}</StyledBarItemLabel>
</a>
</Link>
<a
@ -99,19 +100,24 @@ const MoreMenuPanel = ({
const { t } = useTranslation('common')
return (
<div
className={`fixed bottom-0 z-30 h-96 w-full overflow-hidden bg-th-bkg-2 px-4 transition duration-500 ease-in-out ${
className={`fixed bottom-0 z-30 h-96 w-full overflow-hidden rounded-t-3xl bg-th-bkg-2 px-4 transition duration-300 ease-in-out ${
showPanel ? 'translate-y-0' : 'translate-y-full'
}`}
>
<div className="flex justify-end py-4">
<IconButton onClick={() => setShowPanel(false)} hideBg>
<XMarkIcon className="h-5 w-5" />
<XMarkIcon className="h-6 w-6" />
</IconButton>
</div>
<div
className="border-b border-th-bkg-4"
onClick={() => setShowPanel(false)}
>
<MoreMenuItem
title={t('stats')}
path="/stats"
icon={<ChartBarIcon className="h-5 w-5" />}
/>
<MoreMenuItem
title={t('fees')}
path="/fees"
@ -140,7 +146,7 @@ const MoreMenuItem = ({
isExternal?: boolean
}) => {
const classNames =
'default-transition flex w-full items-center justify-between border-t border-th-bkg-4 px-2 py-3 text-th-fgd-2 hover:text-th-fgd-1'
'default-transition flex w-full items-center justify-between border-t border-th-bkg-4 px-2 py-4 text-th-fgd-2 hover:text-th-fgd-1'
return isExternal ? (
<a
className={classNames}

View File

@ -49,15 +49,15 @@ const IconDropMenu = ({
}`}
disabled={disabled}
>
{open ? <XMarkIcon className="h-5 w-5" /> : icon}
{open ? <XMarkIcon className="h-6 w-6" /> : icon}
</Popover.Button>
<Transition
appear={true}
show={open}
as={Fragment}
enter="transition ease-in duration-100"
enterFrom="opacity-0 scale-90"
enterTo="opacity-100 scale-100"
enterFrom="scale-90"
enterTo="scale-100"
leave="transition ease-out duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"

View File

@ -26,7 +26,7 @@ function Modal({
>
<div className="min-h-screen px-4 text-center">
<Dialog.Overlay
className={`fixed inset-0 bg-black opacity-50 ${
className={`fixed inset-0 backdrop-blur-sm ${
disableOutsideClose ? 'pointer-events-none' : ''
}`}
/>
@ -39,7 +39,7 @@ function Modal({
onClick={onClose}
className={`absolute right-4 top-4 z-50 text-th-fgd-4 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
>
<XMarkIcon className={`h-5 w-5`} />
<XMarkIcon className={`h-6 w-6`} />
</button>
) : null}
<Dialog.Title>{title}</Dialog.Title>

View File

@ -106,8 +106,10 @@ const Notification = ({ notification }: { notification: Notification }) => {
hideNotification()
}
},
parsedTitle || type === 'confirm' || type === 'error'
parsedTitle || type === 'confirm'
? CLIENT_TX_TIMEOUT
: type === 'error'
? 30000
: 8000
)
@ -151,7 +153,9 @@ const Notification = ({ notification }: { notification: Notification }) => {
{parsedTitle || title}
</p>
{description ? (
<p className={`mb-0 mt-0.5 leading-tight text-th-fgd-3`}>
<p
className={`mb-0 mt-0.5 break-all text-sm leading-tight text-th-fgd-3`}
>
{description}
</p>
) : null}

View File

@ -4,7 +4,7 @@ import { FunctionComponent } from 'react'
interface TabButtonsProps {
activeValue: string
onChange: (x: any) => void
values: Array<any>
values: [string, number][]
showBorders?: boolean
rounded?: boolean
fillWidth?: boolean
@ -26,20 +26,29 @@ const TabButtons: FunctionComponent<TabButtonsProps> = ({
showBorders ? 'border-b border-th-bkg-3' : ''
}`}
>
{values.map((v, i) => (
<div className={fillWidth ? 'flex-1' : ''} key={v + i}>
{values.map(([label, count], i) => (
<div className={fillWidth ? 'flex-1' : ''} key={label + i}>
<button
className={`default-transition h-12 w-full px-6 font-bold ${
className={`default-transition flex h-12 w-full items-center justify-center px-6 font-bold ${
rounded ? 'rounded-md' : 'rounded-none'
} ${showBorders ? 'border-r border-th-bkg-3' : ''} ${
v === activeValue
label === activeValue
? 'bg-th-bkg-3 text-th-primary'
: 'hover:cursor-pointer hover:text-th-fgd-2'
}`}
key={`${v}${i}`}
onClick={() => onChange(v)}
key={`${label}${i}`}
onClick={() => onChange(label)}
>
{t(v)}
<span className="">{t(label)} </span>
{count ? (
<div
className={`ml-1.5 rounded ${
label === activeValue ? 'bg-th-bkg-4' : 'bg-th-bkg-3'
} px-1.5 font-mono text-xxs`}
>
{count}
</div>
) : null}
</button>
</div>
))}

View File

@ -1,8 +1,8 @@
import { Transition } from '@headlessui/react'
import { CSSProperties, ReactNode } from 'react'
const transitionEnterStyle = 'transition-all ease-in duration-300'
const transitionExitStyle = 'transition-all ease-out duration-300'
const transitionEnterStyle = 'transition-all ease-out duration-500'
const transitionExitStyle = 'transition-all ease-in duration-300'
export const EnterRightExitLeft = ({
children,

View File

@ -287,7 +287,7 @@ const SwapForm = () => {
/>
) : null}
</div>
<div className="flex justify-center">
<div className="-mb-2 flex justify-center">
<button
className="rounded-full border border-th-bkg-4 p-1.5 text-th-fgd-3 md:hover:text-th-primary"
onClick={handleSwitchTokens}

View File

@ -106,19 +106,19 @@ const SwapFormTokenList = ({
const [search, setSearch] = useState('')
const tokens = mangoStore.getState().jupiterTokens
const walletTokens = mangoStore((s) => s.wallet.tokens)
const jupiterTokens = mangoStore((s) => s.jupiterTokens)
// const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const inputBank = mangoStore((s) => s.swap.inputBank)
const outputBank = mangoStore((s) => s.swap.outputBank)
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const group = mangoStore((s) => s.group)
const popularTokens = useMemo(() => {
return tokens.filter((token) => {
return !token?.name || !token?.symbol
? false
: popularTokenSymbols.includes(token.symbol)
})
}, [tokens])
// const popularTokens = useMemo(() => {
// return tokens.filter((token) => {
// return !token?.name || !token?.symbol
// ? false
// : popularTokenSymbols.includes(token.symbol)
// })
// }, [tokens])
useEffect(() => {
function onEscape(e: any) {
@ -170,11 +170,12 @@ const SwapFormTokenList = ({
}
}, [tokens, walletTokens, inputBank, outputBank, mangoAccount, group])
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
// const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
// setSearch(e.target.value)
// }
const sortedTokens = search ? startSearch(tokenInfos, search) : tokenInfos
// const sortedTokens = search ? startSearch(tokenInfos, search) : tokenInfos
const sortedTokens = tokenInfos
return (
<>
@ -184,7 +185,7 @@ const SwapFormTokenList = ({
: `${t('swap')} ${t('swap:to')}`}
</p>
<IconButton className="absolute top-2 right-2" onClick={onClose} hideBg>
<XMarkIcon className="h-5 w-5" />
<XMarkIcon className="h-6 w-6" />
</IconButton>
{/* No need for search/popular tokens until we have more tokens */}

View File

@ -34,7 +34,7 @@ const SwapSettings = ({ onClose }: { onClose: () => void }) => {
<>
<h3 className="mb-3">{t('settings')}</h3>
<IconButton className="absolute top-2 right-2" onClick={onClose} hideBg>
<XMarkIcon className="h-5 w-5" />
<XMarkIcon className="h-6 w-6" />
</IconButton>
<div className="mt-4">

View File

@ -52,7 +52,7 @@ const MarketSelectDropdown = () => {
<div className="relative flex flex-col overflow-visible">
<Popover.Button className="default-transition flex w-full items-center justify-between hover:text-th-primary">
<MarketLogos baseURI={baseLogoURI} quoteURI={quoteLogoURI} />
<div className="text-xl font-bold md:text-base">
<div className="text-xl font-bold text-th-fgd-1 md:text-base">
{selectedMarket?.name || DEFAULT_MARKET_NAME}
</div>
<ChevronDownIcon
@ -122,7 +122,7 @@ const OraclePrice = () => {
)
return (
<div className="font-mono text-xs text-th-fgd-1">
<div className="font-mono text-xs text-th-fgd-2">
$
{baseTokenBank.uiPrice
? formatFixedDecimals(baseTokenBank.uiPrice)

View File

@ -19,6 +19,11 @@ import { notify } from 'utils/notifications'
import SpotSlider from './SpotSlider'
import { calculateMarketPrice } from 'utils/tradeForm'
const TABS: [string, number][] = [
['Limit', 0],
['Market', 0],
]
const AdvancedTradeForm = () => {
const { t } = useTranslation('common')
const set = mangoStore.getState().set
@ -201,7 +206,7 @@ const AdvancedTradeForm = () => {
<TabButtons
activeValue={tradeForm.tradeType}
onChange={(tab: 'Limit' | 'Market') => setTradeType(tab)}
values={['Limit', 'Market']}
values={TABS}
fillWidth
/>
</div>

View File

@ -1,18 +1,8 @@
import { useCallback, useEffect, useState } from 'react'
// import { useTranslation } from 'next-i18next'
import dynamic from 'next/dynamic'
import ReactGridLayout, { Responsive, WidthProvider } from 'react-grid-layout'
import mangoStore from '@store/mangoStore'
import { GRID_LAYOUT_KEY } from 'utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { breakpoints } from 'utils/theme'
import { useViewport } from 'hooks/useViewport'
import Orderbook from './Orderbook'
import AdvancedMarketHeader from './AdvancedMarketHeader'
import AdvancedTradeForm from './AdvancedTradeForm'
import BalanceAndOpenOrders from './BalanceAndOpenOrders'
import TradingViewChart from './TradingViewChart'
import TradeInfoTabs from './TradeInfoTabs'
const MobileTradeAdvancedPage = () => {
return (
@ -30,7 +20,7 @@ const MobileTradeAdvancedPage = () => {
<Orderbook />
</div>
<div className="col-span-2 border-t border-th-bkg-3 sm:col-span-3">
<BalanceAndOpenOrders />
<TradeInfoTabs />
</div>
</div>
)

View File

@ -1,140 +1,18 @@
import { Serum3Side } from '@blockworks-foundation/mango-v4'
import { IconButton } from '@components/shared/Button'
import SideBadge from '@components/shared/SideBadge'
import TabButtons from '@components/shared/TabButtons'
import Tooltip from '@components/shared/Tooltip'
import {
LinkIcon,
QuestionMarkCircleIcon,
TrashIcon,
} from '@heroicons/react/20/solid'
import { LinkIcon, TrashIcon } from '@heroicons/react/20/solid'
import { Order } from '@project-serum/serum/lib/market'
import { useWallet } from '@solana/wallet-adapter-react'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import Image from 'next/image'
import { useCallback, useMemo, useState } from 'react'
import { useCallback } from 'react'
import { notify } from 'utils/notifications'
import { formatDecimal, formatFixedDecimals } from 'utils/numbers'
import { formatFixedDecimals } from 'utils/numbers'
import MarketLogos from './MarketLogos'
const TABS = ['Balances', 'Orders']
const BalanceAndOpenOrders = () => {
const [selectedTab, setSelectedTab] = useState('Balances')
return (
<div className="hide-scroll h-full overflow-y-scroll">
<div className="sticky top-0 z-10">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}
values={TABS}
showBorders
/>
</div>
{selectedTab === 'Balances' ? <Balances /> : null}
{selectedTab === 'Orders' ? <OpenOrders /> : null}
</div>
)
}
const Balances = () => {
const { t } = useTranslation('common')
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
const group = mangoStore((s) => s.group)
const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const banks = useMemo(() => {
if (group) {
const rawBanks = Array.from(group?.banksMapByName, ([key, value]) => ({
key,
value,
}))
const sortedBanks = mangoAccount
? rawBanks.sort(
(a, b) =>
Math.abs(
mangoAccount?.getTokenBalanceUi(b.value[0]) *
b.value[0].uiPrice!
) -
Math.abs(
mangoAccount?.getTokenBalanceUi(a.value[0]) *
a.value[0].uiPrice!
)
)
: rawBanks
return mangoAccount
? sortedBanks.filter(
(b) => mangoAccount?.getTokenBalanceUi(b.value[0]) !== 0
)
: sortedBanks
}
return []
}, [group, mangoAccount])
return (
<table className="min-w-full">
<thead>
<tr>
<th className="bg-th-bkg-1 text-left">{t('token')}</th>
<th className="bg-th-bkg-1 text-right">{t('balance')}</th>
<th className="bg-th-bkg-1 text-right">{t('in-orders')}</th>
<th className="bg-th-bkg-1 text-right">{t('unsettled')}</th>
</tr>
</thead>
<tbody>
{banks.map(({ key, value }) => {
const bank = value[0]
let logoURI
if (jupiterTokens.length) {
logoURI = jupiterTokens.find(
(t) => t.address === bank.mint.toString()
)!.logoURI
}
return (
<tr key={key} className="text-sm">
<td>
<div className="flex items-center">
<div className="mr-2.5 flex flex-shrink-0 items-center">
{logoURI ? (
<Image alt="" width="20" height="20" src={logoURI} />
) : (
<QuestionMarkCircleIcon className="h-7 w-7 text-th-fgd-3" />
)}
</div>
<span>{bank.name}</span>
</div>
</td>
<td className="pt-4 text-right font-mono">
<div>
{mangoAccount
? formatDecimal(
mangoAccount.getTokenBalanceUi(bank),
bank.mintDecimals
)
: 0}
</div>
</td>
<td className="text-right font-mono">
{spotBalances[bank.mint.toString()]?.inOrders || 0.0}
</td>
<td className="text-right font-mono">
{spotBalances[bank.mint.toString()]?.unsettled || 0.0}
</td>
</tr>
)
})}
</tbody>
</table>
)
}
const OpenOrders = () => {
const { t } = useTranslation('common')
const { connected } = useWallet()
@ -271,4 +149,4 @@ const OpenOrders = () => {
)
}
export default BalanceAndOpenOrders
export default OpenOrders

View File

@ -72,6 +72,7 @@ type cumOrderbookSide = {
cumulativeSize: number
sizePercent: number
maxSizePercent: number
cumulativeSizePercent: number
}
const getCumulativeOrderbookSide = (
@ -79,7 +80,7 @@ const getCumulativeOrderbookSide = (
totalSize: number,
maxSize: number,
depth: number,
backwards = false
isBids = true
): cumOrderbookSide[] => {
let cumulative = orders
.slice(0, depth)
@ -90,13 +91,15 @@ const getCumulativeOrderbookSide = (
size,
cumulativeSize,
sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100),
cumulativeSizePercent: Math.round((size / (cumulativeSize || 1)) * 100),
maxSizePercent: Math.round((size / (maxSize || 1)) * 100),
})
return cumulative
}, [])
if (backwards) {
cumulative = cumulative.reverse()
if (!isBids) {
console.log('cumulative', cumulative)
}
return cumulative
}
@ -245,30 +248,30 @@ const Orderbook = () => {
const sum = (total: number, [, size]: number[], index: number) =>
index < depth ? total + size : total
const totalBidSize = bids.reduce(sum, 0)
const totalAskSize = asks.reduce(sum, 0)
const maxBidSize = Math.max(
...bids.map((b: number[]) => {
return b[1]
})
)
const maxAskSize = Math.max(
...asks.map((a: number[]) => {
return a[1]
})
)
const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0)
const maxSize =
Math.max(
...bids.map((b: number[]) => {
return b[1]
})
) +
Math.max(
...asks.map((a: number[]) => {
return a[1]
})
)
const bidsToDisplay = getCumulativeOrderbookSide(
bids,
totalBidSize,
maxBidSize,
depth,
false
totalSize,
maxSize,
depth
)
const asksToDisplay = getCumulativeOrderbookSide(
asks,
totalAskSize,
maxAskSize,
totalSize,
maxSize,
depth,
false
)
@ -444,6 +447,9 @@ const Orderbook = () => {
size={orderbookData?.asks[index].size}
side="sell"
sizePercent={orderbookData?.asks[index].sizePercent}
cumulativeSizePercent={
orderbookData?.asks[index].cumulativeSizePercent
}
grouping={grouping}
/>
) : null}
@ -480,6 +486,9 @@ const Orderbook = () => {
size={orderbookData?.bids[index].size}
side="buy"
sizePercent={orderbookData?.bids[index].sizePercent}
cumulativeSizePercent={
orderbookData?.bids[index].cumulativeSizePercent
}
grouping={grouping}
/>
) : null}
@ -499,6 +508,7 @@ const OrderbookRow = ({
// invert,
// hasOpenOrder,
minOrderSize,
cumulativeSizePercent,
tickSize,
grouping,
}: {
@ -506,6 +516,7 @@ const OrderbookRow = ({
price: number
size: number
sizePercent: number
cumulativeSizePercent: number
// hasOpenOrder: boolean
// invert: boolean
grouping: number
@ -573,11 +584,11 @@ const OrderbookRow = ({
onClick={handlePriceClick}
>
<>
<div className="flex w-full items-center justify-between hover:bg-th-bkg-2">
<div className="flex w-full items-center justify-between text-th-fgd-3 hover:bg-th-bkg-2">
<div className="flex w-full justify-start pl-2">
<div
style={{ fontFeatureSettings: 'zero 1' }}
className={`z-10 w-full text-right font-mono text-xs leading-5 md:leading-6 ${
className={`z-10 w-full text-right font-mono text-xs ${
/*hasOpenOrder*/ false ? 'text-th-primary' : ''
}`}
// onClick={handleSizeClick}
@ -585,18 +596,24 @@ const OrderbookRow = ({
{formattedSize.toFixed(minOrderSizeDecimals)}
</div>
</div>
<div
className={`z-10 w-full pr-4 text-right font-mono text-xs leading-5 md:leading-6`}
>
<div className={`z-10 w-full pr-4 text-right font-mono text-xs`}>
{formattedPrice.toFixed(groupingDecimalCount)}
</div>
</div>
<Line
className={`absolute left-0 opacity-90 ${
className={`absolute left-0 opacity-40 brightness-125 ${
side === 'buy' ? `bg-th-green-muted` : `bg-th-red-muted`
}`}
data-width={sizePercent + '%'}
data-width={Math.max(sizePercent, 0.5) + '%'}
/>
<Line
className={`absolute left-0 opacity-70 ${
side === 'buy' ? `bg-th-green` : `bg-th-red`
}`}
data-width={
Math.max((cumulativeSizePercent / 100) * sizePercent, 0.1) + '%'
}
/>
</>
</div>

View File

@ -3,6 +3,11 @@ import { useState } from 'react'
import Orderbook from './Orderbook'
import RecentTrades from './RecentTrades'
const TABS: [string, number][] = [
['book', 0],
['trades', 0],
]
const OrderbookAndTrades = () => {
const [activeTab, setActiveTab] = useState('book')
return (
@ -11,7 +16,7 @@ const OrderbookAndTrades = () => {
<TabButtons
activeValue={activeTab}
onChange={(tab: string) => setActiveTab(tab)}
values={['book', 'trades']}
values={TABS}
fillWidth
/>
</div>

View File

@ -30,17 +30,21 @@ const RecentTrades = () => {
const fetchTradesForChart = useCallback(async () => {
if (!selectedMarketPk) return
const response = await fetch(
`https://event-history-api-candles.herokuapp.com/trades/address/${selectedMarketPk}`
)
const parsedResp = await response.json()
const newTrades = parsedResp.data
if (!newTrades) return null
try {
const response = await fetch(
`https://event-history-api-candles.herokuapp.com/trades/address/${selectedMarketPk}`
)
const parsedResp = await response.json()
const newTrades = parsedResp.data
if (!newTrades) return null
if (newTrades.length && trades.length === 0) {
setTrades(newTrades)
} else if (newTrades?.length && !isEqual(newTrades[0], trades[0])) {
setTrades(newTrades)
if (newTrades.length && trades.length === 0) {
setTrades(newTrades)
} else if (newTrades?.length && !isEqual(newTrades[0], trades[0])) {
setTrades(newTrades)
}
} catch (e) {
console.error('Unable to fetch recent trades', e)
}
}, [selectedMarketPk, trades])
@ -92,7 +96,7 @@ const RecentTrades = () => {
<div className={`text-right text-th-fgd-2`}>
{formattedSize.toFixed()}
</div>
<div className={`text-right text-th-fgd-4`}>
<div className={`text-right tracking-tighter text-th-fgd-4`}>
{trade.time && new Date(trade.time).toLocaleTimeString()}
</div>
</div>

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
// import { useTranslation } from 'next-i18next'
import { useCallback, useMemo, useState } from 'react'
import dynamic from 'next/dynamic'
import ReactGridLayout, { Responsive, WidthProvider } from 'react-grid-layout'
@ -8,10 +7,9 @@ import { GRID_LAYOUT_KEY } from 'utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { breakpoints } from 'utils/theme'
import { useViewport } from 'hooks/useViewport'
import Orderbook from './Orderbook'
import AdvancedMarketHeader from './AdvancedMarketHeader'
import AdvancedTradeForm from './AdvancedTradeForm'
import BalanceAndOpenOrders from './BalanceAndOpenOrders'
import TradeInfoTabs from './TradeInfoTabs'
import MobileTradeAdvancedPage from './MobileTradeAdvancedPage'
import OrderbookAndTrades from './OrderbookAndTrades'
@ -60,13 +58,13 @@ const TradeAdvancedPage = () => {
return {
xxxl: [
{ i: 'market-header', x: 0, y: 0, w: 16, h: marketHeaderHeight },
{ i: 'tv-chart', x: 0, y: 1, w: 16, h: 676 },
{ i: 'tv-chart', x: 0, y: 1, w: 16, h: 640 },
{
i: 'balances',
x: 0,
y: 2,
w: 16,
h: getHeight(innerHeight, 300, 676 + marketHeaderHeight),
h: getHeight(innerHeight, 300, 640 + marketHeaderHeight),
},
{
i: 'orderbook',
@ -79,13 +77,13 @@ const TradeAdvancedPage = () => {
],
xxl: [
{ i: 'market-header', x: 0, y: 0, w: 15, h: marketHeaderHeight },
{ i: 'tv-chart', x: 0, y: 1, w: 15, h: 576 },
{ i: 'tv-chart', x: 0, y: 1, w: 15, h: 536 },
{
i: 'balances',
x: 0,
y: 2,
w: 15,
h: getHeight(innerHeight, 300, 576 + marketHeaderHeight),
h: getHeight(innerHeight, 300, 536 + marketHeaderHeight),
},
{
i: 'orderbook',
@ -98,13 +96,13 @@ const TradeAdvancedPage = () => {
],
xl: [
{ i: 'market-header', x: 0, y: 0, w: 14, h: marketHeaderHeight },
{ i: 'tv-chart', x: 0, y: 1, w: 14, h: 520 },
{ i: 'tv-chart', x: 0, y: 1, w: 14, h: 488 },
{
i: 'balances',
x: 0,
y: 2,
w: 14,
h: getHeight(innerHeight, 300, 520 + marketHeaderHeight),
h: getHeight(innerHeight, 300, 488 + marketHeaderHeight),
},
{
i: 'orderbook',
@ -117,13 +115,13 @@ const TradeAdvancedPage = () => {
],
lg: [
{ i: 'market-header', x: 0, y: 0, w: 14, h: marketHeaderHeight },
{ i: 'tv-chart', x: 0, y: 1, w: 14, h: 520 },
{ i: 'tv-chart', x: 0, y: 1, w: 14, h: 488 },
{
i: 'balances',
x: 0,
y: 2,
w: 14,
h: getHeight(innerHeight, 300, 520 + marketHeaderHeight),
h: getHeight(innerHeight, 300, 488 + marketHeaderHeight),
},
{
i: 'orderbook',
@ -136,10 +134,10 @@ const TradeAdvancedPage = () => {
],
md: [
{ i: 'market-header', x: 0, y: 0, w: 18, h: marketHeaderHeight },
{ i: 'tv-chart', x: 0, y: 1, w: 18, h: 520 },
{ i: 'tv-chart', x: 0, y: 1, w: 18, h: 488 },
{ i: 'balances', x: 0, y: 2, w: 18, h: 488 },
{ i: 'orderbook', x: 18, y: 2, w: 6, h: 489 },
{ i: 'trade-form', x: 18, y: 1, w: 6, h: 568 },
{ i: 'orderbook', x: 18, y: 2, w: 6, h: 488 },
{ i: 'trade-form', x: 18, y: 1, w: 6, h: 488 },
],
}
}, [height])
@ -156,6 +154,8 @@ const TradeAdvancedPage = () => {
}, [])
const onBreakpointChange = useCallback((newBreakpoint: string) => {
console.log('newBreakpoints', newBreakpoint)
setCurrentBreakpoint(newBreakpoint)
}, [])
@ -193,7 +193,7 @@ const TradeAdvancedPage = () => {
</div>
</div>
<div key="balances">
<BalanceAndOpenOrders />
<TradeInfoTabs />
</div>
<div
key="trade-form"

View File

@ -0,0 +1,103 @@
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import Image from 'next/image'
import { useMemo } from 'react'
import { formatDecimal } from 'utils/numbers'
const Balances = () => {
const { t } = useTranslation('common')
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances)
const group = mangoStore((s) => s.group)
const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const banks = useMemo(() => {
if (group) {
const rawBanks = Array.from(group?.banksMapByName, ([key, value]) => ({
key,
value,
}))
const sortedBanks = mangoAccount
? rawBanks.sort(
(a, b) =>
Math.abs(
mangoAccount?.getTokenBalanceUi(b.value[0]) *
b.value[0].uiPrice!
) -
Math.abs(
mangoAccount?.getTokenBalanceUi(a.value[0]) *
a.value[0].uiPrice!
)
)
: rawBanks
return mangoAccount
? sortedBanks.filter(
(b) => mangoAccount?.getTokenBalanceUi(b.value[0]) !== 0
)
: sortedBanks
}
return []
}, [group, mangoAccount])
return (
<table className="min-w-full">
<thead>
<tr>
<th className="bg-th-bkg-1 text-left">{t('token')}</th>
<th className="bg-th-bkg-1 text-right">{t('balance')}</th>
<th className="bg-th-bkg-1 text-right">{t('in-orders')}</th>
<th className="bg-th-bkg-1 text-right">{t('unsettled')}</th>
</tr>
</thead>
<tbody>
{banks.map(({ key, value }) => {
const bank = value[0]
let logoURI
if (jupiterTokens.length) {
logoURI = jupiterTokens.find(
(t) => t.address === bank.mint.toString()
)!.logoURI
}
return (
<tr key={key} className="text-sm">
<td>
<div className="flex items-center">
<div className="mr-2.5 flex flex-shrink-0 items-center">
{logoURI ? (
<Image alt="" width="20" height="20" src={logoURI} />
) : (
<QuestionMarkCircleIcon className="h-7 w-7 text-th-fgd-3" />
)}
</div>
<span>{bank.name}</span>
</div>
</td>
<td className="pt-4 text-right font-mono">
<div>
{mangoAccount
? formatDecimal(
mangoAccount.getTokenBalanceUi(bank),
bank.mintDecimals
)
: 0}
</div>
</td>
<td className="text-right font-mono">
{spotBalances[bank.mint.toString()]?.inOrders || 0.0}
</td>
<td className="text-right font-mono">
{spotBalances[bank.mint.toString()]?.unsettled || 0.0}
</td>
</tr>
)
})}
</tbody>
</table>
)
}
export default Balances

View File

@ -0,0 +1,37 @@
import { useMemo, useState } from 'react'
import TabButtons from '@components/shared/TabButtons'
import OpenOrders from './OpenOrders'
import Balances from './TradeBalances'
import UnsettledTrades from './UnsettledTrades'
import mangoStore from '@store/mangoStore'
const TradeInfoTabs = () => {
const [selectedTab, setSelectedTab] = useState('Balances')
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
const tabsWithCount: [string, number][] = useMemo(() => {
return [
['Balances', 0],
['Orders', Object.values(openOrders).flat().length],
['Unsettled', 0],
]
}, [openOrders])
return (
<div className="hide-scroll h-full overflow-y-scroll">
<div className="sticky top-0 z-10">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}
values={tabsWithCount}
showBorders
/>
</div>
{selectedTab === 'Balances' ? <Balances /> : null}
{selectedTab === 'Orders' ? <OpenOrders /> : null}
{selectedTab === 'Unsettled' ? <UnsettledTrades /> : null}
</div>
)
}
export default TradeInfoTabs

View File

@ -138,6 +138,7 @@ const TradingViewChart = () => {
'header_interval_dialog_button',
'show_interval_dialog_on_key_press',
'header_symbol_search',
'popup_hints',
],
fullscreen: defaultProps.fullscreen,
autosize: defaultProps.autosize,

View File

@ -0,0 +1,133 @@
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import { useCallback, useMemo } from 'react'
import { PublicKey } from '@solana/web3.js'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import Button from '@components/shared/Button'
import { notify } from 'utils/notifications'
const UnsettledTrades = () => {
const { t } = useTranslation('common')
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const openOrdersAccounts =
mangoStore.getState().mangoAccount.openOrderAccounts
const group = mangoStore((s) => s.group)
// const jupiterTokens = mangoStore((s) => s.jupiterTokens)
const unsettledSpotBalances = useMemo(() => {
if (!group || !mangoAccount) return {}
const unsettledBalances: Record<string, { base: number; quote: number }> =
{}
mangoAccount.serum3Active().forEach((serumMarket) => {
const market = group.getSerum3MarketByIndex(serumMarket.marketIndex)!
const openOrdersAccForMkt = openOrdersAccounts.find((oo) =>
oo.market.equals(market.serumMarketExternal)
)
const baseTokenUnsettled = toUiDecimals(
openOrdersAccForMkt!.baseTokenFree.toNumber(),
group.getFirstBankByTokenIndex(serumMarket.baseTokenIndex).mintDecimals
)
const quoteTokenUnsettled = toUiDecimals(
openOrdersAccForMkt!.quoteTokenFree
// @ts-ignore
.add(openOrdersAccForMkt['referrerRebatesAccrued'])
.toNumber(),
group.getFirstBankByTokenIndex(serumMarket.quoteTokenIndex).mintDecimals
)
unsettledBalances[market.serumMarketExternal.toString()] = {
base: baseTokenUnsettled,
quote: quoteTokenUnsettled,
}
})
const filtered = Object.entries(unsettledBalances).filter(
([_mkt, balance]) => balance.base > 0 || balance.quote > 0
)
return Object.fromEntries(filtered)!
}, [mangoAccount, group, openOrdersAccounts])
const handleSettleFunds = useCallback(async (mktAddress: string) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
if (!group || !mangoAccount) return
try {
const txid = await client.serum3SettleFunds(
group,
mangoAccount,
new PublicKey(mktAddress)
)
actions.fetchSerumOpenOrders()
actions.reloadMangoAccount()
notify({
type: 'success',
title: 'Successfully settled funds',
txid,
})
} catch (e: any) {
notify({
type: 'error',
title: 'Settle transaction failed',
description: e?.message,
txid: e?.txid,
})
console.error('Settle funds error:', e)
}
}, [])
if (!group) return null
// console.log('unsettledSpotBalances', unsettledSpotBalances)
return (
<table className="min-w-full">
<thead>
<tr>
<th className="bg-th-bkg-1 text-left">Market</th>
<th className="bg-th-bkg-1 text-right">Base</th>
<th className="bg-th-bkg-1 text-right">Quote</th>
<th className="bg-th-bkg-1 text-right"></th>
</tr>
</thead>
<tbody>
{Object.entries(unsettledSpotBalances).map(([mktAddress, balance]) => {
const market = group.getSerum3MarketByPk(new PublicKey(mktAddress))
console.log('market', mktAddress)
const base = market?.name.split('/')[0]
const quote = market?.name.split('/')[1]
return (
<tr key={mktAddress} className="text-sm">
<td>
<div className="flex items-center">
<span>{market ? market.name : ''}</span>
</div>
</td>
<td className="text-right font-mono">
{unsettledSpotBalances[mktAddress].base || 0.0} {base}
</td>
<td className="text-right font-mono">
{unsettledSpotBalances[mktAddress].quote || 0.0} {quote}
</td>
<td className="text-right">
<Button
onClick={() => handleSettleFunds(mktAddress)}
className={`text-white`}
disabled={false}
size="small"
>
<span>Settle</span>
</Button>
</td>
</tr>
)
})}
</tbody>
</table>
)
}
export default UnsettledTrades

View File

@ -62,7 +62,7 @@ const Settings: NextPage = () => {
EXPLORERS[0]
)
const themes = useMemo(() => {
return [t('settings:light'), t('settings:dark'), t('settings:mango')]
return [t('settings:light'), t('settings:mango'), t('settings:dark')]
}, [t])
const handleLangChange = useCallback(

View File

@ -1,6 +1,6 @@
export const COLORS: any = {
BKG1: { Mango: '#18181C', Dark: '#17171B', Light: '#FDFDFD' },
GREEN: { Mango: '#AFD803', Dark: '#5EBF4d', Light: '#5EBF4d' },
GREEN: { Mango: '#A6CD03', Dark: '#5EBF4d', Light: '#5EBF4d' },
PRIMARY: { Mango: '#F2C94C', Dark: '#F2C94C', Light: '#FF9C24' },
RED: { Mango: '#F84638', Dark: '#CC2929', Light: '#CC2929' },
}

View File

@ -266,13 +266,12 @@ table th {
}
table p {
@apply font-mono text-sm tracking-tight text-th-fgd-1;
@apply font-mono text-sm tracking-tight text-th-fgd-2;
}
/* Scrollbars */
.font-mono {
@apply tracking-tight;
-webkit-font-feature-settings: 'zero' 1;
font-feature-settings: 'zero' 1;
}

View File

@ -64,12 +64,12 @@ module.exports = {
dark: '#E4AF11',
},
red: { DEFAULT: '#F84638', dark: '#C7251A', muted: '#6d2832' },
green: { DEFAULT: '#AFD803', dark: '#91B503', muted: '#49601b' },
green: { DEFAULT: '#A6CD03', dark: '#84A21C', muted: '#49601b' },
orange: { DEFAULT: '#FF9C24' },
'bkg-1': '#141026',
'bkg-2': '#1D1832',
'bkg-3': '#2A2440',
'bkg-4': '#37324D',
'bkg-1': '#1C1924', // '#141026',
'bkg-2': '#252232', // '#1D1832',
'bkg-3': '#302B40', // '#2A2440',
'bkg-4': '#383544', // '#37324D',
'fgd-1': '#E5E3EC',
'fgd-2': '#D2CEDE',
'fgd-3': '#C1BED3',

View File

@ -1,6 +1,6 @@
export const LAST_ACCOUNT_KEY = 'mangoAccount-0.1'
export const CLIENT_TX_TIMEOUT = 60000
export const CLIENT_TX_TIMEOUT = 90000
export const INPUT_TOKEN_DEFAULT = 'USDC'