Solana web3.jsでNFTミント後にNFT送信すると「TokenAccountNotFoundError」エラー

前提

以下の処理を実行するプログラムを作成(いずれもweb3.jsを利用)。

  1. Arweaveに画像をアップロード
  2. ArweaveにMetadataをアップロード(Metadataの中に上記画像のURLを記述)
  3. SolanaでNFTをMint(Metaplexのactions.mintNFTを利用)
  4. Solanaで上記NFTを誰かに送信

現象

「4. Solanaで上記NFTを誰かに送信」の処理を実行すると以下「TokenAccountNotFoundError」のエラーが出力。

/Users/user/Documents/Programming/Blockchain/solana-anchor-react-minimal-example/js/mint-and-transfer-nft/node_modules/@solana/spl-token/lib/cjs/state/account.js:5
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
                                                         ^
TokenAccountNotFoundError
    at /Users/user/Documents/Programming/Blockchain/solana-anchor-react-minimal-example/js/mint-and-transfer-nft/node_modules/@solana/spl-token/src/state/account.ts:93:22
    at Generator.next (<anonymous>)
    at fulfilled (/Users/user/Documents/Programming/Blockchain/solana-anchor-react-minimal-example/js/mint-and-transfer-nft/node_modules/@solana/spl-token/lib/cjs/state/account.js:5:58)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)

原因

結論から言うと、処理が早すぎてトークンアカウント取得が間に合わなかったようだった。
Solana上でトランザクション処理される前に、次の処理に進めてしまっていた。

【補足】
NFTを送信するときは、送信者・受信者でそれぞれトークンアカウント(いわゆる子アカウントで、Phantomで表示されるアドレスは親アカウント)を取得し、それを指定する必要がある。

トークンアカウントは以下のようにNFTのミントアドレスや取得したアドレスを指定して取得できる。

  const toTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    fromWallet,
    mint,
    toWallet.publicKey
  );

エラーが「TokenAccountNotFoundError」だったため、凡ミスで指定する値が間違っているように思えたが、実際はプログラム側の処理速度・タイミングの問題で、値が取得できていないだけだった。

対応

各処理にsleepを入れて解決。

  const _sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

  // --- Solana ---
  console.log('\n--- Mint NFT on Solana ---')
  const mintNftAddress = await mintNft(connection, keypair, arweave, uploadMetadataTx);
  await _sleep(1000); // 1000 == 1 sec

  console.log('\n--- Transfer NFT to Someone ---')
  const transferNftTx = await transferNft(connection, keypair, mintNftAddress);

補足

厳密には、トークンアカウントが作成されたかどうかをチェックしたほうが汎用的にエラーを防げるため、実際には以下のような実装になる。

const _sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Mint
const mintNftAddress = await mintNft(connection, keypair, arweave, uploadMetadataTx);

// Chack exist Token Account
let tokenAccountInfo;

for (let i = 0; i < 9; i++) {
    try {
        // Get token account info.
        // Use Try&Catch statement.
        tokenAccountInfo = await getAccount(connection, tokenAccount);
    } catch {
        console.log('Token account has not created.');
    }

    if (tokenAccountInfo) { break } // Break if get info.

    // Wait for tx confirmation
    console.log('Waiting for confirmed...');
    sleep(3);
}

// Transfer
const transferNftTx = await transferNft(connection, keypair, mintNftAddress);

参考

今回使用したプログラム