ThirdwebのclaimConditionを理解する
SDK経由でThirdwebコントラクトを操作する
ThirdwebのDashboardは便利なのでそこで操作すればいいんですが、やりにくいケースもあります。
- KMSウォレットなどを利用している場合
- 大量操作・自動化などをしたい場合
ということでSDK経由でコントラクトを操作していきましょう。
今回はサーバーサイドを想定して、最低限のセットアップと、ハマりがちなclaimConditionについて書きます。
accountの生成
client-sideの場合はwalletを繋げばよいですが、サーバーやスクリプトでwrite transactionを発行するには、account
を使うことになります。
上記のページに、ethersやviemと連携したaccount
の作成方法が記載されていますので、スニペットで要点のみ示します。
import { createThirdwebClient } from 'thirdweb';
import { ethers6Adapter } from 'thirdweb/adapters/ethers6';
const client = createThirdwebClient({ secretKey: process.env.THIRDWEB_SECRET_KEY! });
const account = await ethers6Adapter.signer.fromEthers({ signer });
setClaimCondition
さて、accountの準備ができると後はだいたいドキュメントをかいつまんで読めばDashboardからの類推でなんとなく使えるんですが、claimConditionまわりは前提知識がないと戸惑う人も少なくないのかと思います。
ちなみに、概観は公式のDesign Docsに記載されているのですが、ちょっと見つけにくいかもしれません。
Dashboardでは抽象化されているが、SDKでは明示的に設定が必要なパターン例
たとえば、ThirdwebのDashboard上では、Only Owner
という設定ができます。
しかし、これと同等のことをSDKでやるには、Only Owner
のような抽象は用意されていないためoverrideList
の明示的な設定が必要です。
import { getContract, sendAndConfirmTransaction } from 'thirdweb';
import { setClaimConditions } from 'thirdweb/extensions/erc1155';
import { ethereum } from 'thirdweb/chains';
const contract = getContract({
client,
chain: ethereum,
address: contractAddress,
});
const transaction = setClaimConditions({
contract,
tokenId,
phases: [
{
maxClaimablePerWallet: 0n,
overrideList: [{ address: account.address, maxClaimable: 'unlimited' }],
},
],
});
await sendAndConfirmTransaction({ transaction, account });
setClaimConditionsは内部的に何をしているのか
claim conditionはMerkle Treeを用いて管理されています。
SDKでsetClaimConditions
を実行すると、以下のような流れでMerkle Treeが利用されます。
- サーバー上でMerkle Treeを生成
- Merkle TreeのデータをIPFSにアップロード
- 生成したMerkle Treeのハッシュをコントラクトに記録
merkleRootハッシュのコントラクトへの保存
コントラクトには以下のようにmerkleRoot
にハッシュが保管されます。
mapping(uint256 => ClaimConditionList) public claimCondition;
struct ClaimConditionList {
uint256 currentStartId;
uint256 count;
mapping(uint256 => ClaimCondition) conditions;
mapping(uint256 => mapping(address => uint256)) supplyClaimedByWallet;
}
struct ClaimCondition {
uint256 startTimestamp;
uint256 maxClaimableSupply;
uint256 supplyClaimed;
uint256 quantityLimitPerWallet;
bytes32 merkleRoot;
uint256 pricePerToken;
address currency;
string metadata;
}
このように、ベースとなるclaimConditionはコントラクトに直接記録されますが、overrideList
の項目はMerkle Treeを用いて管理されています。
IPFS URIのコントラクトへの保存
さらにコントラクトレベルのメタデータとしてcontractURI
があり、Merkle Treeデータを格納しているIPFS URIが記録されています。
function contractURI() external view returns (string memory);
string public override contractURI;
// ReturnType<typeof getContractMetadata>
{
name: string;
symbol: string;
description: string;
merkle: { [merkleRoot: `0x${string}`]: `ipfs://${string}` };
}
IPFS URIの中身
IPFS URIの中身は以下のようになっています。
{
merkleRoot: `0x${string}`,
baseUri: `ipfs://${string}`,
originalEntriesUri: `ipfs://${string}`,
shardNybbles: number,
tokenDecimals: number,
isShardedMerkleTree: boolean,
}
ここで、isShardedMerkleTree
がtrue
の場合はシャーディングされたMerkle Treeのデータを格納しています。
また、shardNybbles
はシャーディングのためのパラメータで、シャーディングのための桁数を示します。
たとえば、shardNybbles
が2
であれば、アドレスの先頭2桁がシャーディングのキーになります。
- claimerのアドレスが
0x12...
のとき${baseUri}/12
が該当データのURI - claimerのアドレスが
0x34...
のとき${baseUri}/34
が該当データのURI
さらに、originalEntriesUri
はoverrideListの各エントリがそのまま格納されたデータのURIです。
claimされたときのclaimConditionのチェック
SDKでclaimTo
を実行すると、以下のようなチェックが行われます。
- getContractMetadataを実行し、IPFS URIを取得
- IPFS URIからclaimerに対応したMerkle Treeのデータを取得
- Merkle TreeのデータからclaimToの引数
_allowListProof
を生成
このとき、_allowListProof
はIPFS由来のデータのため、setClaimConditions
直後にclaimTo
を実行すると動作が不安定な場合がありました。
SDKでIPFSデータを取得する際に、反映が遅れたりしているのではないかと思います。
まとめ
- SDKの
setClaimConditions
では、Only Owner
のような設定の仕方ができない - 同じ内容は、
overrideList
を明示的に設定することで実現できる - claimConditionはMerkle Treeを用いて管理されており、データはIPFSに保存されている
- IPFS URLは
contractURI
に記録されている -
_allowListProof
はIPFS由来のデータのため、setClaimConditions
直後にclaimTo
を実行すると動作が不安定な場合がある
Discussion