Merge pull request #385 from blockworks-foundation/serum-comp

add modal and page for spot comp
This commit is contained in:
tjshipe 2022-08-09 20:08:37 -04:00 committed by GitHub
commit ebde7e7e44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 635 additions and 1 deletions

View File

@ -51,7 +51,7 @@ const Modal: any = React.forwardRef<any, any>((props, ref) => {
<div className="">
<button
onClick={onClose}
className={`absolute right-4 top-4 text-th-fgd-1 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
className={`absolute right-4 top-4 text-th-fgd-4 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
>
<XIcon className={`h-5 w-5`} />
</button>

View File

@ -0,0 +1,70 @@
import React from 'react'
import { CheckCircleIcon, XIcon } from '@heroicons/react/solid'
import Modal from './Modal'
import Button from './Button'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { useRouter } from 'next/router'
export const SEEN_SERUM_COMP_KEY = 'seenSerumCompInfo'
const SerumCompModal = ({
isOpen,
onClose,
}: {
isOpen: boolean
onClose?: (x) => void
}) => {
const [, setSeenSerumCompInfo] = useLocalStorageState(SEEN_SERUM_COMP_KEY)
const router = useRouter()
const handleFindOutMore = () => {
setSeenSerumCompInfo(true)
router.push('/win-srm')
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>
<div className="flex flex-col items-center">
<div className="flex items-center justify-center space-x-4">
<img
className={`h-10 w-auto`}
src="/assets/icons/srm.svg"
alt="next"
/>
<XIcon className="h-5 w-5 text-th-primary" />
<img
className={`h-12 w-auto`}
src="/assets/icons/logo.svg"
alt="next"
/>
</div>
</div>
</Modal.Header>
<h1 className="relative m-auto mb-2 w-max">Win a Share in 400k SRM</h1>
<p className="text-center">
50k SRM are up for grabs every week until 12 Sep
</p>
<div className="mt-4 space-y-2 border-t border-th-bkg-4 pt-4">
<div className="flex items-center text-th-fgd-1">
<CheckCircleIcon className="mr-1.5 h-6 w-6 flex-shrink-0 text-th-green" />
<p className="mb-0 text-th-fgd-1">
40k SRM distributed proportionally to everyone who contributes at
least 1% of total spot volume for both maker and taker
</p>
</div>
<div className="flex items-center text-th-fgd-1">
<CheckCircleIcon className="mr-1.5 h-6 w-6 flex-shrink-0 text-th-green" />
<p className="mb-0 text-th-fgd-1">
10k SRM for the top 10 traders by spot PnL
</p>
</div>
</div>
<Button className="mt-6 w-full" onClick={() => handleFindOutMore()}>
Find Out More
</Button>
</Modal>
)
}
export default SerumCompModal

View File

@ -15,6 +15,7 @@ import {
ExternalLinkIcon,
ChevronDownIcon,
ReceiptTaxIcon,
GiftIcon,
} from '@heroicons/react/solid'
import { useRouter } from 'next/router'
import AccountOverviewPopover from './AccountOverviewPopover'
@ -160,6 +161,14 @@ const SideNav = ({ collapsed }) => {
pagePath="/fees"
hideIconBg
/>
<MenuItem
active={pathname === '/win-srm'}
collapsed={false}
icon={<GiftIcon className="h-4 w-4" />}
title="Spot Trading Comp"
pagePath="/win-srm"
hideIconBg
/>
<MenuItem
collapsed={false}
icon={<LightBulbIcon className="h-4 w-4" />}

View File

@ -22,6 +22,7 @@ import { useWallet } from '@solana/wallet-adapter-react'
import AccountsModal from 'components/AccountsModal'
import dayjs from 'dayjs'
import { tokenPrecision } from 'utils'
import SerumCompModal, { SEEN_SERUM_COMP_KEY } from 'components/SerumCompModal'
const DISMISS_CREATE_ACCOUNT_KEY = 'show-create-account'
@ -43,6 +44,10 @@ export async function getStaticProps({ locale }) {
const PerpMarket: React.FC = () => {
const [alphaAccepted] = useLocalStorageState(ALPHA_MODAL_KEY, false)
const [seenSerumCompInfo, setSeenSerumCompInfo] = useLocalStorageState(
SEEN_SERUM_COMP_KEY,
false
)
const [showTour] = useLocalStorageState(SHOW_TOUR_KEY, false)
const [dismissCreateAccount, setDismissCreateAccount] = useLocalStorageState(
DISMISS_CREATE_ACCOUNT_KEY,
@ -175,6 +180,12 @@ const PerpMarket: React.FC = () => {
{!alphaAccepted && (
<AlphaModal isOpen={!alphaAccepted} onClose={() => {}} />
)}
{!seenSerumCompInfo && alphaAccepted ? (
<SerumCompModal
isOpen={!seenSerumCompInfo && alphaAccepted}
onClose={() => setSeenSerumCompInfo(true)}
/>
) : null}
{showCreateAccount ? (
<AccountsModal
isOpen={showCreateAccount}

544
pages/win-srm.tsx Normal file
View File

@ -0,0 +1,544 @@
import { useEffect, useMemo, useState, useCallback } from 'react'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import useMangoStore from '../stores/useMangoStore'
import { mangoGroupSelector } from '../stores/selectors'
import { formatUsdValue } from '../utils'
import { notify } from '../utils/notifications'
import { useTranslation } from 'next-i18next'
import EmptyState from '../components/EmptyState'
import {
CurrencyDollarIcon,
InformationCircleIcon,
LinkIcon,
XIcon,
} from '@heroicons/react/solid'
import { Row, Table, Td, Th, TrBody, TrHead } from '../components/TableElements'
import AccountsModal from '../components/AccountsModal'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from '../components/TradePageGrid'
import useMangoAccount from '../hooks/useMangoAccount'
import { useWallet } from '@solana/wallet-adapter-react'
import { handleWalletConnect } from 'components/ConnectWalletButton'
import Tooltip from 'components/Tooltip'
import Tabs from 'components/Tabs'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, [
'common',
'delegate',
'referrals',
'profile',
])),
// Will be passed to the page component as props
},
}
}
const TABS = ['maker', 'taker', 'pnl']
export default function SerumComp() {
const { t } = useTranslation(['common', 'referrals'])
const mangoGroup = useMangoStore(mangoGroupSelector)
const { mangoAccount } = useMangoAccount()
const { wallet, connected } = useWallet()
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [activeTab, setActiveTab] = useState('maker')
const [makerData, setMakerData] = useState<any>([])
const [takerData, setTakerData] = useState<any>([])
const [pnlData, setPnlData] = useState<any>([])
const [accountPnlData, setAccountPnlData] = useState<any>([])
const [accountPnl, setAccountPnl] = useState(0)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const startDay = dayjs()
.utc()
.hour(0)
.minute(0)
.subtract((new Date().getUTCDay() + 6) % 7, 'day')
const endDay = startDay.add(startDay.get('day') + 6, 'day')
const fetchVolumeData = async () => {
try {
const response = await fetch(
'https://mango-transaction-log.herokuapp.com/v3/stats/serum-volume-leaderboard'
)
const parsedResponse = await response.json()
setMakerData(parsedResponse.volumes[0].mango_accounts)
setTakerData(parsedResponse.volumes[1].mango_accounts)
} catch {
notify({ type: 'error', title: 'Failed to fetch competition data' })
}
}
const fetchSpotPnlData = async () => {
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/serum-pnl-leaderboard`
)
const parsedResponse = await response.json()
setPnlData(parsedResponse.participants)
} catch {
notify({ type: 'error', title: 'Failed to fetch competition data' })
}
}
const fetchAccountPnlData = async (mangoAccountPk: string) => {
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v3/stats/account-performance-detailed?mango-account=${mangoAccountPk}&start-date=${startDay.format(
'YYYY-MM-DD'
)}`
)
const parsedResponse = await response.json()
const entries: any = Object.entries(parsedResponse).sort((a, b) =>
b[0].localeCompare(a[0])
)
setAccountPnlData(entries)
} catch {
notify({ type: 'error', title: 'Failed to fetch account PnL' })
}
}
useEffect(() => {
if (accountPnlData.length) {
const currentPnl =
accountPnlData[0][1].pnl - accountPnlData[0][1].perp_pnl
const startPnl =
accountPnlData[accountPnlData.length - 1][1].pnl -
accountPnlData[accountPnlData.length - 1][1].perp_pnl
setAccountPnl(currentPnl - startPnl)
}
}, [accountPnlData])
useEffect(() => {
if (mangoAccount) {
fetchAccountPnlData(mangoAccount.publicKey.toString())
}
}, [mangoAccount])
useEffect(() => {
fetchVolumeData()
fetchSpotPnlData()
}, [])
const handleTabChange = (tabName) => {
setActiveTab(tabName)
}
const handleConnect = useCallback(() => {
if (wallet) {
handleWalletConnect(wallet)
}
}, [wallet])
const filterForQualified = (accounts) => accounts.filter((a) => a.qualifies)
const volumeTableData = useMemo(() => {
if (makerData.length && takerData.length) {
return activeTab === 'maker'
? filterForQualified(makerData)
: filterForQualified(takerData)
}
return []
}, [makerData, takerData, activeTab])
const accountMakerVolume = useMemo(() => {
if (mangoAccount && makerData.length) {
const found = makerData.find(
(acc) => acc.mango_account === mangoAccount.publicKey.toString()
)
return found
? found
: {
mango_account_volume: 0,
ratio_to_total_volume: 0,
qualifies: false,
}
}
return null
}, [mangoAccount, makerData])
const accountTakerVolume = useMemo(() => {
if (mangoAccount && takerData.length) {
const found = takerData.find(
(acc) => acc.mango_account === mangoAccount.publicKey.toString()
)
return found
? found
: {
mango_account_volume: 0,
ratio_to_total_volume: 0,
qualifies: false,
}
}
return null
}, [mangoAccount, takerData])
const accountPnlQualifies = useMemo(() => {
if (mangoAccount && pnlData.length) {
const found = pnlData
.slice(0, 10)
.find((acc) => acc.mango_account === mangoAccount.publicKey.toString())
return found
}
return null
}, [mangoAccount, pnlData])
return (
<div className="grid grid-cols-12">
<div className="col-span-12 py-6 lg:col-span-10 lg:col-start-2">
<div className="mb-2 flex items-center justify-center space-x-4">
<img
className={`h-10 w-auto`}
src="/assets/icons/srm.svg"
alt="next"
/>
<XIcon className="h-5 w-5 text-th-primary" />
<img
className={`h-12 w-auto`}
src="/assets/icons/logo.svg"
alt="next"
/>
</div>
<div className="mb-4 flex flex-col items-center border-b border-th-bkg-3 pb-4">
<h1 className="relative mb-2 w-max text-center">
Win a Share in 400k SRM
</h1>
<p className="mb-4 text-lg text-th-fgd-2">
50k SRM are up for grabs every week until 12 Sep
</p>
</div>
<h2 className="mb-2">How it Works</h2>
<ul className="list-disc pl-3">
<li className="mb-1 text-base">
Trade any spot market on Mango each week from Mon 00:00 UTC to the
following Mon 00:00 UTC
</li>
<li className="mb-1 text-base">
At the end of the week the traders who contribute at least 1% of
total volume for both maker (limit orders) and taker (market orders)
will win a proportianate share of 40k SRM
</li>
<li className="text-base">
Also, the top 10 traders by PnL will win a share of 10k SRM
</li>
</ul>
</div>
<div className="col-span-12 lg:col-span-10 lg:col-start-2">
{connected ? (
mangoAccount ? (
<div className="grid grid-cols-3 md:gap-4">
<div className="col-span-3">
<h2 className="mb-4 md:mb-0">
Your Account{' '}
<span className="text-sm font-normal text-th-fgd-3">
({`${startDay.format('D MMM')} ${endDay.format('D MMM')}`}
)
</span>
</h2>
</div>
<div className="col-span-3 border-t border-th-bkg-3 p-4 md:col-span-1 md:border-b">
<p className="mb-1">Maker Volume</p>
<span className="text-2xl font-bold lg:text-4xl">
{formatUsdValue(accountMakerVolume?.mango_account_volume)}
</span>
<div
className={`mt-3 w-max rounded-full border ${
accountMakerVolume?.qualifies
? 'border-th-green'
: 'border-th-red'
} py-1 px-3 text-th-fgd-1`}
>
<div className="flex">
<span className="font-bold">
{(
accountMakerVolume?.ratio_to_total_volume * 100
).toFixed(1)}
%
</span>
<span className="mx-1 text-th-fgd-4">|</span>
<span className="flex items-center text-th-fgd-3">
{accountMakerVolume?.qualifies
? 'Qualified'
: 'Unqualified'}
<Tooltip
content="Percentage of total maker volume needs to be 1% or greater to qualify"
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
</Tooltip>
</span>
</div>
</div>
</div>
<div className="col-span-3 border-t border-th-bkg-3 p-4 md:col-span-1 md:border-b">
<p className="mb-1">Taker Volume</p>
<span className="text-2xl font-bold lg:text-4xl">
{formatUsdValue(accountTakerVolume?.mango_account_volume)}
</span>
<div
className={`mt-3 w-max rounded-full border ${
accountTakerVolume?.qualifies
? 'border-th-green'
: 'border-th-red'
} py-1 px-3 text-th-fgd-1`}
>
<div className="flex">
<span className="font-bold">
{(
accountTakerVolume?.ratio_to_total_volume * 100
).toFixed(1)}
%
</span>
<span className="mx-1 text-th-fgd-4">|</span>
<span className="flex items-center text-th-fgd-3">
{accountTakerVolume?.qualifies
? 'Qualified'
: 'Unqualified'}
<Tooltip
content="Percentage of total taker volume needs to be 1% or greater to qualify"
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
</Tooltip>
</span>
</div>
</div>
</div>
<div className="col-span-3 border-y border-th-bkg-3 p-4 md:col-span-1">
<p className="mb-1">PnL</p>
<span className="text-2xl font-bold lg:text-4xl">
{formatUsdValue(accountPnl)}
</span>
<div
className={`mt-3 w-max rounded-full border ${
accountPnlQualifies ? 'border-th-green' : 'border-th-red'
} py-1 px-3 text-th-fgd-1`}
>
<div className="flex">
<span className="flex items-center text-th-fgd-3">
{accountPnlQualifies ? 'Qualified' : 'Unqualified'}
<Tooltip
content="You need to be in the top 10 for spot PnL to qualify"
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-4 hover:cursor-help" />
</Tooltip>
</span>
</div>
</div>
</div>
</div>
) : (
<div className="col-span-12 flex items-center justify-center rounded-md border border-th-bkg-3 p-6">
<EmptyState
buttonText={t('create-account')}
icon={<CurrencyDollarIcon />}
onClickButton={() => setShowAccountsModal(true)}
title={t('no-account-found')}
disabled={!wallet || !mangoGroup}
/>
</div>
)
) : (
<div className="col-span-12 flex items-center justify-center rounded-md border border-th-bkg-3 p-6">
<EmptyState
buttonText={t('connect')}
disabled={!wallet || !mangoGroup}
icon={<LinkIcon />}
onClickButton={handleConnect}
title={t('connect-wallet')}
desc="Connect your wallet to see your competition status"
/>
</div>
)}
</div>
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2">
<h2 className="mb-4">Current Results</h2>
<Tabs activeTab={activeTab} onChange={handleTabChange} tabs={TABS} />
{activeTab === 'maker' || activeTab === 'taker' ? (
volumeTableData.length ? (
!isMobile ? (
<Table>
<thead>
<TrHead>
<Th>Rank</Th>
<Th>Account</Th>
<Th>Volume</Th>
<Th>% of Total Volume</Th>
<Th>Current SRM Prize</Th>
</TrHead>
</thead>
<tbody>
{volumeTableData.map((a, i) => (
<TrBody key={a.mango_account}>
<Td>#{i + 1}</Td>
<Td>
<a
className="default-transition block"
href={`/account?pubkey=${a.mango_account}`}
target="_blank"
rel="noopener noreferrer"
>{`${a.mango_account.slice(
0,
5
)}...${a.mango_account.slice(-5)}`}</a>
</Td>
<Td>{formatUsdValue(a.mango_account_volume)}</Td>
<Td>{(a.ratio_to_total_volume * 100).toFixed(1)}%</Td>
<Td className="flex items-center">
<img
className={`mr-1.5 h-4 w-auto`}
src="/assets/icons/srm.svg"
alt="next"
/>
{a.srm_payout.toLocaleString(undefined, {
maximumFractionDigits: 1,
})}
</Td>
</TrBody>
))}
</tbody>
</Table>
) : (
volumeTableData.map((a, i) => (
<Row key={a.mango_account}>
<div className="flex w-full justify-between text-left">
<div className="flex items-center space-x-3">
<span className="font-bold">#{i + 1}</span>
<div>
<a
className="default-transition block"
href={`/account?pubkey=${a.mango_account}`}
target="_blank"
rel="noopener noreferrer"
>{`${a.mango_account.slice(
0,
5
)}...${a.mango_account.slice(-5)}`}</a>
<p className="mb-0">{`${formatUsdValue(
a.mango_account_volume
)} | ${(a.ratio_to_total_volume * 100).toFixed(
1
)}%`}</p>
</div>
</div>
<p className="mb-0 flex items-center text-th-fgd-1">
<img
className={`mr-1.5 h-4 w-auto`}
src="/assets/icons/srm.svg"
alt="next"
/>
{a.srm_payout.toLocaleString(undefined, {
maximumFractionDigits: 1,
})}
</p>
</div>
</Row>
))
)
) : null
) : null}
{activeTab === 'pnl' ? (
pnlData.length ? (
!isMobile ? (
<Table>
<thead>
<TrHead>
<Th>Rank</Th>
<Th>Account</Th>
<Th>PnL</Th>
<Th>Current SRM Prize</Th>
</TrHead>
</thead>
<tbody>
{pnlData.slice(0, 10).map((a, i) => (
<TrBody key={a.mango_account}>
<Td>#{i + 1}</Td>
<Td>
<a
className="default-transition block"
href={`/account?pubkey=${a.mango_account}`}
target="_blank"
rel="noopener noreferrer"
>{`${a.mango_account.slice(
0,
5
)}...${a.mango_account.slice(-5)}`}</a>
</Td>
<Td>{formatUsdValue(a.spot_pnl)}</Td>
<Td className="flex items-center">
<img
className={`mr-1.5 h-4 w-auto`}
src="/assets/icons/srm.svg"
alt="next"
/>
{i === 0
? '3,500.0'
: i === 1
? '2,000.0'
: i === 2
? '1,500.0'
: '500.0'}
</Td>
</TrBody>
))}
</tbody>
</Table>
) : (
pnlData.slice(0, 10).map((a, i) => (
<Row key={a.mango_account}>
<div className="flex w-full justify-between text-left">
<div className="flex items-center space-x-3">
<span className="font-bold">#{i + 1}</span>
<div>
<a
className="default-transition block"
href={`/account?pubkey=${a.mango_account}`}
target="_blank"
rel="noopener noreferrer"
>{`${a.mango_account.slice(
0,
5
)}...${a.mango_account.slice(-5)}`}</a>
<p className="mb-0">{formatUsdValue(a.spot_pnl)}</p>
</div>
</div>
<p className="mb-0 flex items-center text-th-fgd-1">
<img
className={`mr-1.5 h-4 w-auto`}
src="/assets/icons/srm.svg"
alt="next"
/>
{i === 0
? '3,500.0'
: i === 1
? '2,000.0'
: i === 2
? '1,500.0'
: '500.0'}
</p>
</div>
</Row>
))
)
) : null
) : null}
</div>
{showAccountsModal ? (
<AccountsModal
onClose={() => setShowAccountsModal(false)}
isOpen={showAccountsModal}
/>
) : null}
</div>
)
}