Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

How to get NFT token metadata by mint? #22

Closed
faridmovsumov opened this issue Jan 15, 2022 · 9 comments
Closed

How to get NFT token metadata by mint? #22

faridmovsumov opened this issue Jan 15, 2022 · 9 comments

Comments

@faridmovsumov
Copy link

Hello,

I am super happy to discover this repository and appreciate what you do here.
I checked previous issues and found past discussions regarding metadata, but unfortunately, I couldn't make it work.
Mint is 4EbYVzfS5ad5wYcAdoaxJM7HUeiQq9RnCeSm289CUcC6 (I also tried with 3k7M6KoYQ8kkpcWqceAZokLJHfiVfHGNtU7an5BGPh3C)
image

I am trying to get metadata with the following code

$METADATA_PUBKEY = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");

        /** @var PublicKey $pda */
        list($pda, $bump) = PublicKey::findProgramAddress([
            'metadata',
            $METADATA_PUBKEY,
            (new PublicKey('4EbYVzfS5ad5wYcAdoaxJM7HUeiQq9RnCeSm289CUcC6')),
        ], $METADATA_PUBKEY);
Metadata::fromBuffer($pda->toBuffer()->toArray());

But it is throwing the following error
Tighten\SolanaPhpSdk\Borsh\BorshException: Expected buffer length 32 isn't within bounds in file /Users/farid/projects/web3/vendor/tightenco/solana-php-sdk/src/Borsh/BinaryReader.php on line 177

I also checked this token on Solscan and it looks correct. Not sure if I am using the correct id.
https://solscan.io/token/4EbYVzfS5ad5wYcAdoaxJM7HUeiQq9RnCeSm289CUcC6#metadata

image

Could you please help me to understand what is wrong here?

@faridmovsumov
Copy link
Author

faridmovsumov commented Jan 15, 2022

Finally figured out how to get it. Code should look something like this.

        $mint = "4EbYVzfS5ad5wYcAdoaxJM7HUeiQq9RnCeSm289CUcC6";
        $METADATA_PUBKEY = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');

        /** @var PublicKey $pda */
        list($pda, $bump) = PublicKey::findProgramAddress([
            'metadata',
            $METADATA_PUBKEY,
            (new PublicKey($mint)),
        ], $METADATA_PUBKEY);

        $metadataAddress = $pda->getPublicKey()->toBase58();

        $sdk = new Connection(new SolanaRpcClient(SolanaRpcClient::MAINNET_ENDPOINT));
        $accountInfo = $sdk->getAccountInfo($metadataAddress);
        $base64Data = $accountInfo['data'][0];
        $buffer = Buffer::from(base64_decode($base64Data));
        $metadata = Metadata::fromBuffer($buffer->toArray());
        dd($metadata);

And output
image

@faridmovsumov
Copy link
Author

I refactored the code a bit, now it is possible to fetch all NFT's by the given wallet address. (Laravel)

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Tighten\SolanaPhpSdk\Accounts\Metadata;
use Tighten\SolanaPhpSdk\Connection;
use Tighten\SolanaPhpSdk\Programs\MetaplexProgram;
use Tighten\SolanaPhpSdk\Programs\SplTokenProgram;
use Tighten\SolanaPhpSdk\PublicKey;
use Tighten\SolanaPhpSdk\SolanaRpcClient;
use Tighten\SolanaPhpSdk\Util\Buffer;

class NftController extends Controller
{
    /*
     * @see https://docs.metaplex.com/architecture/deep_dive/overview
     */
    public function getNfts(Request $request)
    {
        $walletAddress = $request->get('walletAddress');
        $solanaRpcClient = new SolanaRpcClient(SolanaRpcClient::MAINNET_ENDPOINT);
        $splTokenProgram = new SplTokenProgram($solanaRpcClient);
        $tokenAccounts = $splTokenProgram->getTokenAccountsByOwner($walletAddress);

        if (empty($tokenAccounts['value'])) {
            new Response("Nft not found", 404);
        }

        $tokens = $tokenAccounts['value'];
        $sdk = new Connection($solanaRpcClient);
        $nftMetaDatas = [];

        foreach ($tokens as $token) {
            try {
                if (empty($token['account']['data']['parsed']['info']['mint'])) {
                    continue;
                }

                $mint = $token['account']['data']['parsed']['info']['mint'];
                $metaplexProgramPublicKey = new PublicKey(MetaplexProgram::METAPLEX_PROGRAM_ID);

                /** @var PublicKey $pda */
                list($pda, $bump) = PublicKey::findProgramAddress([
                    'metadata',
                    $metaplexProgramPublicKey,
                    (new PublicKey($mint)),
                ], $metaplexProgramPublicKey);

                $metadataAddress = $pda->getPublicKey()->toBase58();
                $accountInfo = $sdk->getAccountInfo($metadataAddress);

                if (empty($accountInfo['owner']) || $accountInfo['owner'] !== MetaplexProgram::METAPLEX_PROGRAM_ID) {
                    //This is not nft
                    continue;
                }

                if (empty($accountInfo['data'][0]) || empty($accountInfo['data'][1]) || $accountInfo['data'][1] !== 'base64') {
                    //Not the format we are trying to convert
                    continue;
                }

                $base64Data = $accountInfo['data'][0];
                $buffer = Buffer::from(base64_decode($base64Data));
                $metadata = Metadata::fromBuffer($buffer->toArray());
                $nftMetaDatas[] = $metadata;
            } catch (\Throwable $throwable) {
                Log::error("Error while fetching NFT for wallet address $walletAddress Reason: " . $throwable->getMessage());
                continue;
            }
        }

        if (empty($nftMetaDatas)) {
            new Response("Nft not found", 404);
        }

        return new Response($nftMetaDatas);
    }
}

@neverything
Copy link
Contributor

Cool and thanks for the example! I'm doing it in a similar fashion but with multiple jobs running the background.

One thing that I might add, is an additional check for NFTs as your code currently will return USDT & USDC token accounts as NFTs with metadata.

While it's not perfect, I'm running additional checks mentioned here https://gist.github.com/creativedrewy/9bce794ff278aae23b64e6dc8f10e906#step-2-locate-nft-accounts

@faridmovsumov
Copy link
Author

@neverything thanks for sharing, seems like the biggest challenge is to not hit rate limits. Do you have any example of how do you handle it?

@neverything
Copy link
Contributor

neverything commented Jan 17, 2022

Using rate limiter middleware on the jobs and some custom delays.

Update: But I must admit, I haven't fully figured out how to make it run perfectly and smooth. Currently running roughly 4.5k jobs per hour in the background on Redis/Horizon.

Now that the Borsh implementation with the Metadata stuff is working in this package, I'm trying to implement a more relaxed approach, especially when it comes to updating token accounts and NFT metadata. But I'm still learning (not a pro at any of this yet), hence my thread on twitter https://twitter.com/neverything/status/1478751444329238533

@mattstauffer
Copy link
Contributor

Thanks for sharing your code! Hopefully it will help others :)

@faridmovsumov
Copy link
Author

faridmovsumov commented Jan 23, 2022

Found important detail want to also share here.
getTokenAccountsByOwner method retrieves old NFTs which has been sold.
You should check if the amount is not empty (0 is considered empty in PHP)

if(empty($token['account']['data']['parsed']['info']['tokenAmount']['amount'])){
  Log::info('This NFT does not belongs to account anymore, skip');
  continue;
}

@neverything
Copy link
Contributor

@faridmovsumov thanks for sharing. I was wondering if there was an easy way to check this without going through all the transactions from a wallet or a NFT token account.

@faridmovsumov
Copy link
Author

Unfortunately couldn't find anything easier, there is no option to filter out this in API as far as I understood.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants