Merge pull request #314 from blockworks-foundation/saml33/follow-accounts

follow unowned accounts on account overview
This commit is contained in:
saml33 2023-11-17 12:32:33 +11:00 committed by GitHub
commit d9307ea16b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 844 additions and 65 deletions

View File

@ -50,9 +50,9 @@ Add commits for each self-contained change and give your commits clear messages
All PRs should have a meaningful name and include a description of what the changes are.
If there are visual changes, include screenshots in the description.
- If there are visual changes, include screenshots in the description.
If the PR is unfinished include a "TODO" section with work not yet completed. If there are known issues/bugs include a section outlining what they are.
- If the PR is unfinished include a "TODO" section with work not yet completed. If there are known issues/bugs include a section outlining what they are.
#### Drafts

View File

@ -561,7 +561,7 @@ const MobileTokenListItem = ({ data }: { data: TableData }) => {
enterTo="opacity-100"
>
<Disclosure.Panel>
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 pb-4 pt-4">
<div className="mx-4 grid grid-cols-2 gap-4 border-t border-th-bkg-3 py-4">
<div className="col-span-1">
<Tooltip content={t('tooltip-collateral-value')}>
<p className="tooltip-underline text-xs text-th-fgd-3">

View File

@ -8,11 +8,14 @@ import RecentGainersLosers from './RecentGainersLosers'
import Spot from './Spot'
import useBanks from 'hooks/useBanks'
import TabsText from '@components/shared/TabsText'
import useFollowedAccounts from 'hooks/useFollowedAccounts'
import FollowedAccounts from './FollowedAccounts'
dayjs.extend(relativeTime)
const Explore = () => {
const { t } = useTranslation(['common'])
const { banks } = useBanks()
const { data: followedAccounts } = useFollowedAccounts()
const perpStats = mangoStore((s) => s.perpStats.data)
const [activeTab, setActiveTab] = useState('tokens')
@ -27,10 +30,11 @@ const Explore = () => {
const perpMarkets = mangoStore.getState().perpMarkets
const tabs: [string, number][] = [
['tokens', banks.length],
['perp-markets', perpMarkets.length],
['perp', perpMarkets.length],
['account:followed-accounts', followedAccounts?.length],
]
return tabs
}, [banks])
}, [banks, followedAccounts])
return (
<>
@ -50,7 +54,7 @@ const Explore = () => {
activeTab={activeTab}
onChange={setActiveTab}
tabs={tabsWithCount}
className="text-lg"
className="xl:text-lg"
/>
</div>
</div>
@ -71,6 +75,8 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
<PerpMarketsTable />
</div>
)
case 'account:followed-accounts':
return <FollowedAccounts />
default:
return <Spot />
}

View File

@ -0,0 +1,477 @@
import {
MangoAccount,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
import WalletIcon from '@components/icons/WalletIcon'
import EmptyState from '@components/nftMarket/EmptyState'
import ProfileImage from '@components/profile/ProfileImage'
import Change from '@components/shared/Change'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import SheenLoader from '@components/shared/SheenLoader'
import ToggleFollowButton from '@components/shared/ToggleFollowButton'
import { Disclosure } from '@headlessui/react'
import {
ArrowPathIcon,
ArrowTopRightOnSquareIcon,
ChevronDownIcon,
NoSymbolIcon,
} from '@heroicons/react/20/solid'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
import useFollowedAccounts from 'hooks/useFollowedAccounts'
import { useHiddenMangoAccounts } from 'hooks/useHiddenMangoAccounts'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup'
import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
ActivityFeed,
EmptyObject,
isDepositWithdrawActivityFeedItem,
isPerpTradeActivityFeedItem,
isSpotTradeActivityFeedItem,
isSwapActivityFeedItem,
} from 'types'
import { MANGO_DATA_API_URL } from 'utils/constants'
import { abbreviateAddress } from 'utils/formatting'
import { formatCurrencyValue, formatNumericValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
export type FollowedAccountApi = {
mango_account: string
profile_image_url: string
profile_name: string
wallet_pk: string
}
export interface FollowedAccount extends FollowedAccountApi {
mangoAccount: MangoAccount
}
const getFollowedMangoAccounts = async (accounts: FollowedAccount[]) => {
const client = mangoStore.getState().client
const mangoAccounts = []
for (const account of accounts) {
try {
const publicKey = new PublicKey(account.mango_account)
const mangoAccount = await client.getMangoAccount(publicKey, true)
if (mangoAccount) {
mangoAccounts.push({ ...account, mangoAccount: mangoAccount })
}
} catch (e) {
console.log('failed to load followed mango account', e)
}
}
return mangoAccounts
}
const FollowedAccounts = () => {
const { t } = useTranslation('account')
const { data: followedAccounts, isInitialLoading: loadingFollowedAccounts } =
useFollowedAccounts()
const [followedMangoAccounts, setFollowedMangoAccounts] = useState<
FollowedAccount[]
>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!followedAccounts || !followedAccounts.length) return
const getAccounts = async () => {
setLoading(true)
const accounts = await getFollowedMangoAccounts(followedAccounts)
setFollowedMangoAccounts(accounts)
setLoading(false)
}
getAccounts()
}, [followedAccounts])
return (
<div className="px-4 pt-4 md:px-6 md:pb-10">
{loadingFollowedAccounts || loading ? (
[...Array(3)].map((x, i) => (
<SheenLoader className="mt-2 flex flex-1" key={i}>
<div className="h-[94px] w-full bg-th-bkg-2" />
</SheenLoader>
))
) : followedMangoAccounts?.length ? (
<>
{followedMangoAccounts.map((acc: FollowedAccount) => (
<AccountDisplay account={acc} key={acc.mango_account} />
))}
</>
) : (
<div className="mt-2 flex flex-col items-center rounded-md border border-th-bkg-3 p-4">
<NoSymbolIcon className="mb-1 h-7 w-7 text-th-fgd-4" />
<p className="mb-1 text-base">{t('account:not-following-yet')}</p>
<Link href="/leaderboard" shallow>
<span className="font-bold">{t('account:find-accounts')}</span>
</Link>
</div>
)}
</div>
)
}
export default FollowedAccounts
const fetchActivityData = async (publicKey: PublicKey) => {
try {
const response = await fetch(
`${MANGO_DATA_API_URL}/stats/activity-feed?mango-account=${publicKey.toString()}&limit=10`,
)
const parsedResponse: null | EmptyObject | Array<ActivityFeed> =
await response.json()
if (Array.isArray(parsedResponse)) {
const entries = Object.entries(parsedResponse).sort((a, b) =>
b[0].localeCompare(a[0]),
)
const activity = entries
.map(([key, value]) => {
// ETH should be renamed to ETH (Portal) in the database
const symbol = value.activity_details.symbol
if (symbol === 'ETH') {
value.activity_details.symbol = 'ETH (Portal)'
}
return {
...value,
symbol: key,
}
})
.sort(
(a, b) =>
dayjs(b.block_datetime).unix() - dayjs(a.block_datetime).unix(),
)
return activity
} else return []
} catch (e) {
console.log('failed to fetch followed account activity', e)
return []
}
}
const AccountDisplay = ({ account }: { account: FollowedAccount }) => {
const { mangoAccount, profile_image_url, profile_name } = account
const { name, owner, publicKey } = mangoAccount
const { group } = useMangoGroup()
const { t } = useTranslation(['common', 'account', 'activity'])
const { hiddenAccounts, loadingHiddenAccounts } = useHiddenMangoAccounts()
const { rollingDailyData } = useAccountPerformanceData(publicKey.toString())
const isPrivateAccount = useMemo(() => {
if (!hiddenAccounts?.length) return false
return hiddenAccounts.find((acc) => acc === publicKey.toString())
}, [publicKey, hiddenAccounts])
const {
data: activityData,
isInitialLoading: loadingActivityData,
isFetching: fetchingActivityData,
refetch: refetchActivityData,
} = useQuery(
['followed-account-activity', publicKey],
() => fetchActivityData(publicKey),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: publicKey && !isPrivateAccount && !loadingHiddenAccounts,
},
)
const accountValue = useMemo(() => {
if (!group) return 0
return toUiDecimalsForQuote(mangoAccount.getEquity(group).toNumber())
}, [mangoAccount, group])
const accountPnl = useMemo(() => {
if (!group) return 0
return toUiDecimalsForQuote(mangoAccount.getPnl(group).toNumber())
}, [mangoAccount, group])
const [rollingDailyValueChange, rollingDailyPnlChange] = useMemo(() => {
if (!accountPnl || !rollingDailyData.length) return [0, 0]
const pnlChange = accountPnl - rollingDailyData[0].pnl
const valueChange = accountValue - rollingDailyData[0].account_equity
return [valueChange, pnlChange]
}, [accountPnl, accountValue, rollingDailyData])
return !isPrivateAccount ? (
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`mt-2 flex w-full items-center rounded-lg border border-th-bkg-3 p-4 md:hover:border-th-bkg-4 ${
open ? 'rounded-b-none border-b-0' : ''
}`}
>
<div className="grid w-full grid-cols-2">
<div className="col-span-2 flex h-full items-center md:col-span-1">
<AccountNameDisplay
accountName={name}
accountPk={publicKey}
profileImageUrl={profile_image_url}
profileName={profile_name}
walletPk={owner}
/>
</div>
<div className="col-span-2 mt-3 border-t border-th-bkg-3 pt-3 md:col-span-1 md:mt-0 md:border-t-0 md:pt-0">
<div className="grid grid-cols-2">
<div className="flex flex-col items-start md:items-end">
<p className="mb-1">{t('value')}</p>
<span className="font-mono">
<FormatNumericValue value={accountValue} isUsd />
</span>
<Change
change={rollingDailyValueChange}
prefix="$"
size="small"
/>
</div>
<div className="flex flex-col items-start md:items-end">
<p className="mb-1">{t('pnl')}</p>
<span className="font-mono">
<FormatNumericValue value={accountPnl} isUsd />
</span>
<Change
change={rollingDailyPnlChange}
prefix="$"
size="small"
/>
</div>
</div>
</div>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} ml-4 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</Disclosure.Button>
<Disclosure.Panel>
<div className="rounded-lg rounded-t-none border border-t-0 border-th-bkg-3 p-4 pt-0">
<div className="border-t border-th-bkg-3 pt-4">
<div className="mb-4 flex flex-wrap items-center justify-between">
<h3 className="mr-3 text-base">
{t('activity:latest-activity')}
</h3>
<div className="mt-0.5 flex items-center space-x-4">
<button
className="flex items-center focus:outline-none"
onClick={() => refetchActivityData()}
>
<ArrowPathIcon
className={`mr-1.5 h-4 w-4 ${
fetchingActivityData ? 'animate-spin' : null
}`}
/>
<span>{t('refresh')}</span>
</button>
<ToggleFollowButton
accountPk={publicKey.toString()}
showText
/>
<a
className="flex items-center text-th-fgd-2"
href={`/?address=${publicKey.toString()}`}
rel="noopener noreferrer"
target="_blank"
>
<span>{t('activity:view-account')}</span>
<ArrowTopRightOnSquareIcon className="ml-1.5 h-4 w-4" />
</a>
</div>
</div>
{loadingActivityData ? (
[...Array(4)].map((x, i) => (
<SheenLoader className="flex flex-1" key={i}>
<div className="h-10 w-full bg-th-bkg-2" />
</SheenLoader>
))
) : activityData && activityData.length ? (
activityData.map((activity, i) => (
<div
className="mt-2 rounded-md border border-th-bkg-3 p-4"
key={activity.block_datetime + i}
>
<ActivityContent activity={activity} />
</div>
))
) : (
<EmptyState text={t('activity:no-activity')} />
)}
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
) : (
<div className="mt-2 flex w-full items-center justify-between rounded-md border border-th-bkg-3 p-4">
<AccountNameDisplay
accountName={name}
accountPk={publicKey}
profileImageUrl={profile_image_url}
profileName={profile_name}
walletPk={owner}
/>
<div className="flex flex-col items-end">
<p className="mb-1">{t('account:account-is-private')}</p>
<ToggleFollowButton accountPk={publicKey.toString()} showText />
</div>
</div>
)
}
const ActivityContent = ({ activity }: { activity: ActivityFeed }) => {
const { t } = useTranslation(['common', 'activity', 'trade'])
const { mangoAccountAddress } = useMangoAccount()
if (isSwapActivityFeedItem(activity)) {
const { activity_type, block_datetime } = activity
const {
swap_in_amount,
swap_in_symbol,
swap_out_amount,
swap_out_price_usd,
swap_out_symbol,
} = activity.activity_details
return (
<div className="flex items-center justify-between">
<div>
<p className="mb-1 font-bold text-th-fgd-1">
{t(`activity:${activity_type}`)}
</p>
<p className="mb-0.5 text-th-fgd-2">{`${formatNumericValue(
swap_in_amount,
)} ${formatTokenSymbol(swap_in_symbol)} ${t(
'trade:for',
)} ${formatNumericValue(swap_out_amount)} ${formatTokenSymbol(
swap_out_symbol,
)}`}</p>
<p className="text-xs">
{dayjs(block_datetime).format('DD MMM YYYY, h:mma')}
</p>
</div>
<span className="font-mono">
{formatCurrencyValue(swap_out_amount * swap_out_price_usd)}
</span>
</div>
)
}
if (isSpotTradeActivityFeedItem(activity)) {
const { activity_type, block_datetime } = activity
const { base_symbol, price, quote_symbol, side, size } =
activity.activity_details
return (
<div className="flex items-center justify-between">
<div>
<p className="mb-1 font-bold text-th-fgd-1">
{t(`activity:${activity_type}`)}
</p>
<p className="mb-0.5 text-th-fgd-2">{`${t(
side,
)} ${size} ${base_symbol}/${quote_symbol}`}</p>
<p className="text-xs">
{dayjs(block_datetime).format('DD MMM YYYY, h:mma')}
</p>
</div>
<span className="font-mono">{formatCurrencyValue(price * size)}</span>
</div>
)
}
if (isPerpTradeActivityFeedItem(activity)) {
const { activity_type, block_datetime } = activity
const { perp_market_name, price, quantity, taker, taker_side } =
activity.activity_details
const side =
taker === mangoAccountAddress
? taker_side
: taker_side === 'bid'
? 'trade:short'
: 'trade:long'
return (
<div className="flex items-center justify-between">
<div>
<p className="mb-1 font-bold text-th-fgd-1">
{t(`activity:${activity_type}`)}
</p>
<p className="mb-0.5 text-th-fgd-2">{`${t(
side,
)} ${quantity} ${perp_market_name}`}</p>
<p className="text-xs">
{dayjs(block_datetime).format('DD MMM YYYY, h:mma')}
</p>
</div>
<span className="font-mono">
{formatCurrencyValue(price * quantity)}
</span>
</div>
)
}
if (isDepositWithdrawActivityFeedItem(activity)) {
const { activity_type, block_datetime } = activity
const { quantity, symbol, usd_equivalent } = activity.activity_details
return (
<div className="flex items-center justify-between">
<div>
<p className="mb-1 font-bold text-th-fgd-1">
{t(`activity:${activity_type}`)}
</p>
<p className="mb-0.5 text-th-fgd-2">{`${formatNumericValue(
quantity,
)} ${formatTokenSymbol(symbol)}`}</p>
<p className="text-xs text-th-fgd-4">
{dayjs(block_datetime).format('DD MMM YYYY, h:mma')}
</p>
</div>
<span className="font-mono">{formatCurrencyValue(usd_equivalent)}</span>
</div>
)
}
return null
}
const AccountNameDisplay = ({
accountName,
accountPk,
profileImageUrl,
profileName,
walletPk,
}: {
accountName: string | undefined
accountPk: PublicKey
profileImageUrl: string | undefined
profileName: string | undefined
walletPk: PublicKey
}) => {
return (
<div className="flex items-center space-x-3">
<ProfileImage
imageSize={'48'}
imageUrl={profileImageUrl}
placeholderSize={'32'}
/>
<div>
<p className="mb-1 text-left font-bold text-th-fgd-2">
{accountName ? accountName : abbreviateAddress(accountPk)}
</p>
<div className="flex items-center">
<WalletIcon className="mr-1.5 h-4 w-4" />
<p>
{profileName ? (
<span className="capitalize">{profileName}</span>
) : (
abbreviateAddress(walletPk)
)}
</p>
</div>
</div>
</div>
)
}

View File

@ -128,9 +128,9 @@ const Spot = () => {
return (
<div className="lg:-mt-10">
<div className="flex flex-col px-4 lg:flex-row lg:items-center lg:justify-end lg:px-6 2xl:px-12">
<div className="flex flex-col px-4 md:px-6 lg:flex-row lg:items-center lg:justify-end 2xl:px-12">
<div className="flex w-full flex-col lg:w-auto lg:flex-row lg:space-x-3">
<div className="relative mb-3 w-full lg:mb-0 xl:w-40">
<div className="relative mb-3 w-full lg:mb-0 lg:w-40">
<Input
heightClass="h-10 pl-8"
type="text"

View File

@ -192,8 +192,8 @@ const LeaderboardPage = () => {
<div className="grid grid-cols-12">
<div className="col-span-12 lg:col-span-8 lg:col-start-3">
<h1 className="mb-2">{t('leaderboard')}</h1>
<div className="mb-4 flex items-center justify-between">
<div>
<div className="mb-4 flex w-full flex-col md:flex-row md:items-center md:justify-between">
<div className="mb-3 md:mb-0">
<TabsText
activeTab={leaderboardToShow}
onChange={(v: string) => setLeaderboardToShow(v)}

View File

@ -10,6 +10,7 @@ import {
isEquityLeaderboard,
isPnlLeaderboard,
} from './LeaderboardPage'
import ToggleFollowButton from '@components/shared/ToggleFollowButton'
const LeaderboardTable = ({
data,
@ -60,49 +61,55 @@ const LeaderboardRow = ({
const { isTablet } = useViewport()
return !loading ? (
<a
className="flex w-full items-center justify-between rounded-md border border-th-bkg-3 px-3 py-3 md:px-4 md:hover:bg-th-bkg-2"
href={`/?address=${mango_account}`}
rel="noopener noreferrer"
target="_blank"
>
<div className="flex items-center space-x-3">
<div
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full ${
rank < 4 ? '' : 'bg-th-bkg-3'
} md:mr-2`}
>
<p
className={`relative z-10 font-bold ${
rank < 4 ? 'text-th-bkg-1' : 'text-th-fgd-3'
<div className="flex">
<div className="flex flex-1 items-center rounded-l-md border border-r-0 border-th-bkg-3 bg-th-bkg-2 px-3">
<ToggleFollowButton accountPk={mango_account} />
</div>
<a
className="flex w-full items-center justify-between rounded-md rounded-l-none border border-l-0 border-th-bkg-3 px-3 py-3 md:px-4 md:hover:bg-th-bkg-2"
href={`/?address=${mango_account}`}
rel="noopener noreferrer"
target="_blank"
>
<div className="flex w-full items-center space-x-3">
<div
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full ${
rank < 4 ? '' : 'bg-th-bkg-3'
}`}
>
{rank}
</p>
{rank < 4 ? <MedalIcon className="absolute" rank={rank} /> : null}
<p
className={`relative z-10 font-bold ${
rank < 4 ? 'text-th-bkg-1' : 'text-th-fgd-3'
}`}
>
{rank}
</p>
{rank < 4 ? <MedalIcon className="absolute" rank={rank} /> : null}
</div>
<ProfileImage
imageSize={isTablet ? '32' : '40'}
imageUrl={profile_image_url}
placeholderSize={isTablet ? '20' : '24'}
/>
<div className="flex w-full flex-col items-start sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="capitalize text-th-fgd-2 md:text-base">
{profile_name ||
wallet_pk.slice(0, 4) + '...' + wallet_pk.slice(-4)}
</p>
<p className="text-xs text-th-fgd-4">
Acc:{' '}
{mango_account.slice(0, 4) + '...' + mango_account.slice(-4)}
</p>
</div>
<span className="mr-3 mt-1 text-right font-mono sm:mt-0 md:text-base">
{formatCurrencyValue(value, 2)}
</span>
</div>
</div>
<ProfileImage
imageSize={isTablet ? '32' : '40'}
imageUrl={profile_image_url}
placeholderSize={isTablet ? '20' : '24'}
/>
<div className="text-left">
<p className="capitalize text-th-fgd-2 md:text-base">
{profile_name ||
wallet_pk.slice(0, 4) + '...' + wallet_pk.slice(-4)}
</p>
<p className="text-xs text-th-fgd-4">
Acc: {mango_account.slice(0, 4) + '...' + mango_account.slice(-4)}
</p>
</div>
</div>
<div className="flex items-center">
<span className="mr-3 text-right font-mono md:text-base">
{formatCurrencyValue(value, 2)}
</span>
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
</div>
</a>
</a>
</div>
) : (
<SheenLoader className="flex flex-1">
<div className="h-16 w-full rounded-md bg-th-bkg-2" />

View File

@ -11,13 +11,15 @@ const TabsText = ({
onChange: (tab: string) => void
className?: string
}) => {
const { t } = useTranslation(['common', 'trade'])
const { t } = useTranslation(['common', 'account', 'trade'])
return (
<div className="flex space-x-6 text-base">
{tabs.map((tab) => (
<button
className={`flex items-center space-x-2 font-bold focus:outline-none ${
activeTab === tab[0] ? 'text-th-active' : ''
activeTab === tab[0]
? 'text-th-active md:hover:text-th-active'
: 'text-th-fgd-2 md:hover:text-th-fgd-3'
} ${className}`}
onClick={() => onChange(tab[0])}
key={tab[0]}

View File

@ -0,0 +1,145 @@
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { PublicKey } from '@solana/web3.js'
import {
QueryObserverResult,
RefetchOptions,
RefetchQueryFilters,
} from '@tanstack/react-query'
import { MANGO_DATA_API_URL } from 'utils/constants'
import { notify } from 'utils/notifications'
import { StarIcon as FilledStarIcon } from '@heroicons/react/20/solid'
import { StarIcon } from '@heroicons/react/24/outline'
import { useWallet } from '@solana/wallet-adapter-react'
import useFollowedAccounts from 'hooks/useFollowedAccounts'
import Tooltip from './Tooltip'
import { useTranslation } from 'react-i18next'
import { useMemo, useState } from 'react'
import { FollowedAccountApi } from '@components/explore/FollowedAccounts'
import Loading from './Loading'
const toggleFollowAccount = async (
type: string,
mangoAccountPk: string,
publicKey: PublicKey | null,
signMessage: (message: Uint8Array) => Promise<Uint8Array>,
refetch: <TPageData>(
options?: (RefetchOptions & RefetchQueryFilters<TPageData>) | undefined,
) => Promise<QueryObserverResult>,
setLoading: (loading: boolean) => void,
) => {
try {
if (!publicKey) throw new Error('Wallet not connected!')
if (!signMessage) throw new Error('Wallet does not support message signing')
setLoading(true)
const messageObject = {
mango_account: mangoAccountPk,
action: type,
}
const messageString = JSON.stringify(messageObject)
const message = new TextEncoder().encode(messageString)
const signature = await signMessage(message)
const isPost = type === 'insert'
const method = isPost ? 'POST' : 'DELETE'
const requestOptions = {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet_pk: publicKey.toString(),
message: messageString,
signature: bs58.encode(signature),
}),
}
const response = await fetch(
`${MANGO_DATA_API_URL}/user-data/following`,
requestOptions,
)
if (response.status === 200) {
await refetch()
}
} catch {
notify({ type: 'error', title: 'Failed to follow account' })
} finally {
setLoading(false)
}
}
const ToggleFollowButton = ({
accountPk,
showText,
}: {
accountPk: string
showText?: boolean
}) => {
const { t } = useTranslation('account')
const { publicKey, signMessage } = useWallet()
const { data: followedAccounts, refetch: refetchFollowedAccounts } =
useFollowedAccounts()
const [loading, setLoading] = useState(false)
const [isFollowed, type] = useMemo(() => {
if (!followedAccounts || !followedAccounts.length) return [false, 'insert']
const followed = followedAccounts.find(
(acc: FollowedAccountApi) => acc.mango_account === accountPk,
)
if (followed) {
return [true, 'delete']
} else return [false, 'insert']
}, [accountPk, followedAccounts])
const disabled =
!accountPk ||
loading ||
!publicKey ||
!signMessage ||
(!isFollowed && followedAccounts?.length >= 10)
return (
<Tooltip
content={
!publicKey
? t('account:tooltip-connect-to-follow')
: !isFollowed && followedAccounts?.length >= 10
? t('account:tooltip-follow-max-reached')
: showText
? ''
: isFollowed
? t('account:tooltip-unfollow-account')
: t('account:tooltip-follow-account')
}
>
<button
className="flex items-center focus:outline-none disabled:opacity-50 md:hover:text-th-fgd-3"
disabled={disabled}
onClick={() =>
toggleFollowAccount(
type,
accountPk,
publicKey,
signMessage!,
refetchFollowedAccounts,
setLoading,
)
}
>
{loading ? (
<Loading />
) : isFollowed ? (
<FilledStarIcon className="h-5 w-5 text-th-active" />
) : (
<StarIcon className="h-5 w-5 text-th-fgd-3" />
)}
{showText ? (
<span className="ml-1.5">
{isFollowed ? t('unfollow') : t('follow')}
</span>
) : null}
</button>
</Tooltip>
)
}
export default ToggleFollowButton

View File

@ -0,0 +1,31 @@
import { useWallet } from '@solana/wallet-adapter-react'
import { useQuery } from '@tanstack/react-query'
import { MANGO_DATA_API_URL } from 'utils/constants'
const fetchAccountFollwers = async (walletPk: string | undefined) => {
try {
const response = await fetch(
`${MANGO_DATA_API_URL}/user-data/followers?wallet-pk=${walletPk}`,
)
const data = await response.json()
return data
} catch (e) {
console.error('failed to fetch account followers', e)
}
}
export default function useAccountFollowers() {
const { publicKey } = useWallet()
const { data, isInitialLoading, refetch } = useQuery(
['account-followers', publicKey],
() => fetchAccountFollwers(publicKey?.toString()),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!publicKey,
},
)
return { data, isInitialLoading, refetch }
}

View File

@ -5,22 +5,24 @@ import { useMemo } from 'react'
import { PerformanceDataItem } from 'types'
import { DAILY_MILLISECONDS } from 'utils/constants'
export default function useAccountPerformanceData() {
export default function useAccountPerformanceData(unownedPk?: string) {
const { mangoAccountAddress } = useMangoAccount()
const accountPkToFetch = unownedPk ? unownedPk : mangoAccountAddress
const {
data: performanceData,
isFetching: fetchingPerformanceData,
isInitialLoading: loadingPerformanceData,
} = useQuery(
['performance', mangoAccountAddress],
() => fetchAccountPerformance(mangoAccountAddress, 31),
['performance', accountPkToFetch],
() => fetchAccountPerformance(accountPkToFetch, 31),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
enabled: !!accountPkToFetch,
},
)

View File

@ -0,0 +1,35 @@
import { useWallet } from '@solana/wallet-adapter-react'
import { useQuery } from '@tanstack/react-query'
import { isEmpty } from 'lodash'
import { MANGO_DATA_API_URL } from 'utils/constants'
const fetchFollowedAccounts = async (walletPk: string | undefined) => {
try {
const response = await fetch(
`${MANGO_DATA_API_URL}/user-data/following?wallet-pk=${walletPk}`,
)
const data = await response.json()
if (isEmpty(data)) {
return []
} else return data
} catch (e) {
console.error('failed to fetch followed accounts', e)
return []
}
}
export default function useFollowedAccounts() {
const { publicKey } = useWallet()
const { data, isInitialLoading, refetch } = useQuery(
['followed-accounts', publicKey],
() => fetchFollowedAccounts(publicKey?.toString()),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!publicKey,
},
)
return { data, isInitialLoading, refetch }
}

View File

@ -15,16 +15,13 @@ const fetchAllHiddenMangoAccounts = async (): Promise<string[]> => {
}
export function useHiddenMangoAccounts() {
const { data: hiddenAccounts, isLoading: loadingHiddenAccounts } = useQuery(
['all-hidden-accounts'],
() => fetchAllHiddenMangoAccounts(),
{
const { data: hiddenAccounts, isInitialLoading: loadingHiddenAccounts } =
useQuery(['all-hidden-accounts'], () => fetchAllHiddenMangoAccounts(), {
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
},
)
})
return {
hiddenAccounts,

View File

@ -14,17 +14,18 @@ const fetchProfileDetails = async (walletPk: string | undefined) => {
}
}
export default function useProfileDetails() {
export default function useProfileDetails(unownedPk?: string) {
const { publicKey } = useWallet()
const pkToFetch = unownedPk ? unownedPk : publicKey?.toString()
const { data, isInitialLoading, refetch } = useQuery(
['profile-details', publicKey],
() => fetchProfileDetails(publicKey?.toString()),
['profile-details', pkToFetch],
() => fetchProfileDetails(pkToFetch),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!publicKey,
enabled: !!pkToFetch,
},
)
return { data, isInitialLoading, refetch }

View File

@ -6,6 +6,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, [
'account',
'common',
'leaderboard',
'notifications',

View File

@ -1,4 +1,5 @@
{
"account-is-private": "Account enabled private mode",
"account-stats": "Account Stats",
"assets": "Assets",
"assets-liabilities": "Assets & Liabilities",
@ -6,6 +7,9 @@
"cumulative-interest-chart": "Cumulative Interest Chart",
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"find-accounts": "Find Accounts",
"follow": "Follow",
"followed-accounts": "Followed Accounts",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
@ -19,10 +23,14 @@
"more-account-stats": "More Account Stats",
"no-data": "No data to display",
"no-pnl-history": "No PnL History",
"not-following-yet": "Your not following any accounts yet...",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-connect-to-follow": "Connect to follow accounts",
"tooltip-follow-account": "Follow account on your account page",
"tooltip-follow-max-reached": "Follow max reached",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value",
@ -31,7 +39,9 @@
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid",
"tooltip-total-interest": "The value of interest earned (deposits) minus interest paid (borrows)",
"tooltip-unfollow-account": "Unfollow account",
"total-funding-earned": "Total Funding Earned",
"unfollow": "Unfollow",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral",

View File

@ -13,6 +13,7 @@
"deposits": "Deposits",
"execution-price": "Execution Price",
"filter-results": "Filter",
"latest-activity": "Latest Activity",
"liquidate_perp_base_position_or_positive_pnl": "Perp Liquidation",
"liquidate_token_with_token": "Spot Liquidation",
"liquidated": "Liquidated",

View File

@ -138,6 +138,7 @@
"quantity": "Quantity",
"rate": "Rate (APR)",
"rates": "Rates (APR)",
"refresh": "Refresh",
"refresh-data": "Manually refresh data",
"remove": "Remove",
"remove-delegate": "Remove Delegate",

View File

@ -1,4 +1,5 @@
{
"account-is-private": "Account enabled private mode",
"account-stats": "Account Stats",
"assets": "Assets",
"assets-liabilities": "Assets & Liabilities",
@ -6,6 +7,9 @@
"cumulative-interest-chart": "Cumulative Interest Chart",
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"find-accounts": "Find Accounts",
"follow": "Follow",
"followed-accounts": "Followed Accounts",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
@ -19,10 +23,14 @@
"more-account-stats": "More Account Stats",
"no-data": "No data to display",
"no-pnl-history": "No PnL History",
"not-following-yet": "Your not following any accounts yet...",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-connect-to-follow": "Connect to follow accounts",
"tooltip-follow-account": "Follow account on your account page",
"tooltip-follow-max-reached": "Follow max reached",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value",
@ -31,7 +39,9 @@
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid",
"tooltip-total-interest": "The value of interest earned (deposits) minus interest paid (borrows)",
"tooltip-unfollow-account": "Unfollow account",
"total-funding-earned": "Total Funding Earned",
"unfollow": "Unfollow",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral",

View File

@ -13,6 +13,7 @@
"deposits": "Deposits",
"execution-price": "Execution Price",
"filter-results": "Filter",
"latest-activity": "Latest Activity",
"liquidate_perp_base_position_or_positive_pnl": "Perp Liquidation",
"liquidate_token_with_token": "Spot Liquidation",
"liquidated": "Liquidated",

View File

@ -138,6 +138,7 @@
"quantity": "Quantity",
"rate": "Rate (APR)",
"rates": "Rates (APR)",
"refresh": "Refresh",
"refresh-data": "Manually refresh data",
"remove": "Remove",
"remove-delegate": "Remove Delegate",

View File

@ -1,4 +1,5 @@
{
"account-is-private": "Account enabled private mode",
"account-stats": "Account Stats",
"assets": "Assets",
"assets-liabilities": "Assets & Liabilities",
@ -6,6 +7,9 @@
"cumulative-interest-chart": "Cumulative Interest Chart",
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"find-accounts": "Find Accounts",
"follow": "Follow",
"followed-accounts": "Followed Accounts",
"funding-chart": "Funding Chart",
"init-health": "Init Health",
"maint-health": "Maint Health",
@ -19,10 +23,14 @@
"more-account-stats": "More Account Stats",
"no-data": "No data to display",
"no-pnl-history": "No PnL History",
"not-following-yet": "Your not following any accounts yet...",
"pnl-chart": "PnL Chart",
"pnl-history": "PnL History",
"refresh-balance": "Refresh Balance",
"tooltip-collateral-value": "The amount of capital this token gives you to use for trades and loans.",
"tooltip-connect-to-follow": "Connect to follow accounts",
"tooltip-follow-account": "Follow account on your account page",
"tooltip-follow-max-reached": "Follow max reached",
"tooltip-free-collateral": "The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw",
"tooltip-init-health": "The contribution an asset gives to your initial account health. Initial health affects your ability to open new positions and withdraw collateral from your account. The sum of these values is equal to your account's free collateral.",
"tooltip-leverage": "Total assets value divided by account equity value",
@ -31,7 +39,9 @@
"tooltip-total-collateral": "Total value of collateral for trading and borrowing (including unsettled PnL)",
"tooltip-total-funding": "The sum of perp position funding earned and paid",
"tooltip-total-interest": "The value of interest earned (deposits) minus interest paid (borrows)",
"tooltip-unfollow-account": "Unfollow account",
"total-funding-earned": "Total Funding Earned",
"unfollow": "Unfollow",
"volume-chart": "Volume Chart",
"week-starting": "Week starting {{week}}",
"zero-collateral": "Zero Collateral",

View File

@ -13,6 +13,7 @@
"deposits": "Deposits",
"execution-price": "Execution Price",
"filter-results": "Filter",
"latest-activity": "Latest Activity",
"liquidate_perp_base_position_or_positive_pnl": "Perp Liquidation",
"liquidate_token_with_token": "Spot Liquidation",
"liquidated": "Liquidated",

View File

@ -138,6 +138,7 @@
"quantity": "Quantity",
"rate": "Rate (APR)",
"rates": "Rates (APR)",
"refresh": "Refresh",
"refresh-data": "Manually refresh data",
"remove": "Remove",
"remove-delegate": "Remove Delegate",

View File

@ -1,4 +1,5 @@
{
"account-is-private": "Account enabled private mode",
"account-stats": "帐户统计",
"assets": "资产",
"assets-liabilities": "资产和债务",
@ -6,6 +7,9 @@
"cumulative-interest-chart": "累积利息图表",
"daily-volume": "24小时交易量",
"export": "导出{{dataType}}",
"find-accounts": "Find Accounts",
"follow": "Follow",
"followed-accounts": "Followed Accounts",
"funding-chart": "资金费图表",
"health-contributions": "健康度贡献",
"init-health": "初始健康度",
@ -19,10 +23,14 @@
"more-account-stats": "更多帐户统计",
"no-data": "无数据可显示",
"no-pnl-history": "无盈亏历史",
"not-following-yet": "Your not following any accounts yet...",
"pnl-chart": "盈亏图表",
"pnl-history": "盈亏历史",
"refresh-balance": "更新余额",
"tooltip-collateral-value": "该币种为你提供用于交易与借贷的资本金额。",
"tooltip-connect-to-follow": "Connect to follow accounts",
"tooltip-follow-account": "Follow account on your account page",
"tooltip-follow-max-reached": "Follow max reached",
"tooltip-free-collateral": "你可用于交易和借贷的余额。当你可用的质押品达到0元时你将不能交易、借贷或取款",
"tooltip-init-health": "资产对你的初始账户健康度的贡献。初始健康度会影响你建仓和从账户中提取质押品的能力。这些值的总和等于你帐户的可用质押品。" ,
"tooltip-leverage": "总资价值除以账户余额",
@ -31,7 +39,9 @@
"tooltip-total-collateral": "可用于交易和借贷的质押品(包括未结清的盈亏)",
"tooltip-total-funding": "赚取和支付的合约资金费总和",
"tooltip-total-interest": "你获取的利息(存款)减你付出的利息(借贷)",
"tooltip-unfollow-account": "Unfollow account",
"total-funding-earned": "总资金费",
"unfollow": "Unfollow",
"volume-chart": "交易量图表",
"week-starting": "从{{week}}来算的一周",
"zero-balances": "显示等于零的余额",

View File

@ -13,6 +13,7 @@
"deposits": "存款",
"execution-price": "成交价格",
"filter-results": "筛选",
"latest-activity": "Latest Activity",
"liquidate_perp_base_position_or_positive_pnl": "合约清算",
"liquidate_token_with_token": "现货清算",
"liquidated": "被清算",

View File

@ -137,6 +137,7 @@
"quantity": "数量",
"rate": "利率(APR)",
"rates": "利率(APR)",
"refresh": "Refresh",
"refresh-data": "手动更新数据",
"remove": "删除",
"remove-delegate": "铲除委托",

View File

@ -1,4 +1,5 @@
{
"account-is-private": "Account enabled private mode",
"account-stats": "帳戶統計",
"assets": "資產",
"assets-liabilities": "資產和債務",
@ -6,6 +7,9 @@
"cumulative-interest-chart": "累積利息圖表",
"daily-volume": "24小時交易量",
"export": "導出{{dataType}}",
"find-accounts": "Find Accounts",
"follow": "Follow",
"followed-accounts": "Followed Accounts",
"funding-chart": "資金費圖表",
"health-contributions": "健康度貢獻",
"init-health": "初始健康度",
@ -19,10 +23,14 @@
"more-account-stats": "更多帳戶統計",
"no-data": "無數據可顯示",
"no-pnl-history": "無盈虧歷史",
"not-following-yet": "Your not following any accounts yet...",
"pnl-chart": "盈虧圖表",
"pnl-history": "盈虧歷史",
"refresh-balance": "更新餘額",
"tooltip-collateral-value": "該幣種為你提供用於交易與借貸的資本金額。",
"tooltip-connect-to-follow": "Connect to follow accounts",
"tooltip-follow-account": "Follow account on your account page",
"tooltip-follow-max-reached": "Follow max reached",
"tooltip-free-collateral": "你可用於交易和借貸的餘額。當你可用的質押品達到0元時你將不能交易、借貸或取款",
"tooltip-init-health": "資產對你的初始賬戶健康度的貢獻。初始健康度會影響你建倉和從賬戶中提取質押品的能力。這些值的總和等於你帳戶的可用質押品。",
"tooltip-leverage": "總資價值除以賬戶餘額",
@ -31,7 +39,9 @@
"tooltip-total-collateral": "可用於交易和借貸的質押品(包括未結清的盈虧)",
"tooltip-total-funding": "賺取和支付的合約資金費總和",
"tooltip-total-interest": "你獲取的利息(存款)減你付出的利息(借貸)",
"tooltip-unfollow-account": "Unfollow account",
"total-funding-earned": "總資金費",
"unfollow": "Unfollow",
"volume-chart": "交易量圖表",
"week-starting": "從{{week}}來算的一周",
"zero-balances": "顯示等於零的餘額",

View File

@ -13,6 +13,7 @@
"deposits": "存款",
"execution-price": "成交價格",
"filter-results": "篩選",
"latest-activity": "Latest Activity",
"liquidate_perp_base_position_or_positive_pnl": "合約清算",
"liquidate_token_with_token": "現貨清算",
"liquidated": "被清算",

View File

@ -137,6 +137,7 @@
"quantity": "數量",
"rate": "利率(APR)",
"rates": "利率(APR)",
"refresh": "Refresh",
"refresh-data": "手動更新數據",
"remove": "刪除",
"remove-delegate": "剷除委託",

View File

@ -308,6 +308,12 @@ export interface SwapActivity {
activity_type: string
}
interface DepositWithdrawActivity {
activity_details: DepositWithdrawFeedItem
block_datetime: string
activity_type: string
}
export function isLiquidationActivityFeedItem(
item: ActivityFeed,
): item is LiquidationActivity {
@ -344,6 +350,15 @@ export function isSwapActivityFeedItem(
return false
}
export function isDepositWithdrawActivityFeedItem(
item: ActivityFeed,
): item is DepositWithdrawActivity {
if (item.activity_type === 'deposit' || item.activity_type === 'withdraw') {
return true
}
return false
}
export function isPerpLiquidation(
activityDetails: SpotOrPerpLiquidationItem,
): activityDetails is PerpLiquidationFeedItem {