17-Thumbnail_Image.png

thirdwebのgetOwnedTokenIdsがslowな問題を解消する

thirdweb の getOwnedTokenIds のレスポンスが遅く、利用に耐えないという問題があります。トークン数が少ない場合は問題が顕在化しませんが、トークン数が多い場合この点難しいところです。本記事ではそれに対しての対策を案内します。
公開日2023.01.12
更新日2023.01.12

この記事について

この記事は thirdweb の sdk の NFT 取得メソッドである getOwnedTokenIds が slow な問題に対しての解決策を提示します。
ただし対応できるのは ERC721 に準拠したもののみとなっています。 thirdweb のコントラクト名でいうと NFTDrop ならびに SignatureDrop この 2 点に対して対応可能な方法です。

QA

Q.課題

thirdweb の getOwnedTokenIds のレスポンスが遅く、利用に耐えない。 特にトークン数が多い時地獄。リクエストが遅いだけでなく、そもそもメモリから溢れて動かなくなっちゃった。

A.結論

thirdweb sdk ではなく、ethers.js で実装することで解決します。 下記記事にまとめていますのでご確認ください。
詳細と解説は上記記事に譲りますが、解決策のコードは下記となります。
(thirdweb ユーザーはプロジェクトセットアップの際に、ethers.js をインストールしているはずなので、追加でライブラリをインストールせずにこの snippet で事足りるはずです!)
import { ethers } from "ethers";

const getCollection = async (holderAddress: string): Promise<number[]> => {
  // アドレスが同値かどうかを判定する関数
  const isAddressesEqual = (address1: string, address2: string) => {
    return address1.toLowerCase() === address2.toLowerCase();
  };

  // 設定値: ABI(Transferイベントのみ抽出)
  const sampleAbi = [
    {
      anonymous: false,
      inputs: [
        {
          indexed: true,
          internalType: "address",
          name: "from",
          type: "address",
        },
        {
          indexed: true,
          internalType: "address",
          name: "to",
          type: "address",
        },
        {
          indexed: true,
          internalType: "uint256",
          name: "tokenId",
          type: "uint256",
        },
      ],
      name: "Transfer",
      type: "event",
    },
  ];

  // ① Provider インスタンスを作成する。
  const provider = new ethers.providers.JsonRpcProvider(
    process.env.RPC_URL as string,
    process.env.NETWORK as string
  );

  // ②  Contract インスタンスを作成する。
  const token = new ethers.Contract(
    process.env.NFT_CONTRACT_ADDRESS as string,
    sampleAbi,
    provider
  );

  // ③ 調べたいホルダーのウォレットの過去のtokenIdの送信イベントログすべて取得する。
  const sentLogs = await token.queryFilter(
    token.filters.Transfer(holderAddress, null)
  );

  // ④ 調べたいホルダーのウォレットの過去のtokenIdの受信イベントログすべて取得する。
  const receivedLogs = await token.queryFilter(
    token.filters.Transfer(null, holderAddress)
  );

  // ⑤ ③と④のログを結合し、EventLogを時間が古いものから順に時系列で並べる。
  const logs = sentLogs
    .concat(receivedLogs)
    .sort(
      (a, b) =>
        a.blockNumber - b.blockNumber || a.transactionIndex - b.transactionIndex
    );

  // ⑥ ⑤のログを操作して、調べたいウォレットが最終的に持っている最新のtokenIdを取得する
  const owned = new Set<number>();

  for (const log of logs) {
    if (log.args) {
      const { from, to, tokenId } = log.args;

      if (isAddressesEqual(to, holderAddress)) {
        owned.add(Number(tokenId));
      } else if (isAddressesEqual(from, holderAddress)) {
        owned.delete(Number(tokenId));
      }
    }
  }

  // ⑦ Setをarrayに戻して返却
  return Array.from(owned);
};

// 実行
getCollection(process.env.YOUR_WALLET_ADDRESS as string).then((result) => {
  console.log("result", result);
});

解説

なぜ getOwnedTokenIds が slow なのか

原因を端的に言うと、コントラクトの ownerOf をコントラクト保有の全トークン分呼び出してメモリに一時保存する仕組みになっており、呼び出し時間がかかってしまっている、という点にあります。
またトークンの数によっては、一時保存しているデータがメモリから溢れてしまい処理不可能という状況も起こり得ます。
2023/01/12 現在の thirdweb の sdk の実装を見ていきましょう。

getOwnedTokenIds

  /**
   * Get all token ids of NFTs owned by a specific wallet.
   * @param walletAddress - the wallet address to query, defaults to the connected wallet
   */
  public async getOwnedTokenIds(walletAddress?: string) {
    if (this.query?.owned) {
      return this.query.owned.tokenIds(walletAddress);
    } else {
      const address =
        walletAddress || (await this.contractWrapper.getSignerAddress());
      const allOwners = await this.getAllOwners();
      return (allOwners || [])
        .filter((i) => address?.toLowerCase() === i.owner?.toLowerCase())
        .map((i) => BigNumber.from(i.tokenId));
    }
  }
ここが呼び出しをおこなう関数です。 const allOwners = await this.getAllOwners(); というのがキモで、前述の通り、トークン全てそれぞれのオーナーを丸っと取ってきている実装になっています。
さらにその中身を見ていきましょう。
getAllOwners() を見ていきましょう。
 /**
   * Get All owners of minted NFTs on this contract
   * @returns an array of token ids and owners
   * @twfeature ERC721Supply
   */
  public async getAllOwners() {
    return assertEnabled(this.query, FEATURE_NFT_SUPPLY).allOwners();
  }
末尾の allOwners() がキモなのでさらに追いかけます。
allOwners() を見ていくと
  /**
   * Return all the owners of each token id in this contract
   * @returns
   */
  public async allOwners() {
    return Promise.all(
      [...new Array((await this.totalCount()).toNumber()).keys()].map(
        async (i) => ({
          tokenId: i,
          owner: await this.erc721
            .ownerOf(i)
            .catch(() => constants.AddressZero),
        }),
      ),
    );
  }
となっています。erc721 はコントラクトクラスのオブジェクトで、コントラクト記述の関数がメンバとして記述されていて呼び出せるものになっている、と思っていただいて差し支えないです。
[...new Array((await this.totalCount()).toNumber()).keys()] は tokenId のトークン上限数を出力しています。つまり tokenId の数だけ map を回すと言う構造です。
つまり、トークン上限数分、 ownerOf(i) をぶん回す構造になっているので、リクエストがトークンの数線形に増えていく、と言う構造になっています。
少ないトークン数であってもそれなりに時間がかかるので、現時点では、本メソッドの利用は避けた方が良いと考えます。

解決策

ERC721 であれば、Transfer イベントが定義されており、かつ送信元と送信先のアドレスに index を貼るよう制約があるので、このイベントを利用するのが望ましいです。
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
具体的な利用方法は前述の通り下記記事に記していますのでご参考ください。

余談

なお EventLog の保存には、BloomFilterというアルゴリズムが使われており、興味がありましたら調べてみてください。

宣伝

最後に宣伝です。
hanzochang では、NFT 保有者会員サイトを作成したい、新規事業担当者様・スタートアップ事業者様・起業家様向けに、NFT を用いた会員サイトを実装しています。
thirdweb の活用または、solidity や ethers.js を組み合わせたスタンダードなアプリケーション等さまざまご相談可能です。

プロジェクト実績例

  • NFT ことラクトデプロイ
  • NFTMINT サイト実装・プロジェクトマネジメント
  • NFT 会員サイト実装
  • NFTAirdrop の実装
  • NFT を用いたウォレット保有者本人によるメッセージ送信
  • 解析アプリケーション構築
    • オンチェーンデータと SNS 活動の比較データ
  • 関連する chrome 拡張機能開発
実績等はお問い合わせいただけましたら個別にご紹介いたします。
プロフィールやスキルセット、経歴は下記に掲載しております。ご相談の際に参考にしていただければと存じます。
picture
hanzochang
代表取締役 半澤勇大
慶應義塾大学卒業後、AOI Pro.にてWebプランナーとして勤務。ナショナルクライアントのキャンペーンサイトの企画・演出を担当。その後開発会社に創業メンバーとして参加。Fintech案件や大手企業のDXプロジェクトに関わり、その後個人事業主として独立。2023年にWeb3に特化した開発会社として法人化。

2017年ごろより匿名アカウントでCryptoの調査等を行い、ブロックチェーンメディアやSNSでビットコイン論文等の図解等を発信。
hanzochangはフリーランスのエンジニアです
スキルセットや特徴は下記よりご確認いただけます
fixed
© 2023 hanzochang