load nfts from metaplex
This commit is contained in:
parent
442af25a09
commit
840084433c
|
@ -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
|
||||
${
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -246,6 +246,7 @@ export interface SwapHistoryItem {
|
|||
export interface NFT {
|
||||
address: string
|
||||
image: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface PerpStatsItem {
|
||||
|
|
106
utils/tokens.ts
106
utils/tokens.ts
|
@ -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) =>
|
||||
|
|
Loading…
Reference in New Issue