import { MangoAccountLayout } from '@blockworks-foundation/mango-client' import { ChevronRightIcon, UserGroupIcon, ArrowSmDownIcon, ArrowSmUpIcon, LinkIcon, ExclamationCircleIcon, UserCircleIcon, PencilIcon, } from '@heroicons/react/outline' import { useWallet } from '@solana/wallet-adapter-react' import { PublicKey } from '@solana/web3.js' import { ElementTitle } from 'components' import Button from 'components/Button' import MangoAccountCard, { numberCurrencyCompacter, } from 'components/MangoAccountCard' import Modal from 'components/Modal' import Input from 'components/Input' import ProfileImage from 'components/ProfileImage' import ProfileImageButton from 'components/ProfileImageButton' import SelectMangoAccount from 'components/SelectMangoAccount' import Tabs from 'components/Tabs' import dayjs from 'dayjs' import { useTranslation } from 'next-i18next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useState } from 'react' import useMangoStore from 'stores/useMangoStore' import { abbreviateAddress, formatUsdValue } from 'utils' import { notify } from 'utils/notifications' import { Label } from 'components' import Select from 'components/Select' import EmptyState from 'components/EmptyState' import { handleWalletConnect } from 'components/ConnectWalletButton' import bs58 from 'bs58' import Loading from 'components/Loading' import InlineNotification from 'components/InlineNotification' import { startCase } from 'lodash' import useMangoAccount from 'hooks/useMangoAccount' export async function getStaticProps({ locale }) { return { props: { ...(await serverSideTranslations(locale, ['common', 'profile'])), // Will be passed to the page component as props }, } } const TABS = ['following', 'followers'] export default function Profile() { const { t } = useTranslation(['common', 'profile']) const [profileData, setProfileData] = useState(null) const [walletMangoAccounts, setWalletMangoAccounts] = useState([]) const [loadMangoAccounts, setLoadMangoAccounts] = useState(false) const { initialLoad } = useMangoAccount() const [walletMangoAccountsStats, setWalletMangoAccountsStats] = useState< any[] >([]) const [loadMangoAccountsStats, setLoadMangoAccountsStats] = useState(false) const [following, setFollowing] = useState([]) const [followers, setFollowers] = useState([]) const [activeTab, setActiveTab] = useState('following') const [loadFollowing, setLoadFollowing] = useState(false) const [loadFollowers, setLoadFollowers] = useState(false) const [showEditProfile, setShowEditProfile] = useState(false) const [initialFollowingLoad, setInitialFollowingLoad] = useState(false) const [loadProfileDetails, setLoadProfileDetails] = useState(false) const [profilePk, setProfilePk] = useState('') const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const mangoClient = useMangoStore((s) => s.connection.client) const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache) const mangoAccounts = useMangoStore((s) => s.mangoAccounts) const { publicKey, connected, wallet } = useWallet() const router = useRouter() const { name } = router.query const ownedProfile = useMangoStore((s) => s.profile.details) const loadOwnedProfile = useMangoStore((s) => s.profile.loadDetails) const handleConnect = useCallback(() => { if (wallet) { handleWalletConnect(wallet) } }, [wallet]) useEffect(() => { if (profileData) { setProfilePk(profileData.wallet_pk) } setInitialFollowingLoad(false) setActiveTab('following') }, [profileData]) useEffect(() => { const fetchProfileDetails = async () => { setLoadProfileDetails(true) try { const response = await fetch( `https://mango-transaction-log.herokuapp.com/v3/user-data/profile-details?profile-name=${name ?.toString() .replace(/-/g, ' ')}` ) const data = await response.json() setProfileData(data) setLoadProfileDetails(false) } catch (e) { notify({ type: 'error', title: t('profile:profile-fetch-fail') }) console.log(e) setLoadProfileDetails(false) } } if (name) { fetchProfileDetails() } else { if (loadOwnedProfile) { setLoadProfileDetails(true) } else { setProfileData(ownedProfile) setLoadProfileDetails(false) } } }, [name, loadOwnedProfile]) useEffect(() => { if (mangoGroup && profilePk && !initialFollowingLoad) { fetchFollowing() } }, [mangoGroup, profilePk]) useEffect(() => { if (profilePk) { fetchFollowers() } }, [profilePk]) useEffect(() => { if (mangoGroup) { const getProfileAccounts = async (unownedPk: string) => { setLoadMangoAccounts(true) const accounts = await fetchAllMangoAccounts(new PublicKey(unownedPk)) if (accounts) { setWalletMangoAccounts(accounts) } setLoadMangoAccounts(false) } if (profilePk) { getProfileAccounts(profilePk.toString()) } else { setWalletMangoAccounts(mangoAccounts) } } }, [profilePk, mangoAccounts, mangoGroup]) useEffect(() => { const getAccountsStats = async () => { setLoadMangoAccountsStats(true) const accountsStats: any[] = [] for (const acc of walletMangoAccounts) { const stats = await fetchAccountStats(acc.publicKey.toString()) accountsStats.push({ mangoAccount: acc.publicKey.toString(), stats, }) } setWalletMangoAccountsStats(accountsStats) setLoadMangoAccountsStats(false) } if (walletMangoAccounts.length) { getAccountsStats() } }, [walletMangoAccounts]) const fetchFollowers = async () => { if (!publicKey && !profilePk) return const pubkey = profilePk ? profilePk : publicKey ? publicKey?.toString() : null setLoadFollowers(true) try { const followerRes = await fetch( `https://mango-transaction-log.herokuapp.com/v3/user-data/followers?wallet-pk=${pubkey}` ) const parsedResponse = await followerRes.json() if (parsedResponse.length > 0) { setFollowers(parsedResponse) } else { setFollowers([]) } setLoadFollowers(false) } catch { setLoadFollowers(false) notify({ type: 'error', title: 'Unable to load followers', }) } } const fetchFollowing = async () => { if (!mangoGroup || !profilePk) return setLoadFollowing(true) try { const followingInfo: any[] = [] const followingRes = await fetch( `https://mango-transaction-log.herokuapp.com/v3/user-data/following?wallet-pk=${profilePk}` ) const parsedResponse = await followingRes.json() for (let i = 0; i < parsedResponse.length; i++) { const pk = new PublicKey(parsedResponse[i].mango_account) const mangoAccount = await mangoClient.getMangoAccount( pk, mangoGroup?.dexProgramId ) const stats = await fetchAccountStats(parsedResponse[i].mango_account) followingInfo.push({ ...parsedResponse[i], mango_account: mangoAccount, stats, }) } setFollowing(followingInfo) setLoadFollowing(false) setInitialFollowingLoad(true) } catch { notify({ type: 'error', title: 'Unable to load following', }) setLoadFollowing(false) } } const fetchAccountStats = async (pk) => { const response = await fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/account-performance-detailed?mango-account=${pk}&start-date=${dayjs() .subtract(1, 'day') .format('YYYY-MM-DD')}` ) const parsedResponse = await response.json() const entries: any = Object.entries(parsedResponse).sort((a, b) => b[0].localeCompare(a[0]) ) const stats = entries .map(([key, value]) => { return { ...value, time: key } }) .filter((x) => x) return stats } const fetchAllMangoAccounts = async (walletPk) => { if (!walletPk || !mangoGroup) return const delegateFilter = [ { memcmp: { offset: MangoAccountLayout.offsetOf('delegate'), bytes: walletPk.toBase58(), }, }, ] const accountSorter = (a, b) => a.publicKey.toBase58() > b.publicKey.toBase58() ? 1 : -1 return Promise.all([ mangoClient.getMangoAccountsForOwner(mangoGroup, walletPk, true), mangoClient.getAllMangoAccounts(mangoGroup, delegateFilter, false), ]) .then((values) => { const [mangoAccounts, delegatedAccounts] = values if (mangoAccounts.length + delegatedAccounts.length > 0) { const sortedAccounts = mangoAccounts .slice() .sort(accountSorter) .concat(delegatedAccounts.sort(accountSorter)) return sortedAccounts } }) .catch((err) => { notify({ type: 'error', title: 'Unable to load mango account', description: err.message, }) console.log('Could not get margin accounts for wallet', err) }) } const handleTabChange = (tabName) => { setActiveTab(tabName) } const canEdit = useMemo(() => { if (publicKey) { return publicKey.toString() === profileData?.wallet_pk } return false }, [publicKey, profileData]) const totalValue = useMemo(() => { return walletMangoAccounts.reduce((a, c) => { const value = c.computeValue(mangoGroup, mangoCache) return a + Number(value) }, 0) }, [walletMangoAccounts]) const totalPnl = useMemo(() => { return walletMangoAccountsStats.reduce((a, c) => { const value = c.stats.length > 0 ? c.stats[0].pnl : 0 return a + value }, 0) }, [walletMangoAccountsStats]) const accountsLoaded = publicKey ? !loadMangoAccounts && !initialLoad : !loadMangoAccounts const accountsStatsLoaded = publicKey ? !loadMangoAccounts && !initialLoad && !loadMangoAccountsStats : !loadMangoAccounts && !loadMangoAccountsStats return name && !profileData && !loadProfileDetails ? (
🙃
} title={t('profile:no-profile-exists')} /> ) : (
{connected || name ? (
{!loadProfileDetails ? ( <>

{profileData?.profile_name}

{t( `profile:${profileData?.trader_category.toLowerCase()}` )}
) : (
)}
{!loadFollowing ? (

{following.length}{' '} {t('following')}

) : (
)} {!loadFollowers ? (

{followers.length}{' '} {t('followers')}

) : (
)}
{canEdit ? ( ) : null} {!canEdit && publicKey ? ( ) : null}
) : null}
{connected || name ? (
{accountsLoaded ? ( <>
{t('profile:total-value')}
{formatUsdValue(totalValue)}
) : (
)}
{accountsStatsLoaded ? ( <>
{t('profile:total-pnl')}
{formatUsdValue(totalPnl)}
) : (
)}
{activeTab === 'following' ? ( loadFollowing ? (
) : following.length > 0 ? ( following.map((user) => { const accountEquity = user.mango_account.computeValue( mangoGroup, mangoCache ) const pnl: number = user.stats.length > 0 ? user.stats[0].pnl : 0 return ( ) }) ) : (
} title={t('profile:no-following')} />
) ) : null} {activeTab === 'followers' ? ( loadFollowers ? (
) : followers.length > 0 ? ( followers.map((user) => { return ( ) }) ) : (
} title={t('profile:no-followers')} />
) ) : null}
) : (
} onClickButton={handleConnect} title={t('connect-wallet')} />
)} {showEditProfile ? ( setShowEditProfile(false)} profile={profileData} /> ) : null}
) } const TRADER_CATEGORIES = [ 'day-trader', 'degen', 'discretionary', 'market-maker', 'swing-trader', 'trader', 'yolo', ] const EditProfileModal = ({ isOpen, onClose, profile, }: { isOpen: boolean onClose: () => void profile?: any }) => { const { t } = useTranslation(['common', 'profile']) const { publicKey, signMessage } = useWallet() const [profileName, setProfileName] = useState( startCase(profile?.profile_name) || '' ) const [traderCategory, setTraderCategory] = useState( profile?.trader_category || TRADER_CATEGORIES[5] ) const [inputErrors, setInputErrors] = useState({}) const [loadUniquenessCheck, setLoadUniquenessCheck] = useState(false) const [loadUpdateProfile, setLoadUpdateProfile] = useState(false) const [updateError, setUpdateError] = useState('') const actions = useMangoStore((s) => s.actions) const validateProfileName = async (name) => { const re = /^([a-zA-Z0-9]+\s)*[a-zA-Z0-9]+$/ if (!re.test(name)) { setInputErrors({ ...inputErrors, regex: t('profile:invalid-characters'), }) } if (name.length > 29) { setInputErrors({ ...inputErrors, length: t('profile:length-error'), }) } try { setLoadUniquenessCheck(true) const response = await fetch( `https://mango-transaction-log.herokuapp.com/v3/user-data/check-profile-name-unique?profile-name=${name.toLowerCase()}` ) const uniquenessCheck = await response.json() setLoadUniquenessCheck(false) if (response.status === 200 && !uniquenessCheck) { setInputErrors({ ...inputErrors, uniqueness: t('profile:uniqueness-fail'), }) } } catch { setInputErrors({ ...inputErrors, api: t('profile:uniqueness-api-fail'), }) setLoadUniquenessCheck(false) } } const onChangeNameInput = (name) => { setProfileName(name) setInputErrors({}) } const saveProfile = async () => { setUpdateError('') const name = profileName.toLowerCase() if (profile?.profile_name !== name) { await validateProfileName(name) } if (!Object.keys(inputErrors).length) { setLoadUpdateProfile(true) try { if (!publicKey) throw new Error('Wallet not connected!') if (!signMessage) throw new Error('Wallet does not support message signing!') const messageString = JSON.stringify({ profile_name: name, trader_category: traderCategory, profile_image_url: profile?.profile_image_url, }) const message = new TextEncoder().encode(messageString) const signature = await signMessage(message) const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wallet_pk: publicKey.toString(), message: messageString, signature: bs58.encode(signature), }), } const response = await fetch( 'https://mango-transaction-log.herokuapp.com/v3/user-data/profile-details', requestOptions ) if (response.status === 200) { setLoadUpdateProfile(false) await actions.fetchProfileDetails(publicKey.toString()) onClose() notify({ type: 'success', title: t('profile:profile-update-success'), }) } } catch { setLoadUpdateProfile(false) setUpdateError(t('profile:profile-update-fail')) } } } return ( {t('profile:edit-profile')} {updateError ? (
) : null}
onChangeNameInput(e.target.value)} /> {Object.keys(inputErrors).length ? (

{Object.values(inputErrors).toString()}

) : null}
) } const Accounts = ({ accounts, accountsStats, canEdit, fetchFollowers, loaded, }: { accounts: any[] accountsStats: any[] canEdit: boolean fetchFollowers: () => void loaded: boolean }) => { const { t } = useTranslation(['common', 'profile']) const { publicKey, signMessage } = useWallet() const actions = useMangoStore((s) => s.actions) const following = useMangoStore((s) => s.profile.following) useEffect(() => { if (publicKey && !canEdit) { actions.fetchProfileFollowing(publicKey?.toString()) } }, [publicKey, canEdit]) const handleToggleFollow = async ( isFollowed: boolean, mangoAccountPk: string ) => { if (publicKey) { isFollowed ? await actions.unfollowAccount(mangoAccountPk, publicKey, signMessage) : await actions.followAccount(mangoAccountPk, publicKey, signMessage) fetchFollowers() } } return (

{t('accounts')}

{canEdit ? ( loaded ? ( accounts.length ? ( ) : (

{t('no-account-found')}

) ) : (
) ) : loaded ? ( accounts.length ? (
{accounts.map((acc) => { const statsAccount = accountsStats.find( (a) => a.mangoAccount === acc.publicKey.toString() ) const pnl: number = statsAccount && statsAccount.stats.length > 0 ? statsAccount.stats[0].pnl : 0 const isFollowed = following.find( (a) => a.mango_account === acc.publicKey.toString() ) return (
{isFollowed ? ( )}
) })}
) : (

{t('no-account-found')}

) ) : (
)}
) }