この記事について
この記事は 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();
};
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",
},
];
const provider = new ethers.providers.JsonRpcProvider(
process.env.RPC_URL as string,
process.env.NETWORK as string
);
const token = new ethers.Contract(
process.env.NFT_CONTRACT_ADDRESS as string,
sampleAbi,
provider
);
const sentLogs = await token.queryFilter(
token.filters.Transfer(holderAddress, null)
);
const receivedLogs = await token.queryFilter(
token.filters.Transfer(null, holderAddress)
);
const logs = sentLogs
.concat(receivedLogs)
.sort(
(a, b) =>
a.blockNumber - b.blockNumber || a.transactionIndex - b.transactionIndex
);
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));
}
}
}
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
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(); というのがキモで、前述の通り、トークン全てそれぞれのオーナーを丸っと取ってきている実装になっています。
さらにその中身を見ていきましょう。
/**
* 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() がキモなのでさらに追いかけます。
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 を用いたウォレット保有者本人によるメッセージ送信
関連する chrome 拡張機能開発
実績等はお問い合わせいただけましたら個別にご紹介いたします。
プロフィールやスキルセット、経歴は下記に掲載しております。ご相談の際に参考にしていただければと存じます。