
thirdwebのgetOwnedTokenIdsがslowな問題を解消する
thirdweb の getOwnedTokenIds のレスポンスが遅く、利用に耐えないという問題があります。トークン数が少ない場合は問題が顕在化しませんが、トークン数が多い場合この点難しいところです。本記事ではそれに対しての対策を案内します。
公開日2023.01.12
更新日2023.01.12
この記事について
この記事は thirdweb の sdk の NFT 取得メソッドである getOwnedTokenIds が slow な問題に対しての解決策を提示します。
getOwnedTokenIdsただし対応できるのは ERC721 に準拠したもののみとなっています。 thirdweb のコントラクト名でいうと NFTDrop ならびに SignatureDrop この 2 点に対して対応可能な方法です。
Q.課題
thirdweb の getOwnedTokenIds のレスポンスが遅く、利用に耐えない。 特にトークン数が多い時地獄。リクエストが遅いだけでなく、そもそもメモリから溢れて動かなくなっちゃった。
A.結論
thirdweb sdk ではなく、ethers.js で実装することで解決します。 下記記事にまとめていますのでご確認ください。
NFT 会員サイト作成 TIPS - 保有 NFT 一覧を高速に取得する方法
NFT 会員サイト作成 TIPS - 保有 NFT 一覧を高速に取得する方法詳細と解説は上記記事に譲りますが、解決策のコードは下記となります。
(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();
}
/**
* 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);
具体的な利用方法は前述の通り下記記事に記していますのでご参考ください。
NFT 会員サイト作成 TIPS - 保有 NFT 一覧を高速に取得する方法
余談
なお EventLog の保存には、BloomFilterというアルゴリズムが使われており、興味がありましたら調べてみてください。
BloomFilter最後に宣伝です。
hanzochang では、NFT 保有者会員サイトを作成したい、新規事業担当者様・スタートアップ事業者様・起業家様向けに、NFT を用いた会員サイトを実装しています。
thirdweb の活用または、solidity や ethers.js を組み合わせたスタンダードなアプリケーション等さまざまご相談可能です。
プロジェクト実績例
- NFT ことラクトデプロイ
- NFTMINT サイト実装・プロジェクトマネジメント
- NFT 会員サイト実装
- NFTAirdrop の実装
- NFT を用いたウォレット保有者本人によるメッセージ送信
- 解析アプリケーション構築
- 関連する chrome 拡張機能開発
実績等はお問い合わせいただけましたら個別にご紹介いたします。
プロフィールやスキルセット、経歴は下記に掲載しております。ご相談の際に参考にしていただければと存じます。
お問い合わせはこちらから
ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。