verze-app / solana-php-sdk

Simple PHP SDK for Solana JSON RPC endpoints
MIT License
88 stars 45 forks source link

How to get NFT token metadata by mint? #22

Closed faridmovsumov closed 2 years ago

faridmovsumov commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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 commented 2 years ago

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

faridmovsumov commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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