diff --git a/@types/index.d.ts b/@types/index.d.ts index 9b9471d..b630a55 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -1,4 +1,12 @@ +import 'dayjs' + declare module '*.svg' { const content: any export default content } + +declare module 'dayjs' { + interface Dayjs { + utc() + } +} diff --git a/components/LeaderboardTable.tsx b/components/LeaderboardTable.tsx new file mode 100644 index 0000000..fea3447 --- /dev/null +++ b/components/LeaderboardTable.tsx @@ -0,0 +1,242 @@ +import { useEffect, useState } from 'react' +import styled from '@emotion/styled' +import dayjs from 'dayjs' +import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table' +import { AreaChart, Area, ReferenceLine, XAxis, YAxis, Tooltip } from 'recharts' +import { ExternalLinkIcon } from '@heroicons/react/outline' +import { usdFormatter } from '../utils' +import { AwardIcon, TrophyIcon } from './icons' +import useMangoStore from '../stores/useMangoStore' + +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + +const StyledTooltipWrapper = styled.div` + min-width: 180px; +` + +const LeaderboardTable = () => { + const [pnlHistory, setPnlHistory] = useState([]) + const [loading, setLoading] = useState(false) + const pnlLeaderboard = useMangoStore((s) => s.pnlLeaderboard) + + /* API Returns: + * [ { cumulative_pnl: -3.687498 + date: "2021-06-10" + margin_account: "J8XtwLVyZjeH1PG1Nnk9cWbLn3zEemS1rCbn4x6AjtXM" + name: "" + owner: "APLKzSqJQw79q4U4ipBWnLdqkVzijSPNpDCNKwL8mW3B" + }, ... ] + */ + useEffect(() => { + const getPnlHistory = async () => { + setLoading(true) + const start = dayjs().utc().subtract(31, 'day').format('YYYY-MM-DD') + console.log(start) + const results = await Promise.all( + pnlLeaderboard.slice(pnlHistory.length).map(async (acc) => { + const response = await fetch( + `https://mango-transaction-log.herokuapp.com/stats/pnl_history/${acc.margin_account}?start_date=${start}` + ) + const parsedResponse = await response.json() + return parsedResponse ? parsedResponse.reverse() : [] + }) + ) + setPnlHistory(pnlHistory.concat(results)) + setLoading(false) + } + getPnlHistory() + }, [pnlLeaderboard]) + + const formatPnlHistoryData = (data) => { + const start = new Date( + dayjs().utc().hour(0).minute(0).subtract(31, 'day') + ).getTime() + + return data.filter((d) => new Date(d.date).getTime() > start) + } + + const tooltipContent = (tooltipProps) => { + if (tooltipProps.payload.length > 0) { + return ( + +
+
Date
+
+ {tooltipProps.payload[0].payload.date} +
+
+
+
PNL
+
+ {usdFormatter.format( + tooltipProps.payload[0].payload.cumulative_pnl + )} +
+
+
+ ) + } + return null + } + + return ( +
+
+
+ {pnlLeaderboard.length > 0 ? ( +
+ + + + + + + + + + + + {pnlLeaderboard.map((acc, index) => ( + + + + + + + + ))} + +
+ Rank + + Account + + PNL + + PNL / Time + +
+ View on Step + +
+
+
+ {acc.rank} + {acc.rank === 1 ? ( + + ) : null} + {acc.rank === 2 || acc.rank === 3 ? ( + + ) : null} +
+
+ {acc.name + ? acc.name + : `${acc.margin_account.slice( + 0, + 5 + )}...${acc.margin_account.slice(-5)}`} + +
+ {usdFormatter.format(acc.pnl)} +
+
+ {loading && !pnlHistory[index] ? ( +
+ ) : ( + + + + + + + + )} +
+ + View + + +
+
+ ) : ( +
+
+
+
+
+
+
+ )} +
+
+
+ ) +} + +export default LeaderboardTable diff --git a/components/TopBar.tsx b/components/TopBar.tsx index 83d9651..5c84026 100644 --- a/components/TopBar.tsx +++ b/components/TopBar.tsx @@ -31,6 +31,7 @@ const TopBar = () => { Account Borrow Alerts + Leaderboard Stats Learn
diff --git a/components/account-page/AccountAssets.tsx b/components/account-page/AccountAssets.tsx index 8105937..936085b 100644 --- a/components/account-page/AccountAssets.tsx +++ b/components/account-page/AccountAssets.tsx @@ -147,7 +147,7 @@ export default function AccountAssets() { scope="col" className={`px-6 py-3 text-left font-normal`} > - Available + Deposits { ) } + +export const AwardIcon = ({ className }) => { + return ( + + + + + ) +} + +export const TrophyIcon = ({ className }) => { + return ( + + + + ) +} diff --git a/pages/leaderboard.tsx b/pages/leaderboard.tsx new file mode 100644 index 0000000..f43676c --- /dev/null +++ b/pages/leaderboard.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react' +import useMangoStore from '../stores/useMangoStore' +import PageBodyContainer from '../components/PageBodyContainer' +import TopBar from '../components/TopBar' +import LeaderboardTable from '../components/LeaderboardTable' +import { LinkButton } from '../components/Button' + +export default function Leaderboard() { + const [offsetResults, setOffsetResults] = useState(0) + const actions = useMangoStore((s) => s.actions) + const pnlLeaderboard = useMangoStore((s) => s.pnlLeaderboard) + + useEffect(() => { + actions.fetchPnlLeaderboard(offsetResults, 29) + }, []) + + const handleShowMore = async () => { + const offset = offsetResults + 25 + await actions.fetchPnlLeaderboard(offset, 29) + setOffsetResults(offset) + } + + return ( +
+ + +
+

+ Leaderboard +

+
+
+

Top 100 accounts by PNL over the last 30 days

+ + {pnlLeaderboard.length < 100 ? ( + handleShowMore()} + > + Show More + + ) : null} +
+
+
+ ) +} diff --git a/public/assets/icons/step.png b/public/assets/icons/step.png new file mode 100644 index 0000000..a7364f5 Binary files /dev/null and b/public/assets/icons/step.png differ diff --git a/stores/useMangoStore.tsx b/stores/useMangoStore.tsx index fe9fe49..6adac4b 100644 --- a/stores/useMangoStore.tsx +++ b/stores/useMangoStore.tsx @@ -10,11 +10,15 @@ import { } from '@blockworks-foundation/mango-client' import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions' import { AccountInfo, Connection, PublicKey } from '@solana/web3.js' +import dayjs from 'dayjs' import { EndpointInfo, WalletAdapter } from '../@types/types' import { getWalletTokenInfo } from '../utils/tokens' import { isDefined } from '../utils/index' import { notify } from '../utils/notifications' +const utc = require('dayjs/plugin/utc') +dayjs.extend(utc) + export const ENDPOINTS: EndpointInfo[] = [ { name: 'mainnet-beta', @@ -57,6 +61,14 @@ interface AccountInfoList { [key: string]: AccountInfo } +interface AccountPnl { + margin_account: string + name: string + owner: string + pnl: number + rank: number +} + interface MangoStore extends State { notifications: Array<{ type: string @@ -119,9 +131,12 @@ interface MangoStore extends State { liquidationHistory: any[] withdrawalHistory: any[] tradeHistory: any[] + pnlHistory: any[] + pnlLeaderboard: any[] + accountPnl: AccountPnl set: (x: any) => void actions: { - [key: string]: () => void + [key: string]: (...args: any[]) => void } } @@ -177,6 +192,9 @@ const useMangoStore = create((set, get) => ({ liquidationHistory: [], withdrawalHistory: [], tradeHistory: [], + pnlHistory: [], + pnlLeaderboard: [], + accountPnl: null, set: (fn) => set(produce(fn)), actions: { async fetchWalletBalances() { @@ -428,6 +446,82 @@ const useMangoStore = create((set, get) => ({ state.withdrawalHistory = results }) }, + async fetchPnlHistory(marginAccount = null) { + const selectedMarginAccount = + marginAccount || + get().selectedMarginAccount.current.publicKey.toString() + const set = get().set + + if (!selectedMarginAccount) return + + const response = await fetch( + `https://mango-transaction-log.herokuapp.com/stats/pnl_history/${selectedMarginAccount}` + ) + const parsedResponse = await response.json() + const results = parsedResponse.length ? parsedResponse.reverse() : [] + + set((state) => { + state.pnlHistory = results + }) + }, + async fetchPnlLeaderboard(offset = 0, start?: number) { + const baseUrl = + 'https://mango-transaction-log.herokuapp.com/stats/pnl_leaderboard' + + const startAt = start + ? dayjs().utc().subtract(start, 'day').format('YYYY-MM-DD') + : null + + const url = startAt + ? `${baseUrl}?start_date=${startAt}&limit=25&offset=${offset}` + : `${baseUrl}?limit=25&offset=${offset}` + + const response = await fetch(url) + const parsedResponse = await response.json() + const results = parsedResponse ? parsedResponse : [] + + const currentLeaderboard = get().pnlLeaderboard + + if (currentLeaderboard.length > 0 && offset > 0) { + const updatedLeaderboard = currentLeaderboard.concat(results) + set((state) => { + state.pnlLeaderboard = updatedLeaderboard + }) + } else { + set((state) => { + state.pnlLeaderboard = results + }) + } + }, + async fetchPnlByAccount(marginAccount = null, start?: number) { + const selectedMarginAccount = + marginAccount || get().selectedMarginAccount.current + const set = get().set + + if (!selectedMarginAccount) return + + const startAt = start + ? new Date(Date.now() - start * 24 * 60 * 60 * 1000).toLocaleDateString( + 'en-ZA' + ) + : null + + const baseUrl = + 'https://mango-transaction-log.herokuapp.com/stats/pnl_leaderboard_rank' + + const url = startAt + ? `${baseUrl}/${selectedMarginAccount.publicKey.toString()}?start_date=${startAt}` + : `${baseUrl}/${selectedMarginAccount.publicKey.toString()}` + + const response = await fetch(url) + const parsedResponse = + response.status === 200 ? await response.json() : null + const results = parsedResponse ? parsedResponse : null + + set((state) => { + state.accountPnl = results + }) + }, }, })) diff --git a/yarn.lock b/yarn.lock index 57336e8..74f9f14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2515,9 +2515,9 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -"borsh@git+https://github.com/defactojob/borsh-js.git#field-mapper": +"borsh@https://github.com/defactojob/borsh-js#field-mapper": version "0.3.1" - resolved "git+https://github.com/defactojob/borsh-js.git#33a0d24af281112c0a48efb3fa503f3212443de9" + resolved "https://github.com/defactojob/borsh-js#33a0d24af281112c0a48efb3fa503f3212443de9" dependencies: "@types/bn.js" "^4.11.5" bn.js "^5.0.0"