support nfts as profile pics

This commit is contained in:
saml33 2022-05-11 13:29:35 +10:00
parent a3fc5b2a57
commit 2aeff1032e
21 changed files with 2837 additions and 87 deletions

View File

@ -12,15 +12,17 @@ import {
CurrencyDollarIcon, CurrencyDollarIcon,
DuplicateIcon, DuplicateIcon,
LogoutIcon, LogoutIcon,
UserCircleIcon,
} from '@heroicons/react/outline' } from '@heroicons/react/outline'
import { notify } from 'utils/notifications' import { notify } from 'utils/notifications'
import { abbreviateAddress, copyToClipboard } from 'utils' import { abbreviateAddress, copyToClipboard } from 'utils'
import useMangoStore from 'stores/useMangoStore' import useMangoStore from 'stores/useMangoStore'
import { ProfileIcon, WalletIcon } from './icons' import { WalletIcon, ProfileIcon } from './icons'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { WalletSelect } from 'components/WalletSelect' import { WalletSelect } from 'components/WalletSelect'
import AccountsModal from './AccountsModal' import AccountsModal from './AccountsModal'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import NftProfilePicModal from './NftProfilePicModal'
export const handleWalletConnect = (wallet: Wallet) => { export const handleWalletConnect = (wallet: Wallet) => {
if (!wallet) { if (!wallet) {
@ -43,11 +45,15 @@ export const handleWalletConnect = (wallet: Wallet) => {
export const ConnectWalletButton: React.FC = () => { export const ConnectWalletButton: React.FC = () => {
const { connected, publicKey, wallet, wallets, select } = useWallet() const { connected, publicKey, wallet, wallets, select } = useWallet()
const { t } = useTranslation('common') const { t } = useTranslation(['common', 'profile'])
const pfp = useMangoStore((s) => s.wallet.pfp) const pfp = useMangoStore((s) => s.wallet.pfp)
const loadingTransaction = useMangoStore(
(s) => s.wallet.nfts.loadingTransaction
)
const set = useMangoStore((s) => s.set) const set = useMangoStore((s) => s.set)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const [showAccountsModal, setShowAccountsModal] = useState(false) const [showAccountsModal, setShowAccountsModal] = useState(false)
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
const installedWallets = useMemo(() => { const installedWallets = useMemo(() => {
const installed: Wallet[] = [] const installed: Wallet[] = []
@ -77,6 +83,10 @@ export const ConnectWalletButton: React.FC = () => {
setShowAccountsModal(false) setShowAccountsModal(false)
}, []) }, [])
const handleCloseProfilePicModal = useCallback(() => {
setShowProfilePicModal(false)
}, [])
const handleDisconnect = useCallback(() => { const handleDisconnect = useCallback(() => {
wallet?.adapter?.disconnect() wallet?.adapter?.disconnect()
set((state) => { set((state) => {
@ -107,11 +117,21 @@ export const ConnectWalletButton: React.FC = () => {
<Menu> <Menu>
{({ open }) => ( {({ open }) => (
<div className="relative" id="profile-menu-tip"> <div className="relative" id="profile-menu-tip">
<Menu.Button className="flex h-10 w-10 items-center justify-center rounded-full bg-th-bkg-4 text-white hover:bg-th-bkg-4 hover:text-th-fgd-3 focus:outline-none"> <Menu.Button
className={`flex h-10 w-10 items-center justify-center rounded-full bg-th-bkg-button hover:bg-th-bkg-4 hover:bg-th-bkg-4 hover:text-th-fgd-3 focus:outline-none ${
loadingTransaction ? 'animate-pulse bg-th-bkg-4' : ''
}`}
>
{pfp?.isAvailable ? ( {pfp?.isAvailable ? (
<img alt="" src={pfp.url} className="rounded-full" /> <img
alt=""
src={pfp.url}
className={`default-transition h-10 w-10 rounded-full hover:opacity-60 ${
loadingTransaction ? 'opacity-40' : ''
}`}
/>
) : ( ) : (
<ProfileIcon className="h-6 w-6" /> <ProfileIcon className="h-6 w-6 text-th-fgd-3" />
)} )}
</Menu.Button> </Menu.Button>
<Transition <Transition
@ -135,6 +155,19 @@ export const ConnectWalletButton: React.FC = () => {
<div className="pl-2 text-left">{t('accounts')}</div> <div className="pl-2 text-left">{t('accounts')}</div>
</button> </button>
</Menu.Item> </Menu.Item>
<Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
onClick={() => setShowProfilePicModal(true)}
>
<UserCircleIcon className="h-4 w-4" />
<div className="pl-2 text-left">
{pfp?.isAvailable
? t('profile:edit-profile-pic')
: t('profile:set-profile-pic')}
</div>
</button>
</Menu.Item>
<Menu.Item> <Menu.Item>
<button <button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none" className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-primary focus:outline-none"
@ -198,6 +231,12 @@ export const ConnectWalletButton: React.FC = () => {
isOpen={showAccountsModal} isOpen={showAccountsModal}
/> />
)} )}
{showProfilePicModal && (
<NftProfilePicModal
onClose={handleCloseProfilePicModal}
isOpen={showProfilePicModal}
/>
)}
</> </>
) )
} }

View File

@ -0,0 +1,260 @@
import { useState, useEffect } from 'react'
import { PublicKey } from '@solana/web3.js'
import { notify } from 'utils/notifications'
import useMangoStore from '../stores/useMangoStore'
import { useWallet } from '@solana/wallet-adapter-react'
import { PhotographIcon } from '@heroicons/react/outline'
import Modal from './Modal'
import { ElementTitle } from './styles'
import {
createRemoveProfilePictureTransaction,
createSetProfilePictureTransaction,
} from '@solflare-wallet/pfp'
import Button from './Button'
import { useTranslation } from 'next-i18next'
import { connectionSelector } from '../stores/selectors'
import { LinkButton } from 'components'
interface SelectedNft {
mint: PublicKey
tokenAddress: PublicKey
}
const ImgWithLoader = (props) => {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative">
{isLoading && (
<PhotographIcon className="absolute left-1/2 top-1/2 z-10 h-1/4 w-1/4 -translate-x-1/2 -translate-y-1/2 transform animate-pulse text-th-fgd-4" />
)}
<img {...props} onLoad={() => setIsLoading(false)} />
</div>
)
}
const NftProfilePicModal = ({ isOpen, onClose }) => {
const { t } = useTranslation(['common', 'profile'])
const connection = useMangoStore(connectionSelector)
const { publicKey, wallet } = useWallet()
const pfp = useMangoStore((s) => s.wallet.pfp)
const nfts = useMangoStore((s) => s.wallet.nfts.data)
const nftAccounts = useMangoStore((s) => s.wallet.nfts.accounts)
const initialLoad = useMangoStore((s) => s.wallet.nfts.initialLoad)
const nftsLoading = useMangoStore((s) => s.wallet.nfts.loading)
const [selectedProfile, setSelectedProfile] = useState<SelectedNft | null>(
null
)
const actions = useMangoStore((s) => s.actions)
const mangoClient = useMangoStore.getState().connection.client
const [offset, setOffset] = useState(0)
const setMangoStore = useMangoStore((s) => s.set)
useEffect(() => {
if (publicKey) {
actions.fetchNftAccounts(connection, publicKey)
}
}, [publicKey])
useEffect(() => {
if (!initialLoad && publicKey && nftAccounts.length > 0) {
actions.fetchNfts(connection, publicKey)
}
}, [publicKey, initialLoad, nftAccounts])
useEffect(() => {
if (pfp?.isAvailable && pfp.mintAccount && pfp.tokenAccount) {
setSelectedProfile({
mint: pfp.mintAccount,
tokenAddress: pfp.tokenAccount,
})
}
}, [pfp])
const handleSaveProfilePic = async () => {
if (!publicKey || !selectedProfile || !wallet) {
return
}
try {
setMangoStore((state) => {
state.wallet.nfts.loadingTransaction = true
})
onClose()
const transaction = await createSetProfilePictureTransaction(
publicKey,
selectedProfile.mint,
selectedProfile.tokenAddress
)
if (transaction) {
const txid = await mangoClient.sendTransaction(
transaction,
wallet.adapter,
[]
)
if (txid) {
notify({
title: t('profile:profile-pic-success'),
description: '',
txid,
})
} else {
notify({
title: t('profile:profile-pic-failure'),
description: t('transaction-failed'),
})
}
} else {
notify({
title: t('profile:profile-pic-failure'),
description: t('transaction-failed'),
})
}
} catch (e) {
notify({
title: t('profile:profile-pic-failure'),
description: e.message,
txid: e.txid,
type: 'error',
})
} finally {
actions.fetchProfilePicture(wallet)
setMangoStore((state) => {
state.wallet.nfts.loadingTransaction = false
})
}
}
const handleRemoveProfilePic = async () => {
if (!publicKey || !wallet) {
return
}
try {
setMangoStore((state) => {
state.wallet.nfts.loadingTransaction = true
})
onClose()
const transaction = await createRemoveProfilePictureTransaction(publicKey)
if (transaction) {
const txid = await mangoClient.sendTransaction(
transaction,
wallet.adapter,
[]
)
if (txid) {
notify({
title: t('profile:profile-pic-remove-success'),
description: '',
txid,
})
} else {
notify({
title: t('profile:profile-pic-remove-failure'),
description: t('transaction-failed'),
})
}
} else {
notify({
title: t('profile:profile-pic-remove-failure'),
description: t('transaction-failed'),
})
}
} catch (e) {
notify({
title: t('profile:profile-pic-remove-failure'),
description: e.message,
txid: e.txid,
type: 'error',
})
} finally {
actions.fetchProfilePicture(wallet)
setMangoStore((state) => {
state.wallet.nfts.loadingTransaction = false
})
}
}
const handleLoadMore = async () => {
const offsetNfts = offset + 9
await actions.fetchNfts(connection, publicKey, offsetNfts)
setOffset(offsetNfts)
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>
<div className="my-3 flex w-full items-center justify-between">
<ElementTitle noMarginBottom>
{t('profile:choose-profile')}
</ElementTitle>
<div className="flex items-center space-x-4">
<Button
disabled={!selectedProfile}
onClick={() => handleSaveProfilePic()}
>
{t('save')}
</Button>
{pfp?.isAvailable ? (
<LinkButton
className="text-xs"
onClick={() => handleRemoveProfilePic()}
>
{t('profile:remove')}
</LinkButton>
) : null}
</div>
</div>
</Modal.Header>
{nftAccounts.length > 0 ? (
<div className="flex flex-col items-center">
<div className="mb-4 grid w-full grid-flow-row grid-cols-3 gap-4">
{nfts.map((n) => {
return (
<button
className={`default-transitions col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-3 py-4 hover:bg-th-bkg-4 ${
selectedProfile?.tokenAddress.toString() === n.tokenAddress
? 'border-th-primary'
: 'border-th-bkg-3'
}`}
key={n.tokenAddress}
onClick={() =>
setSelectedProfile({
mint: new PublicKey(n.mint),
tokenAddress: new PublicKey(n.tokenAddress),
})
}
>
<ImgWithLoader
className="h-20 w-20 flex-shrink-0 rounded-full"
src={n.val.image}
/>
</button>
)
})}
{nftsLoading
? [
...Array(
nftAccounts.length - nfts.length > 9
? 9
: nftAccounts.length - nfts.length
),
].map((i) => (
<div
className="col-span-1 h-28 animate-pulse rounded-md bg-th-bkg-3"
key={i}
/>
))
: null}
</div>
{nftAccounts.length !== nfts.length ? (
<LinkButton onClick={() => handleLoadMore()}>
{t('show-more')}
</LinkButton>
) : null}
</div>
) : null}
</Modal>
)
}
export default NftProfilePicModal

View File

@ -21,6 +21,7 @@
"@headlessui/react": "^0.0.0-insiders.2dbc38c", "@headlessui/react": "^0.0.0-insiders.2dbc38c",
"@heroicons/react": "^1.0.0", "@heroicons/react": "^1.0.0",
"@jup-ag/react-hook": "^1.0.0-beta.22", "@jup-ag/react-hook": "^1.0.0-beta.22",
"@nfteyez/sol-rayz": "^0.10.2",
"@notifi-network/notifi-react-hooks": "^0.12.1", "@notifi-network/notifi-react-hooks": "^0.12.1",
"@project-serum/serum": "0.13.55", "@project-serum/serum": "0.13.55",
"@project-serum/sol-wallet-adapter": "0.2.0", "@project-serum/sol-wallet-adapter": "0.2.0",

View File

@ -54,6 +54,8 @@ import { Menu, Transition } from '@headlessui/react'
import { useWallet } from '@solana/wallet-adapter-react' import { useWallet } from '@solana/wallet-adapter-react'
import { handleWalletConnect } from 'components/ConnectWalletButton' import { handleWalletConnect } from 'components/ConnectWalletButton'
import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup' import { MangoAccountLookup } from 'components/account_page/MangoAccountLookup'
import NftProfilePicModal from 'components/NftProfilePicModal'
import { ProfileIcon } from 'components'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
@ -64,6 +66,7 @@ export async function getStaticProps({ locale }) {
'delegate', 'delegate',
'alerts', 'alerts',
'share-modal', 'share-modal',
'profile',
])), ])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
@ -92,6 +95,11 @@ export default function Account() {
const [mngoAccrued, setMngoAccrued] = useState(ZERO_BN) const [mngoAccrued, setMngoAccrued] = useState(ZERO_BN)
const [viewIndex, setViewIndex] = useState(0) const [viewIndex, setViewIndex] = useState(0)
const [activeTab, setActiveTab] = useState(TABS[0]) const [activeTab, setActiveTab] = useState(TABS[0])
const [showProfilePicModal, setShowProfilePicModal] = useState(false)
const pfp = useMangoStore((s) => s.wallet.pfp)
const loadingTransaction = useMangoStore(
(s) => s.wallet.nfts.loadingTransaction
)
const connecting = wallet?.adapter?.connecting const connecting = wallet?.adapter?.connecting
const isMobile = width ? width < breakpoints.sm : false const isMobile = width ? width < breakpoints.sm : false
@ -120,6 +128,10 @@ export default function Account() {
setShowDelegateModal(false) setShowDelegateModal(false)
}, []) }, [])
const handleCloseProfilePicModal = useCallback(() => {
setShowProfilePicModal(false)
}, [])
const handleConnect = useCallback(() => { const handleConnect = useCallback(() => {
if (wallet) { if (wallet) {
handleWalletConnect(wallet) handleWalletConnect(wallet)
@ -265,41 +277,67 @@ export default function Account() {
<div className="flex flex-col pt-4 pb-6 md:flex-row md:items-end md:justify-between md:pb-4 md:pt-10"> <div className="flex flex-col pt-4 pb-6 md:flex-row md:items-end md:justify-between md:pb-4 md:pt-10">
{mangoAccount ? ( {mangoAccount ? (
<> <>
<div className="pb-3 md:pb-0"> <div className="flex items-center pb-3 md:pb-0">
<div className="mb-1 flex items-center"> <button
<h1 className={`mr-3`}> disabled={!!pubkey}
{mangoAccount?.name || t('account')} className={`relative mr-4 flex h-20 w-20 items-center justify-center rounded-full ${
</h1> loadingTransaction
{!pubkey ? ( ? 'animate-pulse bg-th-bkg-4'
<IconButton : 'bg-th-bkg-button'
className="h-7 w-7" }`}
onClick={() => setShowNameModal(true)} onClick={() => setShowProfilePicModal(true)}
>
{pfp?.isAvailable ? (
<img
alt=""
src={pfp.url}
className={`default-transition h-20 w-20 rounded-full hover:opacity-60 ${
loadingTransaction ? 'opacity-40' : ''
}`}
/>
) : (
<ProfileIcon className="h-12 w-12 text-th-fgd-3" />
)}
<div className="default-transition absolute bottom-0 top-0 left-0 right-0 flex h-full w-full items-center justify-center rounded-full bg-[rgba(0,0,0,0.6)] opacity-0 hover:opacity-100">
<PencilIcon className="h-5 w-5 text-th-fgd-1" />
</div>
</button>
<div>
<div className="mb-1 flex items-center">
<h1 className={`mr-3`}>
{mangoAccount?.name || t('account')}
</h1>
{!pubkey ? (
<IconButton
className="h-7 w-7"
onClick={() => setShowNameModal(true)}
>
<PencilIcon className="h-3.5 w-3.5" />
</IconButton>
) : null}
</div>
<div className="flex h-4 items-center">
<LinkButton
className="flex items-center text-th-fgd-4 no-underline"
onClick={() =>
handleCopyAddress(mangoAccount.publicKey.toString())
}
> >
<PencilIcon className="h-3.5 w-3.5" /> <span className="text-xxs font-normal sm:text-xs">
</IconButton> {mangoAccount.publicKey.toBase58()}
) : null} </span>
</div> <DuplicateIcon className="ml-1.5 h-4 w-4" />
<div className="flex h-4 items-center"> </LinkButton>
<LinkButton {isCopied ? (
className="flex items-center text-th-fgd-4 no-underline" <span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
onClick={() => Copied
handleCopyAddress(mangoAccount.publicKey.toString()) </span>
} ) : null}
> </div>
<span className="text-xxs font-normal sm:text-xs"> <div className="flex items-center text-xxs text-th-fgd-4">
{mangoAccount.publicKey.toBase58()} <ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
</span> {t('account-address-warning')}
<DuplicateIcon className="ml-1.5 h-4 w-4" /> </div>
</LinkButton>
{isCopied ? (
<span className="ml-2 rounded bg-th-bkg-3 px-1.5 py-0.5 text-xs">
Copied
</span>
) : null}
</div>
<div className="flex items-center text-xxs text-th-fgd-4">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4" />
{t('account-address-warning')}
</div> </div>
</div> </div>
{!pubkey ? ( {!pubkey ? (
@ -502,6 +540,12 @@ export default function Account() {
onClose={handleCloseDelegateModal} onClose={handleCloseDelegateModal}
/> />
) : null} ) : null}
{showProfilePicModal ? (
<NftProfilePicModal
isOpen={showProfilePicModal}
onClose={handleCloseProfilePicModal}
/>
) : null}
</div> </div>
) )
} }

View File

@ -11,7 +11,7 @@ import { useTranslation } from 'next-i18next'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common', 'profile'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -22,7 +22,7 @@ import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common', 'profile'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -31,6 +31,7 @@ export async function getStaticProps({ locale }) {
'tv-chart', 'tv-chart',
'alerts', 'alerts',
'share-modal', 'share-modal',
'profile',
])), ])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },

View File

@ -11,7 +11,7 @@ const TABS = ['perp', 'spot']
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common', 'profile'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -52,7 +52,11 @@ import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common', 'referrals'])), ...(await serverSideTranslations(locale, [
'common',
'referrals',
'profile',
])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -34,7 +34,11 @@ import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common', 'calculator'])), ...(await serverSideTranslations(locale, [
'common',
'calculator',
'profile',
])),
}, },
} }
} }

View File

@ -11,7 +11,11 @@ import TopBar from '../components/TopBar'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common', 'tv-chart'])), ...(await serverSideTranslations(locale, [
'common',
'tv-chart',
'profile',
])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -16,7 +16,7 @@ import { useTranslation } from 'next-i18next'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common'])), ...(await serverSideTranslations(locale, ['common', 'profile'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -13,7 +13,7 @@ import { useWallet } from '@solana/wallet-adapter-react'
export async function getStaticProps({ locale }) { export async function getStaticProps({ locale }) {
return { return {
props: { props: {
...(await serverSideTranslations(locale, ['common', 'swap'])), ...(await serverSideTranslations(locale, ['common', 'swap', 'profile'])),
// Will be passed to the page component as props // Will be passed to the page component as props
}, },
} }

View File

@ -83,7 +83,7 @@
"connect-wallet-tip-desc": "We'll show you around...", "connect-wallet-tip-desc": "We'll show you around...",
"connect-wallet-tip-title": "Connect your wallet", "connect-wallet-tip-title": "Connect your wallet",
"connected-to": "Connected to wallet ", "connected-to": "Connected to wallet ",
"copy-address": "Copy address", "copy-address": "Copy Address",
"country-not-allowed": "Country Not Allowed", "country-not-allowed": "Country Not Allowed",
"country-not-allowed-tooltip": "You are using an open-source frontend facilitated by the Mango DAO. As such, it restricts access to certain regions out of an abundance of caution, due to regulatory uncertainty.", "country-not-allowed-tooltip": "You are using an open-source frontend facilitated by the Mango DAO. As such, it restricts access to certain regions out of an abundance of caution, due to regulatory uncertainty.",
"create-account": "Create Account", "create-account": "Create Account",
@ -356,7 +356,7 @@
"short": "short", "short": "short",
"show-all": "Show all in Nav", "show-all": "Show all in Nav",
"show-less": "Show less", "show-less": "Show less",
"show-more": "Show more", "show-more": "Show More",
"show-tips": "Show Tips", "show-tips": "Show Tips",
"show-zero": "Show zero balances", "show-zero": "Show zero balances",
"side": "Side", "side": "Side",

View File

@ -0,0 +1,10 @@
{
"choose-profile": "Choose a Profile Pic",
"edit-profile-pic": "Edit Profile Pic",
"profile-pic-failure": "Failed to set profile pic",
"profile-pic-success": "Successfully set profile pic",
"profile-pic-remove-failure": "Failed to remove profile pic",
"profile-pic-remove-success": "Successfully removed profile pic",
"remove": "Remove",
"set-profile-pic": "Set Profile Pic"
}

View File

@ -0,0 +1,10 @@
{
"choose-profile": "Choose a Profile Pic",
"edit-profile-pic": "Edit Profile Pic",
"profile-pic-failure": "Failed to set profile pic",
"profile-pic-success": "Successfully set profile pic",
"profile-pic-remove-failure": "Failed to remove profile pic",
"profile-pic-remove-success": "Successfully removed profile pic",
"remove": "Remove",
"set-profile-pic": "Set Profile Pic"
}

View File

@ -0,0 +1,10 @@
{
"choose-profile": "Choose a Profile Pic",
"edit-profile-pic": "Edit Profile Pic",
"profile-pic-failure": "Failed to set profile pic",
"profile-pic-success": "Successfully set profile pic",
"profile-pic-remove-failure": "Failed to remove profile pic",
"profile-pic-remove-success": "Successfully removed profile pic",
"remove": "Remove",
"set-profile-pic": "Set Profile Pic"
}

View File

@ -0,0 +1,10 @@
{
"choose-profile": "Choose a Profile Pic",
"edit-profile-pic": "Edit Profile Pic",
"profile-pic-failure": "Failed to set profile pic",
"profile-pic-success": "Successfully set profile pic",
"profile-pic-remove-failure": "Failed to remove profile pic",
"profile-pic-remove-success": "Successfully removed profile pic",
"remove": "Remove",
"set-profile-pic": "Set Profile Pic"
}

View File

@ -40,6 +40,8 @@ import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp'
import { decodeBook } from '../hooks/useHydrateStore' import { decodeBook } from '../hooks/useHydrateStore'
import { IOrderLineAdapter } from '../public/charting_library/charting_library' import { IOrderLineAdapter } from '../public/charting_library/charting_library'
import { Wallet } from '@solana/wallet-adapter-react' import { Wallet } from '@solana/wallet-adapter-react'
import { getParsedNftAccountsByOwner } from '@nfteyez/sol-rayz'
import { getTokenAccountsByMint } from 'utils/tokens'
export const ENDPOINTS: EndpointInfo[] = [ export const ENDPOINTS: EndpointInfo[] = [
{ {
@ -130,6 +132,32 @@ export interface AlertRequest {
notifiAlertId: string | undefined notifiAlertId: string | undefined
} }
interface NFTFiles {
type: string
uri: string
}
interface NFTProperties {
category: string
files: NFTFiles[]
}
interface NFTData {
image: string
name: string
description: string
properties: NFTProperties
collection: {
family: string
name: string
}
}
interface NFTWithMint {
val: NFTData
mint: string
tokenAddress: string
}
export type MangoStore = { export type MangoStore = {
notificationIdCounter: number notificationIdCounter: number
notifications: Array<Notification> notifications: Array<Notification>
@ -197,6 +225,13 @@ export type MangoStore = {
wallet: { wallet: {
tokens: WalletToken[] | any[] tokens: WalletToken[] | any[]
pfp: ProfilePicture | undefined pfp: ProfilePicture | undefined
nfts: {
data: NFTWithMint[] | []
initialLoad: boolean
loading: boolean
accounts: any[]
loadingTransaction: boolean
}
} }
settings: { settings: {
uiLocked: boolean uiLocked: boolean
@ -211,6 +246,12 @@ export type MangoStore = {
actions: { actions: {
fetchWalletTokens: (wallet: Wallet) => void fetchWalletTokens: (wallet: Wallet) => void
fetchProfilePicture: (wallet: Wallet) => void fetchProfilePicture: (wallet: Wallet) => void
fetchNfts: (
connection: Connection,
walletPk: PublicKey | null,
offset?: number
) => void
fetchNftAccounts: (connection: Connection, walletPk: PublicKey) => void
fetchAllMangoAccounts: (wallet: Wallet) => Promise<void> fetchAllMangoAccounts: (wallet: Wallet) => Promise<void>
fetchMangoGroup: () => Promise<void> fetchMangoGroup: () => Promise<void>
fetchTradeHistory: () => void fetchTradeHistory: () => void
@ -341,6 +382,13 @@ const useMangoStore = create<
wallet: { wallet: {
tokens: [], tokens: [],
pfp: undefined, pfp: undefined,
nfts: {
data: [],
initialLoad: false,
loading: false,
accounts: [],
loadingTransaction: false,
},
}, },
settings: { settings: {
uiLocked: true, uiLocked: true,
@ -423,6 +471,82 @@ const useMangoStore = create<
console.log('Could not get profile picture', e) console.log('Could not get profile picture', e)
} }
}, },
async fetchNftAccounts(connection: Connection, walletPk: PublicKey) {
const set = get().set
try {
const nfts = await getParsedNftAccountsByOwner({
publicAddress: walletPk.toBase58(),
connection: connection,
})
const data = Object.keys(nfts)
.map((key) => nfts[key])
.filter((data) => data.primarySaleHappened)
set((state) => {
state.wallet.nfts.accounts = data
})
} catch (error) {
console.log(error)
}
},
async fetchNfts(
connection: Connection,
walletPk: PublicKey,
offset = 0
) {
const set = get().set
set((state) => {
state.wallet.nfts.loading = true
})
try {
const nftAccounts = get().wallet.nfts.accounts
const loadedNfts = get().wallet.nfts.data
const arr: NFTWithMint[] = []
if (loadedNfts.length < nftAccounts.length) {
for (let i = offset; i < offset + 9; i++) {
try {
const response = await fetch(nftAccounts[i].data.uri)
const val = await response.json()
const tokenAccounts = await getTokenAccountsByMint(
connection,
nftAccounts[i].mint
)
arr.push({
val,
mint: nftAccounts[i].mint,
tokenAddress: tokenAccounts
.find(
(x) =>
x.account.owner.toBase58() === walletPk.toBase58()
)!
.publicKey.toBase58(),
})
} catch (e) {
console.log(e)
}
}
}
if (loadedNfts.length === 0) {
set((state) => {
state.wallet.nfts.data = arr
state.wallet.nfts.initialLoad = true
state.wallet.nfts.loading = false
})
} else {
set((state) => {
state.wallet.nfts.data = [...loadedNfts, ...arr]
state.wallet.nfts.loading = false
})
}
} catch (error) {
notify({
title: 'Unable to fetch nfts',
description: '',
})
set((state) => {
state.wallet.nfts.loading = false
})
}
},
async fetchAllMangoAccounts(wallet) { async fetchAllMangoAccounts(wallet) {
const set = get().set const set = get().set
const mangoGroup = get().selectedMangoGroup.current const mangoGroup = get().selectedMangoGroup.current

View File

@ -1,5 +1,5 @@
import { TokenAccountLayout } from '@blockworks-foundation/mango-client' import { TokenAccountLayout } from '@blockworks-foundation/mango-client'
import { PublicKey } from '@solana/web3.js' import { PublicKey, Connection } from '@solana/web3.js'
export const TOKEN_PROGRAM_ID = new PublicKey( export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
@ -22,3 +22,28 @@ export function parseTokenAccountData(data: Buffer): {
amount, amount,
} }
} }
export async function getTokenAccountsByMint(
connection: Connection,
mint: string
): Promise<ProgramAccount<any>[]> {
const results = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, {
filters: [
{
dataSize: 165,
},
{
memcmp: {
offset: 0,
bytes: mint,
},
},
],
})
return results.map((r) => {
const publicKey = r.pubkey
const data = Buffer.from(r.account.data)
const account = parseTokenAccountData(data)
return { publicKey, account }
})
}

2278
yarn.lock

File diff suppressed because it is too large Load Diff