load nfts from metaplex

This commit is contained in:
saml33 2023-04-14 21:48:09 +10:00
parent 442af25a09
commit 840084433c
9 changed files with 1300 additions and 103 deletions

View File

@ -9,7 +9,7 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
disabled?: boolean
prefixClassname?: string
wrapperClassName?: string
error?: boolean
hasError?: boolean
prefix?: string
prefixClassName?: string
suffix?: string
@ -22,7 +22,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
onChange,
maxLength,
className,
error,
hasError,
wrapperClassName = 'w-full',
disabled,
prefix,
@ -40,10 +40,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
</div>
) : null}
<input
{...props}
className={`${className} default-transition h-12 w-full flex-1 rounded-md border bg-th-input-bkg px-3 text-base
text-th-fgd-1 ${
error ? 'border-th-down' : 'border-th-input-border'
hasError ? 'border-th-down' : 'border-th-input-border'
} focus:outline-none
md:hover:border-th-input-border-hover
${

View File

@ -613,7 +613,7 @@ const ListToken = () => {
<div>
<Label text={t('oracle')} />
<Input
error={formErrors.oraclePk !== undefined}
hasError={formErrors.oraclePk !== undefined}
type="text"
value={advForm.oraclePk}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -632,7 +632,7 @@ const ListToken = () => {
<div>
<Label text={t('token-index')} />
<Input
error={formErrors.tokenIndex !== undefined}
hasError={formErrors.tokenIndex !== undefined}
type="number"
value={advForm.tokenIndex.toString()}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -651,7 +651,7 @@ const ListToken = () => {
<div>
<Label text={t('openbook-market-external')} />
<Input
error={
hasError={
formErrors.openBookMarketExternalPk !==
undefined
}
@ -676,7 +676,7 @@ const ListToken = () => {
<div>
<Label text={t('base-bank')} />
<Input
error={formErrors.baseBankPk !== undefined}
hasError={formErrors.baseBankPk !== undefined}
type="text"
value={advForm.baseBankPk.toString()}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -695,7 +695,7 @@ const ListToken = () => {
<div>
<Label text={t('quote-bank')} />
<Input
error={formErrors.quoteBankPk !== undefined}
hasError={formErrors.quoteBankPk !== undefined}
type="text"
value={advForm.quoteBankPk.toString()}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -714,7 +714,9 @@ const ListToken = () => {
<div>
<Label text={t('openbook-program')} />
<Input
error={formErrors.openBookProgram !== undefined}
hasError={
formErrors.openBookProgram !== undefined
}
type="text"
value={advForm.openBookProgram.toString()}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -736,7 +738,7 @@ const ListToken = () => {
<div>
<Label text={t('market-name')} />
<Input
error={formErrors.marketName !== undefined}
hasError={formErrors.marketName !== undefined}
type="text"
value={advForm.marketName.toString()}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -755,7 +757,7 @@ const ListToken = () => {
<div>
<Label text={t('proposal-title')} />
<Input
error={formErrors.proposalTitle !== undefined}
hasError={formErrors.proposalTitle !== undefined}
type="text"
value={advForm.proposalTitle.toString()}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -777,7 +779,7 @@ const ListToken = () => {
<div>
<Label text={t('proposal-des')} />
<Input
error={
hasError={
formErrors.proposalDescription !== undefined
}
type="text"

View File

@ -8,14 +8,18 @@ import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { notify } from 'utils/notifications'
import { MANGO_DATA_API_URL } from 'utils/constants'
const ImgWithLoader = (props: { className: string; src: string }) => {
const ImgWithLoader = (props: {
className: string
src: string
alt: string
}) => {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative">
{isLoading && (
<PhotoIcon className="absolute left-1/2 top-1/2 z-10 h-1/4 w-1/4 -translate-x-1/2 -translate-y-1/2 animate-pulse text-th-fgd-4" />
)}
<img {...props} onLoad={() => setIsLoading(false)} alt="" />
<img {...props} onLoad={() => setIsLoading(false)} alt={props.alt} />
</div>
)
}
@ -158,17 +162,18 @@ const EditNftProfilePic = ({ onClose }: { onClose: () => void }) => {
{nfts.length > 0 ? (
<div className="flex flex-col items-center">
<div className="mb-4 grid w-full grid-flow-row grid-cols-3 gap-3">
{nfts.map((n) => (
{nfts.map((n, i) => (
<button
className={`default-transition col-span-1 flex items-center justify-center rounded-md border bg-th-bkg-2 py-3 sm:py-4 md:hover:bg-th-bkg-3 ${
selectedProfile === n.image
? 'border-th-active'
: 'border-th-bkg-3'
}`}
key={n.image}
key={n.image + i}
onClick={() => setSelectedProfile(n.image)}
>
<ImgWithLoader
alt={n.name}
className="h-16 w-16 flex-shrink-0 rounded-full sm:h-20 sm:w-20"
src={n.image}
/>
@ -178,7 +183,7 @@ const EditNftProfilePic = ({ onClose }: { onClose: () => void }) => {
</div>
) : nftsLoading ? (
<div className="mb-4 grid w-full grid-flow-row grid-cols-3 gap-4">
{[...Array(9)].map((i) => (
{[...Array(9)].map((x, i) => (
<div
className="col-span-1 h-[90px] animate-pulse rounded-md bg-th-bkg-3 sm:h-28"
key={i}

View File

@ -159,7 +159,7 @@ const EditProfileForm = ({
<Label text={t('profile:profile-name')} />
<Input
type="text"
error={!!inputError.length}
hasError={!!inputError.length}
value={profileName}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
onChangeNameInput(e.target.value)

View File

@ -21,6 +21,7 @@
"@blockworks-foundation/mango-v4": "^0.9.15",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
"@metaplex-foundation/js": "0.18.3",
"@project-serum/anchor": "0.25.0",
"@pythnetwork/client": "2.15.0",
"@solana/spl-governance": "0.3.25",

View File

@ -23,7 +23,7 @@ import {
import EmptyWallet from '../utils/wallet'
import { Notification, notify } from '../utils/notifications'
import {
fetchNftsFromHolaplexIndexer,
getNFTsByOwner,
getTokenAccountsByOwnerWithWrappedSol,
TokenAccount,
} from '../utils/tokens'
@ -664,9 +664,9 @@ const mangoStore = create<MangoStore>()(
state.wallet.nfts.loading = true
})
try {
const data = await fetchNftsFromHolaplexIndexer(ownerPk)
const nfts = await getNFTsByOwner(ownerPk, connection)
set((state) => {
state.wallet.nfts.data = data.nfts
state.wallet.nfts.data = nfts
state.wallet.nfts.loading = false
})
} catch (error) {

View File

@ -246,6 +246,7 @@ export interface SwapHistoryItem {
export interface NFT {
address: string
image: string
name: string
}
export interface PerpStatsItem {

View File

@ -1,6 +1,18 @@
import { PublicKey, Connection } from '@solana/web3.js'
import { TokenInstructions } from '@project-serum/serum'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import {
getAssociatedTokenAddress,
toUiDecimals,
} from '@blockworks-foundation/mango-v4'
import {
Metaplex,
Nft,
Sft,
SftWithToken,
NftWithToken,
Metadata,
JsonMetadata,
} from '@metaplex-foundation/js'
export class TokenAccount {
publicKey!: PublicKey
@ -26,6 +38,16 @@ export class TokenAccount {
}
}
type RawNft = Nft | Sft | SftWithToken | NftWithToken
type NftWithATA = RawNft & {
owner: null | PublicKey
tokenAccountAddress: null | PublicKey
}
function exists<T>(item: T | null | undefined): item is T {
return !!item
}
export async function getTokenAccountsByOwnerWithWrappedSol(
connection: Connection,
owner: PublicKey
@ -63,43 +85,59 @@ export async function getTokenAccountsByOwnerWithWrappedSol(
return [solAccount].concat(tokenAccounts)
}
export const fetchNftsFromHolaplexIndexer = async (owner: PublicKey) => {
const result = await fetch('https://graph.holaplex.com/v1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
query nfts($owners: [PublicKey!]) {
nfts(
owners: $owners,
limit: 10000, offset: 0) {
name
mintAddress
address
image
updateAuthorityAddress
collection {
creators {
verified
address
}
mintAddress
}
const enhanceNFT = (nft: NftWithATA) => {
return {
image: nft.json?.image || '',
name: nft.json?.name || '',
address: nft.metadataAddress.toBase58(),
}
}
}
function loadNft(
nft: Metadata<JsonMetadata<string>> | Nft | Sft,
connection: Connection
) {
const metaplex = new Metaplex(connection)
}
`,
variables: {
owners: [owner.toBase58()],
},
}),
return Promise.race([
metaplex
.nfts()
// @ts-ignore
.load({ metadata: nft })
.catch((e) => {
console.error(e)
return null
}),
])
}
export async function getNFTsByOwner(owner: PublicKey, connection: Connection) {
const metaplex = new Metaplex(connection)
const rawNfts = await metaplex.nfts().findAllByOwner({
owner,
})
const body = await result.json()
return body.data
const nfts = await Promise.all(
rawNfts.map((nft) => loadNft(nft, connection))
).then((nfts) =>
Promise.all(
nfts.filter(exists).map(async (nft) => ({
...nft,
owner,
tokenAccountAddress: await getAssociatedTokenAddress(
nft.mint.address,
owner,
true
).catch((e) => {
console.error(e)
return null
}),
}))
)
)
return nfts.map(enhanceNFT)
}
export const formatTokenSymbol = (symbol: string) =>

1245
yarn.lock

File diff suppressed because it is too large Load Diff