import { PublicKey, Connection } from '@solana/web3.js' import { TokenInstructions } from '@project-serum/serum' import { getAssociatedTokenAddress, toUiDecimals, } from '@blockworks-foundation/mango-v4' import { Metaplex, Nft, Sft, SftWithToken, NftWithToken, Metadata, JsonMetadata, } from '@metaplex-foundation/js' import { safeRace } from '@solana/promises' import { PhishingController, PhishingControllerActions, } from '@metamask/phishing-controller' import { ControllerMessenger } from '@metamask/base-controller' function getRestrictedMessenger() { const controllerMessenger = new ControllerMessenger< PhishingControllerActions, never >() const messenger = controllerMessenger.getRestricted({ name: 'PhishingController', allowedActions: [], allowedEvents: [], }) return messenger } const phishingControllerMessenger = getRestrictedMessenger() const phishingController = new PhishingController({ messenger: phishingControllerMessenger, }) export class TokenAccount { publicKey!: PublicKey mint!: PublicKey owner!: PublicKey amount!: number decimals!: number uiAmount: number constructor( publicKey: PublicKey, decoded: { mint: PublicKey owner: PublicKey amount: number decimals: number uiAmount: number }, ) { this.publicKey = publicKey this.uiAmount = 0 Object.assign(this, decoded) } } type RawNft = Nft | Sft | SftWithToken | NftWithToken type NftWithATA = RawNft & { owner: null | PublicKey tokenAccountAddress: null | PublicKey } function exists(item: T | null | undefined): item is T { return !!item } export async function getTokenAccountsByOwnerWithWrappedSol( connection: Connection, owner: PublicKey, ): Promise { const solReq = connection.getAccountInfo(owner) const tokenReq = connection.getParsedTokenAccountsByOwner(owner, { programId: TokenInstructions.TOKEN_PROGRAM_ID, }) // fetch data const [solResp, tokenResp] = await Promise.all([solReq, tokenReq]) // parse token accounts const tokenAccounts = tokenResp.value.map((t) => { return { publicKey: t.pubkey, mint: t.account.data.parsed.info.mint, owner: t.account.data.parsed.info.owner, amount: t.account.data.parsed.info.tokenAmount.amount, uiAmount: t.account.data.parsed.info.tokenAmount.uiAmount, decimals: t.account.data.parsed.info.tokenAmount.decimals, } }) // create fake wrapped sol account to reflect sol balances in user's wallet const lamports = solResp?.lamports || 0 const solAccount = new TokenAccount(owner, { mint: TokenInstructions.WRAPPED_SOL_MINT, owner, amount: lamports, uiAmount: toUiDecimals(lamports, 9), decimals: 9, }) // prepend SOL account to beginning of list return [solAccount].concat(tokenAccounts) } const enhanceNFT = (nft: NftWithATA) => { return { image: nft.json?.image || '', name: nft.json?.name || '', address: nft.metadataAddress.toBase58(), collectionAddress: nft.collection?.address.toBase58(), mint: nft.mint.address.toBase58(), tokenAccount: nft.tokenAccountAddress?.toBase58() || '', json: nft.json, } } function loadNft( nft: Metadata> | Nft | Sft, connection: Connection, ) { const metaplex = new Metaplex(connection) return safeRace([ metaplex .nfts() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @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) await phishingController.maybeUpdateState() const rawNfts = ( await metaplex.nfts().findAllByOwner({ owner, }) ).filter((x) => { const isUrlSafe = !phishingController.test(x.uri).result return isUrlSafe }) 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) => { if (symbol.toLowerCase().includes('portal')) { const truncSymbol = symbol.split(' ')[0].toUpperCase() return truncSymbol === 'WBTC' ? 'wBTC' : truncSymbol } return symbol === 'MSOL' ? 'mSOL' : symbol }