Merge pull request #123 from blockworks-foundation/metaplex-load-nfts

Metaplex load nfts
This commit is contained in:
tlrsssss 2023-04-16 22:19:45 -04:00 committed by GitHub
commit d228b1f467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1320 additions and 145 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

@ -7,7 +7,7 @@ import {
getVoteRecordAddress,
} from '@solana/spl-governance'
import { VoteCountdown } from './VoteCountdown'
import { MintInfo } from '@solana/spl-token'
import { RawMint } from '@solana/spl-token'
import VoteResults from './VoteResult'
import QuorumProgress from './VoteProgress'
import GovernanceStore from '@store/governanceStore'
@ -40,7 +40,7 @@ const ProposalCard = ({
mangoMint,
}: {
proposal: ProgramAccount<Proposal>
mangoMint: MintInfo
mangoMint: RawMint
}) => {
const { t } = useTranslation('governance')
const connection = mangoStore((s) => s.connection)

View File

@ -3,7 +3,7 @@ import GovernanceStore from '@store/governanceStore'
import mangoStore from '@store/mangoStore'
import { useEffect, useState } from 'react'
import { isInCoolOffTime } from 'utils/governance/proposals'
import { MintInfo } from '@solana/spl-token'
import { RawMint } from '@solana/spl-token'
import { MANGO_MINT } from 'utils/constants'
import { PublicKey } from '@solana/web3.js'
import dynamic from 'next/dynamic'
@ -27,7 +27,7 @@ const Vote = () => {
const loadingVoter = GovernanceStore((s) => s.loadingVoter)
const loadingRealm = GovernanceStore((s) => s.loadingRealm)
const [mangoMint, setMangoMint] = useState<MintInfo | null>(null)
const [mangoMint, setMangoMint] = useState<RawMint | null>(null)
const [votingProposals, setVotingProposals] = useState<
ProgramAccount<Proposal>[]
>([])

View File

@ -4,7 +4,7 @@ import {
InformationCircleIcon,
} from '@heroicons/react/20/solid'
import { Governance, ProgramAccount, Proposal } from '@solana/spl-governance'
import { MintInfo } from '@solana/spl-token'
import { RawMint } from '@solana/spl-token'
import GovernanceStore from '@store/governanceStore'
import { useTranslation } from 'next-i18next'
import { getMintMaxVoteWeight } from 'utils/governance/proposals'
@ -13,7 +13,7 @@ import { fmtTokenAmount } from 'utils/governance/tools'
type Props = {
governance: ProgramAccount<Governance>
proposal: ProgramAccount<Proposal>
communityMint: MintInfo
communityMint: RawMint
}
const QuorumProgress = ({ governance, proposal, communityMint }: Props) => {

View File

@ -1,12 +1,12 @@
import { Proposal } from '@solana/spl-governance'
import VoteResultsBar from './VoteResultBar'
import { fmtTokenAmount } from 'utils/governance/tools'
import { MintInfo } from '@solana/spl-token'
import { RawMint } from '@solana/spl-token'
import { useTranslation } from 'next-i18next'
type VoteResultsProps = {
proposal: Proposal
communityMint: MintInfo
communityMint: RawMint
}
const VoteResults = ({ proposal, communityMint }: VoteResultsProps) => {

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

@ -138,17 +138,6 @@ const ConnectedMenu = () => {
</button>
</Menu.Item>
) : null}
{/* <Menu.Item>
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal hover:cursor-pointer hover:text-th-active focus:outline-none"
onClick={() => setShowProfileImageModal(true)}
>
<ProfileIcon className="h-4 w-4" />
<div className="pl-2 text-left">
{t('edit-profile-image')}
</div>
</button>
</Menu.Item> */}
<Menu.Item>
<button
className="default-transition flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none md:hover:cursor-pointer md:hover:text-th-fgd-1"

View File

@ -21,9 +21,11 @@
"@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",
"@solana/spl-token": "0.3.7",
"@solana/wallet-adapter-base": "0.9.20",
"@solana/wallet-adapter-react": "0.15.32",
"@solana/wallet-adapter-wallets": "0.19.11",
@ -102,6 +104,8 @@
"@solana/wallet-adapter-wallets>@solana/wallet-adapter-torus>@toruslabs/solana-embed>@toruslabs/base-controllers>@toruslabs/broadcast-channel>@toruslabs/eccrypto>secp256k1": true,
"@solana/wallet-adapter-wallets>@solana/wallet-adapter-torus>@toruslabs/solana-embed>@toruslabs/base-controllers>ethereumjs-util>ethereum-cryptography>secp256k1": true,
"@solana/wallet-adapter-wallets>@solana/wallet-adapter-torus>@toruslabs/solana-embed>@toruslabs/openlogin-jrpc>@toruslabs/openlogin-utils>keccak": true,
"@metaplex-foundation/js>@bundlr-network/client>arbundles>secp256k1": true,
"@metaplex-foundation/js>@bundlr-network/client>arbundles>keccak": true,
"@solana/web3.js>bigint-buffer": false,
"@solana/web3.js>rpc-websockets>bufferutil": true,
"@solana/web3.js>rpc-websockets>utf-8-validate": true,

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,5 +1,5 @@
import { BN } from '@coral-xyz/anchor'
import { MintInfo } from '@solana/spl-token'
import { Mint } from '@solana/spl-token'
import { PublicKey } from '@solana/web3.js'
import { VsrClient } from '../voteStakeRegistryClient'
@ -55,7 +55,7 @@ export interface Deposit {
}
export interface DepositWithMintAccount extends Deposit {
mint: TokenProgramAccount<MintInfo>
mint: TokenProgramAccount<Mint>
index: number
available: BN
vestingRate: BN | null

View File

@ -1,4 +1,3 @@
import { MintInfo } from '@solana/spl-token'
import { BN, EventParser } from '@coral-xyz/anchor'
import { Connection, PublicKey, Transaction } from '@solana/web3.js'
import {
@ -17,6 +16,7 @@ import {
tryGetRegistrar,
tryGetVoter,
} from '../accounts/vsrAccounts'
import { RawMint } from '@solana/spl-token'
type Event = {
depositEntryIndex: number
@ -67,7 +67,7 @@ export const getDeposits = async ({
])
const mintCfgs = existingRegistrar?.votingMints || []
const mints: { [key: string]: TokenProgramAccount<MintInfo> | undefined } = {}
const mints: { [key: string]: TokenProgramAccount<RawMint> | undefined } = {}
let votingPower = new BN(0)
let votingPowerFromDeposits = new BN(0)
let deposits: DepositWithMintAccount[] = []

View File

@ -8,7 +8,7 @@ import {
} from '@solana/spl-governance'
import BigNumber from 'bignumber.js'
import dayjs from 'dayjs'
import { MintInfo } from '@solana/spl-token'
import { RawMint } from '@solana/spl-token'
export const isInCoolOffTime = (
proposal: Proposal | undefined,
@ -38,7 +38,7 @@ export const isInCoolOffTime = (
/** Returns max VoteWeight for given mint and max source */
export function getMintMaxVoteWeight(
mint: MintInfo,
mint: RawMint,
maxVoteWeightSource: MintMaxVoteWeightSource
) {
if (maxVoteWeightSource.type === MintMaxVoteWeightSourceType.SupplyFraction) {

View File

@ -7,7 +7,7 @@ import {
} from '@solana/spl-governance'
import { Connection, PublicKey } from '@solana/web3.js'
import { TokenProgramAccount } from './accounts/vsrAccounts'
import { u64, MintLayout, MintInfo } from '@solana/spl-token'
import { MintLayout, RawMint } from '@solana/spl-token'
import BN from 'bn.js'
export async function fetchRealm({
@ -57,7 +57,7 @@ export function arrayToRecord<T>(
export async function tryGetMint(
connection: Connection,
publicKey: PublicKey
): Promise<TokenProgramAccount<MintInfo> | undefined> {
): Promise<TokenProgramAccount<RawMint> | undefined> {
try {
const result = await connection.getAccountInfo(publicKey)
const data = Buffer.from(result!.data)
@ -75,22 +75,8 @@ export async function tryGetMint(
}
}
export function parseMintAccountData(data: Buffer): MintInfo {
export function parseMintAccountData(data: Buffer): RawMint {
const mintInfo = MintLayout.decode(data)
if (mintInfo.mintAuthorityOption === 0) {
mintInfo.mintAuthority = null
} else {
mintInfo.mintAuthority = new PublicKey(mintInfo.mintAuthority)
}
mintInfo.supply = u64.fromBuffer(mintInfo.supply)
mintInfo.isInitialized = mintInfo.isInitialized != 0
if (mintInfo.freezeAuthorityOption === 0) {
mintInfo.freezeAuthority = null
} else {
mintInfo.freezeAuthority = new PublicKey(mintInfo.freezeAuthority)
}
return mintInfo
}

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