diff --git a/components/Modal.tsx b/components/Modal.tsx index a0c7feeb..7cd21927 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -51,7 +51,7 @@ const Modal: any = React.forwardRef((props, ref) => {
diff --git a/components/SerumCompModal.tsx b/components/SerumCompModal.tsx new file mode 100644 index 00000000..03b19093 --- /dev/null +++ b/components/SerumCompModal.tsx @@ -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 ( + + +
+
+ next + + next +
+
+
+

Win a Share in 400k SRM

+

+ 50k SRM are up for grabs every week until 12 Sep +

+
+
+ +

+ 40k SRM distributed proportionally to everyone who contributes at + least 1% of total spot volume for both maker and taker +

+
+
+ +

+ 10k SRM for the top 10 traders by spot PnL +

+
+
+ +
+ ) +} + +export default SerumCompModal diff --git a/components/SideNav.tsx b/components/SideNav.tsx index e75776ba..d182b733 100644 --- a/components/SideNav.tsx +++ b/components/SideNav.tsx @@ -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 /> + } + title="Spot Trading Comp" + pagePath="/win-srm" + hideIconBg + /> } diff --git a/pages/index.tsx b/pages/index.tsx index 5dc06ca7..723f552e 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -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 && ( {}} /> )} + {!seenSerumCompInfo && alphaAccepted ? ( + setSeenSerumCompInfo(true)} + /> + ) : null} {showCreateAccount ? ( ([]) + const [takerData, setTakerData] = useState([]) + const [pnlData, setPnlData] = useState([]) + const [accountPnlData, setAccountPnlData] = useState([]) + 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 ( +
+
+
+ next + + next +
+
+

+ Win a Share in 400k SRM +

+

+ 50k SRM are up for grabs every week until 12 Sep +

+
+

How it Works

+
    +
  • + Trade any spot market on Mango each week from Mon 00:00 UTC to the + following Mon 00:00 UTC +
  • +
  • + 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 +
  • +
  • + Also, the top 10 traders by PnL will win a share of 10k SRM +
  • +
+
+
+ {connected ? ( + mangoAccount ? ( +
+
+

+ Your Account{' '} + + ({`${startDay.format('D MMM')} – ${endDay.format('D MMM')}`} + ) + +

+
+
+

Maker Volume

+ + {formatUsdValue(accountMakerVolume?.mango_account_volume)} + +
+
+ + {( + accountMakerVolume?.ratio_to_total_volume * 100 + ).toFixed(1)} + % + + | + + {accountMakerVolume?.qualifies + ? 'Qualified' + : 'Unqualified'} + + + + +
+
+
+
+

Taker Volume

+ + {formatUsdValue(accountTakerVolume?.mango_account_volume)} + +
+
+ + {( + accountTakerVolume?.ratio_to_total_volume * 100 + ).toFixed(1)} + % + + | + + {accountTakerVolume?.qualifies + ? 'Qualified' + : 'Unqualified'} + + + + +
+
+
+
+

PnL

+ + {formatUsdValue(accountPnl)} + +
+
+ + {accountPnlQualifies ? 'Qualified' : 'Unqualified'} + + + + +
+
+
+
+ ) : ( +
+ } + onClickButton={() => setShowAccountsModal(true)} + title={t('no-account-found')} + disabled={!wallet || !mangoGroup} + /> +
+ ) + ) : ( +
+ } + onClickButton={handleConnect} + title={t('connect-wallet')} + desc="Connect your wallet to see your competition status" + /> +
+ )} +
+
+

Current Results

+ + {activeTab === 'maker' || activeTab === 'taker' ? ( + volumeTableData.length ? ( + !isMobile ? ( + + + + + + + + + + + + {volumeTableData.map((a, i) => ( + + + + + + + + ))} + +
RankAccountVolume% of Total VolumeCurrent SRM Prize
#{i + 1} + {`${a.mango_account.slice( + 0, + 5 + )}...${a.mango_account.slice(-5)}`} + {formatUsdValue(a.mango_account_volume)}{(a.ratio_to_total_volume * 100).toFixed(1)}% + next + {a.srm_payout.toLocaleString(undefined, { + maximumFractionDigits: 1, + })} +
+ ) : ( + volumeTableData.map((a, i) => ( + +
+
+ #{i + 1} +
+ {`${a.mango_account.slice( + 0, + 5 + )}...${a.mango_account.slice(-5)}`} +

{`${formatUsdValue( + a.mango_account_volume + )} | ${(a.ratio_to_total_volume * 100).toFixed( + 1 + )}%`}

+
+
+

+ next + {a.srm_payout.toLocaleString(undefined, { + maximumFractionDigits: 1, + })} +

+
+
+ )) + ) + ) : null + ) : null} + {activeTab === 'pnl' ? ( + pnlData.length ? ( + !isMobile ? ( + + + + + + + + + + + {pnlData.slice(0, 10).map((a, i) => ( + + + + + + + ))} + +
RankAccountPnLCurrent SRM Prize
#{i + 1} + {`${a.mango_account.slice( + 0, + 5 + )}...${a.mango_account.slice(-5)}`} + {formatUsdValue(a.spot_pnl)} + next + {i === 0 + ? '3,500.0' + : i === 1 + ? '2,000.0' + : i === 2 + ? '1,500.0' + : '500.0'} +
+ ) : ( + pnlData.slice(0, 10).map((a, i) => ( + +
+
+ #{i + 1} + +
+

+ next + {i === 0 + ? '3,500.0' + : i === 1 + ? '2,000.0' + : i === 2 + ? '1,500.0' + : '500.0'} +

+
+
+ )) + ) + ) : null + ) : null} +
+ {showAccountsModal ? ( + setShowAccountsModal(false)} + isOpen={showAccountsModal} + /> + ) : null} +
+ ) +}