Hats Protocolを理解する3!!
はじめに
この記事は以下の記事の続きになります!!
前回の記事で HatsProtocol がどういうプロトコルかまとめました。
今回は SDK を実際に使って機能を試していきたいと思います!!
SDK を使ってみた
では次に SDK を触っていきたいと思います!!
SDK の GitHub は以下です。
自分で触ってみた記録は以下の GitHub にまとめてみました!!
SDK は viem を使うことを前提としているみたいですね。
yarn を使っている場合は以下のコマンドで必要なライブラリをインストールできます。
yarn add @hatsprotocol/sdk-v1-core
サブグラフの SDK も使うのであれば追加で以下のライブラリをインストールします。
yarn add @hatsprotocol/sdk-v1-subgraph
Hats Modules SDK を使うのであれば追加で以下のライブラリをインストールします。
yarn add @hatsprotocol/modules-sdk viem
Hats Signer Gate SDK を使うのであれば追加で以下のライブラリをインストールします。
yarn add @hatsprotocol/hsg-sdk viem
Hats Account SDK を使うのであれば以下のライブラリをインストールします。
yarn add @hatsprotocol/hats-account-sdk viem
このサンプルコードを動かすためには準備が必要なのですがそれについては README をご覧ください。
環境がセットアップできたらまず以下のコマンドでインストールします。
yarn
その後、以下のコマンドを実行してしましょう!!
yarn sample sample
以下のように出力されれば OK です!!
Current Block Number: 6484557n
Wallet's Balance: 55.995776264812553303 ETH
getting hat info
hatInfo: {
details: '',
maxSupply: 0,
supply: 0,
eligibility: '0x0000000000000000000000000000000000000000',
toggle: '0x0000000000000000000000000000000000000000',
imageUri: 'ipfs://bafkreiflezpk3kjz6zsv23pbvowtatnd5hmqfkdro33x5mh2azlhne3ah4',
numChildren: 0,
mutable: false,
active: false
}
isWearer: false
isAdmin: false
isGoodStanding: true
isEligible: true
numTrees: 443
level: 14
localLevel: 14
domain: 0
requestedAdminHat: 0n
adminHat: 0n
tippyTopHatDomain: 0
adminId: 0n
childrens: []
Done in 6.30s.
hat の情報が取得できていますね!!!
少しだけコードの解説をします。
import {
createPublicClient,
createWalletClient,
formatEther,
http,
} from "viem";
import { sepolia } from "viem/chains";
import * as dotenv from "dotenv";
import { privateKeyToAccount } from "viem/accounts";
import { HatsClient } from "@hatsprotocol/sdk-v1-core";
dotenv.config();
// 環境変数を取得する。
const { RPC_URL, PRIVATE_KEY } = process.env;
// signerオブジェクトを作成する
export const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
// Sepolia ネットワークのクライアントを作成する
export const client = createPublicClient({
chain: sepolia,
transport: http(RPC_URL),
});
// Wallet Client の作成
export const walletClient = createWalletClient({
chain: sepolia,
transport: http(RPC_URL),
account,
});
// HatsProtocol用のインスタンスを生成する。
export const hatsClient = new HatsClient({
chainId: sepolia.id,
publicClient: client,
walletClient,
});
/**
* メインスクリプト
*/
async function main() {
try {
// 最後のブロックの番号を取得する
const blockNumber = await client.getBlockNumber();
console.log("Current Block Number:", blockNumber);
// ウォレットの残高を取得する
const balance = await client.getBalance({ address: account.address });
console.log(`Wallet's Balance: ${formatEther(balance)} ETH`);
// hat ID
const hatId = 442;
console.log("getting hat info");
// hatの情報を取得する。
const hatInfo = await hatsClient.viewHat(BigInt(hatId));
console.log("hatInfo:", hatInfo);
// hatの着用者かどうかチェックする。
const isWearer = await hatsClient.isWearerOfHat({
wearer: account.address,
hatId: BigInt(hatId),
});
console.log("isWearer:", isWearer);
// hatの管理者かどうかチェックする。
const isAdmin = await hatsClient.isAdminOfHat({
user: account.address,
hatId: BigInt(hatId),
});
console.log("isAdmin:", isAdmin);
// 着用者が良好な状態にあるかチェックする。
const isGoodStanding = await hatsClient.isInGoodStanding({
wearer: account.address,
hatId: BigInt(hatId),
});
console.log("isGoodStanding:", isGoodStanding);
// 適格な着用者であるかどうかチェックする。
const isEligible = await hatsClient.isEligible({
wearer: account.address,
hatId: BigInt(hatId),
});
console.log("isEligible:", isEligible);
// ツリーの数を取得する。
const numTrees = await hatsClient.getTreesCount();
console.log("numTrees:", numTrees);
// 帽子のレベルを取得する。
const level = await hatsClient.getHatLevel(BigInt(hatId));
console.log("level:", level);
// ローカルでの帽子のレベルを取得する。
const localLevel = await hatsClient.getLocalHatLevel(BigInt(hatId));
console.log("localLevel:", localLevel);
// 帽子のドメインを取得する。
const domain = await hatsClient.getTopHatDomain(BigInt(hatId));
console.log("domain:", domain);
// ツリーのリンクリクエストを取得します。
const requestedAdminHat = await hatsClient.getLinkageRequest(domain);
console.log("requestedAdminHat:", requestedAdminHat);
// リンクされたツリーの管理者を取得する。
const adminHat = await hatsClient.getLinkedTreeAdmin(domain);
console.log("adminHat:", adminHat);
// 指定されたツリーが含まれるグローバルツリーのトッパーハットのツリードメインを取得する。
const tippyTopHatDomain = await hatsClient.getTippyTopHatDomain(domain);
console.log("tippyTopHatDomain:", tippyTopHatDomain);
// 帽子の管理者IDを取得する。
const adminId = await hatsClient.getAdmin(BigInt(hatId));
console.log("adminId:", adminId);
// 指定したHatIdに紐づく子供の帽子を取得する。
const children = await hatsClient.getChildrenHats(BigInt(hatId));
console.log("childrens:", children);
} catch (error) {
console.error("Error:", error);
}
}
main();
HatsClient
インスタンスを生成するためにchainID
やpublicClient
、オプションでWalletClient
が必要になるのでそれらのインスタンスをviem
のメソッドを利用して用意しています。
そしてあとはインスタンスを生成させメソッドを呼び出しているだけの比較的シンプルな実装になっています!!
次に Subgragh の SDK の機能を試すサンプルスクリプトを実行してみたいと思います!!
yarn sample subgraph
以下のような結果が返ってくるのではないでしょうか?
hat: {
id: '0x0000000100020001000100000000000000000000000000000000000000000000',
maxSupply: '1000',
wearers: [
{ id: '0x0040daac32d83c78546ae36da42a496b28ab09e1' },
{ id: '0x0109c7e6604b96af83ca272bcf84645ed29e7154' },
{ id: '0x011e0be1128af8c51646181368589ccfbddf746a' },
{ id: '0x017ff2643e1a6d500a54e2c15f8186c87795cbbe' },
{ id: '0x018e494352a3e68e16d03ed976fd64134bd82e72' },
{ id: '0x020f64f264ab7e90ef24a108c379a796a82175df' },
{ id: '0x03b79c0c1487a68aeabd9aa4ce779dad77855f52' },
{ id: '0x03f33bb5e7ca4fee122b1b443cebf2ed265c434a' },
{ id: '0x03f7a3fd58b090abe577651fb92fb4789826191e' },
{ id: '0x04ce3ca877bdbb2faffa63f9eee55d7d639a1700' },
{ id: '0x051ac9d0442d5c689e6a301bebc82821f42fc93a' },
{ id: '0x05a1ff0a32bc24265bcb39499d0c5d9a6cb2011c' },
{ id: '0x063560d831876c9bcebdb1ac48d81815e45a0ab9' },
{ id: '0x071e1682748679cdef2fe3e1fcfb23b8b9d13a03' },
{ id: '0x0731f454cb8682d0176ff28e413b0eba42cc82b0' },
{ id: '0x07f6c379bf06113f7f445317de2238d03911b9e6' },
{ id: '0x088e6beb2bb157940c44440578cf07072eba1cd8' },
{ id: '0x0a453f46f8ae9a99b2b901a26b53e92be6c3c43e' },
{ id: '0x0b06ca5dcc8a10be0951d4e140d4312702b8d0ec' },
{ id: '0x0b5f5a722ac5e8ecedf4da39a656fe5f1e76b34c' },
{ id: '0x0c887420937d8f9305ff872eaa5aaf5e379a811a' },
{ id: '0x0d89421d6eec0a4385f95f410732186a2ab45077' },
{ id: '0x0e11de3e815491b1383d2b9c4f99095c20b5bd90' },
{ id: '0x0ea26051f7657d59418da186137141cea90d0652' },
{ id: '0x0f07d407ac41d6dbb2cf237b0704c0ab5b9b8754' },
{ id: '0x0f1d41cc51e97dc9d0cad80dc681777eed3675e3' },
{ id: '0x0f3332af122adab1b5897b21a72315eb06ebdb31' },
{ id: '0x108c1f6802c6d991fcec033294787c08d718f445' },
{ id: '0x1233d45017d270d7ecc07494cb86d3e8dda643a0' },
{ id: '0x1235ce8f885ccca020740c1fc83b221e693bb5ff' },
{ id: '0x1253594843798ff0fcd7fa221b820c2d3ca58fd5' },
{ id: '0x1296b0a992abc44675ff6800dd86696dc9366490' },
{ id: '0x12c5b6d18536abc4766af3c2612b87eb75ec10d5' },
{ id: '0x13c877de8a85255454620ffc002cfa9ca12dcfc2' },
{ id: '0x140b4cbd81d7ceb0adfc96e5c2d640ae39ddfa22' },
{ id: '0x1421d52714b01298e2e9aa969e14c9317b3e1cfa' },
{ id: '0x143ba1aebe867c46fd48347f8da0ecdca046a40f' },
{ id: '0x177d688b3e49e3a1039e3de50d392e48cb6ca869' },
{ id: '0x178f420637c6667ba467041dd62d93e39ea1f232' },
{ id: '0x17e33637f6b64e9082ea499481b6e6ebae7eea23' },
{ id: '0x19ba17c6969b82642bceccfdfb48df9ea844e18c' },
{ id: '0x1a9cee6e1d21c3c09fb83a980ea54299f01920cd' },
{ id: '0x1aa5b637f5283a9fe53771762fc8f6f0f2d87b79' },
{ id: '0x1ad37c45ebbd03caf2551c22541d7d5e4d8aadab' },
{ id: '0x1b0132aa8db835738ee33f83d67d5a14c532b65d' },
{ id: '0x1b2c142ae4b9c72d2b8957079563d171b7f72892' },
{ id: '0x1b784725944ee55eb74f41e29d1262a0dd4d9135' },
{ id: '0x1c51517d8277c9ad6d701fb5394cec0c18219edb' },
{ id: '0x1c9f765c579f94f6502acd9fc356171d85a1f8d0' },
{ id: '0x1d3bf13f8f7a83390d03db5e23a950778e1d1309' },
{ id: '0x1da44dc5bd3ccad7c9de272a58b5507f5bc251fa' },
{ id: '0x1df428833f2c9fb1ef098754e5d710432450d706' },
{ id: '0x1e7f92540941f9539be4dbd2d7652ddce7a05a71' },
{ id: '0x1e8f9d26cf0808168e02450508991fd9c594426d' },
{ id: '0x1fde40a4046eda0ca0539dd6c77abf8933b94260' },
{ id: '0x1feadfd0e023318da5a0024e28b3a87ca5e5886d' },
{ id: '0x223da87421786dd8960bf2350e6c499bebca64d1' },
{ id: '0x224aba5d489675a7bd3ce07786fada466b46fa0f' },
{ id: '0x23db246031fd6f4e81b0814e9c1dc0901a18da2d' },
{ id: '0x2487fc7e019860afbfc7fb16689e421843c777e2' },
{ id: '0x24f193262c575a66d729334f57bdf82d8aff74cb' },
{ id: '0x25910143c255828f623786f46fe9a8941b7983bb' },
{ id: '0x26165f32607d3f8bee6cd5f0c58e94df77291af3' },
{ id: '0x26250d5b0265a9df5f59b9086cab1095254d38b1' },
{ id: '0x266e7f99787676a24a42c15bfbd7b1b734e99c4d' },
{ id: '0x26e3a9c84fdb9b7fe33dfd5e8d273d016e4e4fb6' },
{ id: '0x270de0ae1bef06d1de5cdbdcf411357f5784ed2e' },
{ id: '0x2758b8b35d2df81f764a909efc3b6aca547d7147' },
{ id: '0x27773b203954fbbb3e98dfa1a85a99e1c2f40f56' },
{ id: '0x289bded2521b51167da31752a5b121a52aa1e4b5' },
{ id: '0x29185eb8cfd22aa719529217bfbade61677e0ad2' },
{ id: '0x29864e4d1588c4164dee7cc495147ec141f9c9d5' },
{ id: '0x2a045211d7d1167d6f5f5812959fa9613f58df8a' },
{ id: '0x2abec368577257cee4b1197337b5491d4d9ed578' },
{ id: '0x2c2e67a4c5ea3335408406503844d4879c84a9f6' },
{ id: '0x2c2ebecd11077849d244263e6a5bcdf702a2664d' },
{ id: '0x2c64f2ccc998613a69b667623f65aeb75e157a24' },
{ id: '0x2cbd785ae43f796e29d4f15c5f16502de6292361' },
{ id: '0x2cc5fbc2d537e5bcd24c9782486a3014e029b1fb' },
{ id: '0x2d4ac9c27fffcd87d7fa2619f537c7eb0db96fb7' },
{ id: '0x2da2b7a81f6105a7e82816f1eb058fb5225e6e51' },
{ id: '0x2e29c9750a203eed22c037061f5fd801f6e1429f' },
{ id: '0x2e57674ea14fa08a6dda865dd139ce3924ab5951' },
{ id: '0x2fb523a295a3b194ee24dc358ba98865e61af6de' },
{ id: '0x3057a4c65d58a632dc76cff4bbe1fd97960b7053' },
{ id: '0x30a07def8043614ebb1f9d239841bb8292ffa2f9' },
{ id: '0x30cf7cb2fe80d394086dab29e1b569e0ddf0a8c0' },
{ id: '0x3219f726edfe8cdc2844bbd08025a33339b8084b' },
{ id: '0x32285e4de7ec54df85572851a2dd1bf191f2651e' },
{ id: '0x327c780114d157bee5780d56d62cfee6d8dc603e' },
{ id: '0x330a1b029a0c41b749b43274c992d535d714bf2c' },
{ id: '0x33785892bb8d9926b8e11dca233bdfce3e331aca' },
{ id: '0x34b7103905aad8a763bf8818cda22e5e9af72624' },
{ id: '0x3659c2cfb2d95cbbcbbcd45f2a41e6d7325f6436' },
{ id: '0x3803e8b525ae9fa18977f964f483598090d5084c' },
{ id: '0x38958f8b2ae828eca1e2a30c8e931d224cada075' },
{ id: '0x39b8edbc6d6bab985bf03b498166db588c00278e' },
{ id: '0x3b48e557d2145fa6df280d2b52f66aa46a3635a3' },
{ id: '0x3b60e31cfc48a9074cd5bebb26c9eaa77650a43f' },
{ id: '0x3c41f941098681bfdb14ed423709cc7c29c1e5e6' }
],
events: [
{
id: 'HatMinted-123816594-0',
transactionID: '0x2e55ee62c212a98eca82bcd75cdddadef8e476eda81402f06a5b93b8755a38a6',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123813523-24',
transactionID: '0xc3226c1c39531684b1ef6e7cd83a0031720cc7c6e4627f0f4b897dd175435938',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123811781-26',
transactionID: '0xad9c4c0893fc5606b83786f4994c4a5300564959d8e3511c842ee37837ac87b7',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123802820-5',
transactionID: '0xc25e4060ab07dd5d6369fabe102675e598730d988536d2c151fef9eed54ebef6',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123800034-73',
transactionID: '0xcadc2ef1ca95d9e696ff0d6904bd62a9cd4bf7a7e4403097343220cd0b0b4dda',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123755840-19',
transactionID: '0x5bfa04d7f973140a5f6a9738897b4dfddf676541dd7ff7d6dc55d25562709617',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123734644-66',
transactionID: '0xf0cb23ad32cb5f713ead9a8144c86c32f816e3a1b52a01fe1a07238fb909053f',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123733570-8',
transactionID: '0xbbf3565f4700e068825a4dfa4e9a22a3e00be2eca4a3fc57a393bddcb5174442',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123701721-0',
transactionID: '0xe835a75c74374eb89e47717503521c2934f9924ed62aff8c93a749c5c89a0f03',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
},
{
id: 'HatMinted-123691901-49',
transactionID: '0xcd8e0dd7964f0e4ed5154925970f628e8b261ec5e6975d4c0fd10964fe54f56f',
__typename: 'HatMintedEvent',
wearer: [Object],
operator: '0xd0929e6ae5406cbee08604de99f83cf2ce52d903'
}
]
}
hatsByIds: [
{
id: '0x0000000100020001000000000000000000000000000000000000000000000000',
wearers: [ [Object], [Object] ]
},
{
id: '0x0000000100020001000100000000000000000000000000000000000000000000',
wearers: [
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object]
]
}
]
Done in 3.05s.
コードは以下の通りです!!
Subgraph 用のインスタンスを生成し、取得したい情報を定義してメソッドを実行するだけです!
import { HatsSubgraphClient } from "@hatsprotocol/sdk-v1-subgraph";
import { optimism, sepolia } from "viem/chains";
// Subgraph用のインスタンスを生成
const hatsSubgraphClient = new HatsSubgraphClient({
config: {
[sepolia.id]: {
endpoint:
"https://api.studio.thegraph.com/query/55784/hats-v1-sepolia/version/latest",
},
[optimism.id]: {
endpoint:
"https://api.studio.thegraph.com/query/55784/hats-v1-optimism/version/latest",
},
},
});
/**
* メインスクリプト
*/
const main = async () => {
// hatの情報を取得する。
const hat = await hatsSubgraphClient.getHat({
chainId: 10, // optimism
hatId: BigInt(
"0x0000000100020001000100000000000000000000000000000000000000000000"
),
props: {
maxSupply: true, // get the maximum amount of wearers for the hat
wearers: {
// get the hat's wearers
props: {}, // for each wearer, include only its ID (address)
},
events: {
// get the hat's events
props: {
transactionID: true, // for each event, include the transaction ID
},
filters: {
first: 10, // fetch only the latest 10 events
},
},
},
});
console.log("hat:", hat);
// サンプル用のクエリを実行する
const res = await hatsSubgraphClient.getHatsByIds({
chainId: 10, // optimism
hatIds: [
BigInt(
"0x0000000100020001000100000000000000000000000000000000000000000000"
),
BigInt(
"0x0000000100020001000000000000000000000000000000000000000000000000"
),
],
props: {
wearers: {
// get each hat's wearers
props: {
currentHats: {
// for each wearer, get its hats
props: {}, // for each hat, include only its ID
},
},
},
},
});
console.log("hatsByIds:", res);
};
main();
次に TopHat をミントするスクリプトを実行してみます!!
ソースコードはこちらです。
yarn sample mintTopHat
すると以下のような結果が返ってきたのではないでしょうか?
mintTopHatResult: {
status: 'success',
transactionHash: '0xadcb165c2a65f6a0b348a0387c4cc5426cf59607585ce32e486454efaf5b977a',
hatId: 12078056106883486628010822758984794541789440701298176471534417391648768n
}
次に Hat を作るスクリプトを実装してみようと思います!!
ソースコードはこちらです。
yarn sample createHat
createHatResult: {
status: 'success',
transactionHash: '0x2d0a7c492a0ba9a49ab6fda97bfb329a6a22dff6c27f65670ab97fe229b03898',
hatId: 12078056518259625958312333297727090181127066946982142879929383228801024n
}
まとめて Hat を作るスクリプトも試してみます!!
ソースコードはこちらです。
yarn sample batchCreateHats
batchCreateHatsResult: {
status: 'success',
transactionHash: '0xaa70d7e8bf6e7eaf2ee586297a93892942b6db0385a90f11f117de9826fd6654',
hatIds: [
12078056929635765288613843836469385820464693192666109288324349065953280n
]
}
次に作った Hat をミントするスクリプトを実行してみます!!
ソースコードはこちらです。
yarn sample mintHat
mintTopHatResult: {
status: 'success',
transactionHash: '0x734483b0ebba7e8ad3a75c263a1e0742e61215fb33afae2feb06356fce30987c'
}
次にこのは Hat を別の人に transfer してみたいと思います!!
ソースコードは以下の通りです。
yarn sample transferHat
すると....
ちゃんと移転できました!!
transferHatResult: {
status: 'success',
transactionHash: '0xaa5366f06f93f5003e36ea612dd80c0608d5b2178f43f327cba7526416f4538f'
}
Done in 13.16s.
hats-module-template を試してみた!
以下のように HatsModule を開発するためのテンプレートが公開されています。
今回はこれを試してみました!!
使用しているフレームワークは、 foundry です。
試したソースコードは以下に格納しています。
README にあるように環境変数を設定したら早速動かしてみましょう。
-
yarn sample-hats-module setUp
forge install
が実行される。 -
yarn sample-hats-module fmt
-
yarn sample-hats-module build
デフォルトで用意されているコントラクトは次の通り。
HatModule コントラクトを継承しているだけの非常にシンプルなコントラクトです。// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; // import { console2 } from "forge-std/Test.sol"; // remove before deploy import { HatsModule } from "hats-module/HatsModule.sol"; /** * HatsModuleを継承したModuleコントラクト */ contract Module is HatsModule { /*////////////////////////////////////////////////////////////// CUSTOM ERRORS //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// DATA MODELS //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ /** * This contract is a clone with immutable args, which means that it is deployed with a set of * immutable storage variables (ie constants). Accessing these constants is cheaper than accessing * regular storage variables (such as those set on initialization of a typical EIP-1167 clone), * but requires a slightly different approach since they are read from calldata instead of storage. * * Below is a table of constants and their location. * * For more, see here: https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args * * ----------------------------------------------------------------------+ * CLONE IMMUTABLE "STORAGE" | * ----------------------------------------------------------------------| * Offset | Constant | Type | Length | Source | * ----------------------------------------------------------------------| * 0 | IMPLEMENTATION | address | 20 | HatsModule | * 20 | HATS | address | 20 | HatsModule | * 40 | hatId | uint256 | 32 | HatsModule | * 72+ | {other constants} | address | - | {this} | * ----------------------------------------------------------------------+ */ /*////////////////////////////////////////////////////////////// MUTABLE STATE //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /// @notice Deploy the implementation contract and set its version /// @dev This is only used to deploy the implementation contract, and should not be used to deploy clones constructor(string memory _version) HatsModule(_version) { } /*////////////////////////////////////////////////////////////// INITIALIZOR //////////////////////////////////////////////////////////////*/ function _setUp(bytes calldata _initData) internal override { // decode init data } /*////////////////////////////////////////////////////////////// PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////*/ /*////////////////////////////////////////////////////////////// MODIFERS //////////////////////////////////////////////////////////////*/ }
-
yarn sample-hats-module test
実行するテストコードは次の通り
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; import { Test, console2 } from "forge-std/Test.sol"; import { Module } from "../src/Module.sol"; import { Deploy, DeployPrecompiled } from "../script/Deploy.s.sol"; import { HatsModuleFactory, IHats, deployModuleInstance, deployModuleFactory } from "hats-module/utils/DeployFunctions.sol"; import { IHats } from "hats-protocol/Interfaces/IHats.sol"; /** * テストスクリプト */ contract ModuleTest is Deploy, Test { /// @dev Inherit from DeployPrecompiled instead of Deploy if working with pre-compiled contracts /// @dev variables inhereted from Deploy script // Module public implementation; // bytes32 public SALT; uint256 public fork; uint256 public BLOCK_NUMBER = 17_671_864; // deployment block for Hats.sol IHats public HATS = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); // v1.hatsprotocol.eth HatsModuleFactory public factory; Module public instance; bytes public otherImmutableArgs; bytes public initArgs; uint256 public hatId; uint256 saltNonce; string public MODULE_VERSION; function setUp() public virtual { // create and activate a fork, at BLOCK_NUMBER fork = vm.createSelectFork(vm.rpcUrl("mainnet"), BLOCK_NUMBER); // deploy implementation via the script prepare(false, MODULE_VERSION); // run メソッド run(); // deploy the hats module factory factory = deployModuleFactory(HATS, SALT, "test factory"); } } contract WithInstanceTest is ModuleTest { function setUp() public virtual override { super.setUp(); // set up the hats // set up the other immutable args otherImmutableArgs = abi.encodePacked(); // set up the init args initArgs = abi.encode(); // set up the salt nonce saltNonce = 1; // deploy an instance of the module instance = Module(deployModuleInstance(factory, address(implementation), hatId, otherImmutableArgs, initArgs, saltNonce)); } } contract Deployment is WithInstanceTest { /// @dev ensure that both the implementation and instance are properly initialized function test_initialization() public { // implementation vm.expectRevert("Initializable: contract is already initialized"); implementation.setUp("setUp attempt"); // instance vm.expectRevert("Initializable: contract is already initialized"); instance.setUp("setUp attempt"); } function test_version() public { assertEq(instance.version(), MODULE_VERSION); } function test_implementation() public { assertEq(address(instance.IMPLEMENTATION()), address(implementation)); } function test_hats() public { assertEq(address(instance.HATS()), address(HATS)); } function test_hatId() public { assertEq(instance.hatId(), hatId); } // test other initial values } contract UnitTests is WithInstanceTest { }
Ran 5 tests for test/Module.t.sol:Deployment [PASS] test_hatId() (gas: 13088) [PASS] test_hats() (gas: 13212) [PASS] test_implementation() (gas: 13205) [PASS] test_initialization() (gas: 19603) [PASS] test_version() (gas: 18366) Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.05s (1.08ms CPU time) Ran 1 test suite in 2.05s (2.05s CPU time): 5 tests passed, 0 failed, 0 skipped (5 total tests) Done in 3.02s.
-
yarn sample-hats-module deploy -vvv -f sepolia --broadcast --tc Deploy
[⠊] Compiling... No files changed, compilation skipped Script ran successfully. == Logs == Module: 0x6FE1ACeaa808095122Ddb9583718F7789E808068 ## Setting up 1 EVM. ========================== Chain 11155111 Estimated gas price: 4.485515613 gwei Estimated total gas used for script: 659336 Estimated amount required: 0.002957461922212968 ETH ========================== ##### sepolia ✅ [Success]Hash: 0x9ecea2811aaac332a566a298614a55bc456cac05bf3fc35d2956f4fd80b98460 Block: 6536359 Paid: 0.001446508407557616 ETH (477516 gas * 3.029235476 gwei) ✅ Sequence #1 on sepolia | Total Paid: 0.001446508407557616 ETH (477516 gas * avg 3.029235476 gwei) ========================== ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. Transactions saved to: /Users/harukikondo/git/HatsProtocolSample/pkgs/sample-hats-module/broadcast/Deploy.s.sol/11155111/run-latest.json Sensitive values saved to: /Users/harukikondo/git/HatsProtocolSample/pkgs/sample-hats-module/cache/Deploy.s.sol/11155111/run-latest.json ✨ Done in 36.41s.
デプロイスクリプトは以下の通りです。
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.19; import { Script, console2 } from "forge-std/Script.sol"; import { Module } from "../src/Module.sol"; import { SampleForwarder } from "../src/SampleForwarder.sol"; import { HelloWorld } from "../src/HelloWorld.sol"; contract Deploy is Script { Module public implementation; SampleForwarder public forwarder; HelloWorld public helloWorld; bytes32 public SALT = bytes32(abi.encode("change this to the value of your choice")); // default values bool internal _verbose = true; string internal _version = "0.0.5"; // increment this with each new deployment /// @dev Override default values, if desired function prepare(bool verbose, string memory version) public { _verbose = verbose; _version = version; } /// @dev Set up the deployer via their private key from the environment function deployer() public returns (address) { uint256 privKey = vm.envUint("PRIVATE_KEY"); return vm.rememberKey(privKey); } function _log(string memory prefix) internal view { if (_verbose) { console2.log(string.concat(prefix, "Module:"), address(implementation)); console2.log(string.concat(prefix, "SampleForwarder:"), address(forwarder)); console2.log(string.concat(prefix, "HelloWorld:"), address(helloWorld)); } } /// @dev Deploy the contract to a deterministic address via forge's create2 deployer factory. function run() public virtual { vm.startBroadcast(deployer()); // deploy forwarder contract forwarder = new SampleForwarder(); // deploy HelloWorld contract helloWorld = new HelloWorld(address(forwarder)); /** * @dev Deploy the contract to a deterministic address via forge's create2 deployer factory, which is at this * address on all chains: `0x4e59b44847b379578588920cA78FbF26c0B4956C`. * The resulting deployment address is determined by only two factors: * 1. The bytecode hash of the contract to deploy. Setting `bytecode_hash` to "none" in foundry.toml ensures that * never differs regardless of where its being compiled * 2. The provided salt, `SALT` */ implementation = new Module{ salt: SALT }(_version, address(forwarder)); vm.stopBroadcast(); _log(""); } } /// @dev Deploy pre-compiled ir-optimized bytecode to a non-deterministic address contract DeployPrecompiled is Deploy { /// @dev Update SALT and default values in Deploy contract function run() public override { vm.startBroadcast(deployer()); bytes memory args = abi.encode( /* insert constructor args here */ ); /// @dev Load and deploy pre-compiled ir-optimized bytecode. implementation = Module(deployCode("optimized-out/Module.sol/Module.json", args)); vm.stopBroadcast(); _log("Precompiled "); } }
最後に
HatsProtocol を調べてみましたが、Web3 版の AWS IAM ロールみたいで非常に面白いと面白いと思いました!!
先日東京で開催された ETH Tokyo でこの HatsProtocol を使ったプロダクトを開発してみたのですが DAO ツールとは相性が良さそうですね。
Safe とも連携できるので個人のウォレットをいちいち登録したり解除したりするよりも Safe 管理用の Hat を作ってそれを被せたり脱がせたりする方が良さそうです!!
ETH Tokyo で作成したプロダクトは以下から確認ができます!!!!
ありがたいことにファイナリストに選出していただき、GMO と Cabinet からもスポンサー賞を受賞できました!
GitHub のリポジトリは以下の通りです!!!
HatsProtol の他に Splits、INTMAX Wallet SDK、ENS、RainbowKit Viem、Wagmi、メタトランザクションの仕組みを採用していて非常に参考になると思います!!
OSS で開発を続けていこうと思いますので興味のある方はぜひご連絡ください!!!
ここまで読んでいただきありがとうございました!!!!!
Discussion