🐶

Hats Protocolを理解する3!!

2024/08/16に公開

はじめに

この記事は以下の記事の続きになります!!

https://zenn.dev/mashharuki/articles/f3a7047e0f0003

前回の記事で HatsProtocol がどういうプロトコルかまとめました。

今回は SDK を実際に使って機能を試していきたいと思います!!

SDK を使ってみた

では次に SDK を触っていきたいと思います!!

SDK の GitHub は以下です。

https://github.com/Hats-Protocol/sdk-v1-core

自分で触ってみた記録は以下の GitHub にまとめてみました!!

https://github.com/mashharuki/HatsProtocolSample

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インスタンスを生成するためにchainIDpublicClient、オプションで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 をミントするスクリプトを実行してみます!!

ソースコードはこちらです。

https://github.com/mashharuki/HatsProtocolSample/blob/main/pkgs/sample/src/mintTopHat.ts

yarn sample mintTopHat

すると以下のような結果が返ってきたのではないでしょうか?

mintTopHatResult: {
  status: 'success',
  transactionHash: '0xadcb165c2a65f6a0b348a0387c4cc5426cf59607585ce32e486454efaf5b977a',
  hatId: 12078056106883486628010822758984794541789440701298176471534417391648768n
}

https://sepolia.etherscan.io/tx/0xadcb165c2a65f6a0b348a0387c4cc5426cf59607585ce32e486454efaf5b977a

次に Hat を作るスクリプトを実装してみようと思います!!

ソースコードはこちらです。

https://github.com/mashharuki/HatsProtocolSample/blob/main/pkgs/sample/src/createHat.ts

yarn sample createHat
createHatResult: {
  status: 'success',
  transactionHash: '0x2d0a7c492a0ba9a49ab6fda97bfb329a6a22dff6c27f65670ab97fe229b03898',
  hatId: 12078056518259625958312333297727090181127066946982142879929383228801024n
}

https://sepolia.etherscan.io/tx/0x2d0a7c492a0ba9a49ab6fda97bfb329a6a22dff6c27f65670ab97fe229b03898

まとめて Hat を作るスクリプトも試してみます!!

ソースコードはこちらです。

https://github.com/mashharuki/HatsProtocolSample/blob/main/pkgs/sample/src/batchCreateHats.ts

yarn sample batchCreateHats
batchCreateHatsResult: {
  status: 'success',
  transactionHash: '0xaa70d7e8bf6e7eaf2ee586297a93892942b6db0385a90f11f117de9826fd6654',
  hatIds: [
    12078056929635765288613843836469385820464693192666109288324349065953280n
  ]
}

https://sepolia.etherscan.io/tx/0xaa70d7e8bf6e7eaf2ee586297a93892942b6db0385a90f11f117de9826fd6654

次に作った Hat をミントするスクリプトを実行してみます!!

ソースコードはこちらです。

https://github.com/mashharuki/HatsProtocolSample/blob/main/pkgs/sample/src/mintHat.ts

yarn sample mintHat
mintTopHatResult: {
  status: 'success',
  transactionHash: '0x734483b0ebba7e8ad3a75c263a1e0742e61215fb33afae2feb06356fce30987c'
}

https://sepolia.etherscan.io/tx/0x734483b0ebba7e8ad3a75c263a1e0742e61215fb33afae2feb06356fce30987c

次にこのは Hat を別の人に transfer してみたいと思います!!

ソースコードは以下の通りです。

https://github.com/mashharuki/HatsProtocolSample/blob/main/pkgs/sample/src/transferHats.ts

yarn sample transferHat

すると....

ちゃんと移転できました!!

transferHatResult: {
  status: 'success',
  transactionHash: '0xaa5366f06f93f5003e36ea612dd80c0608d5b2178f43f327cba7526416f4538f'
}
Done in 13.16s.

https://sepolia.etherscan.io/tx/0xaa5366f06f93f5003e36ea612dd80c0608d5b2178f43f327cba7526416f4538f

hats-module-template を試してみた!

以下のように HatsModule を開発するためのテンプレートが公開されています。

今回はこれを試してみました!!

https://github.com/Hats-Protocol/hats-module-template

使用しているフレームワークは、 foundry です。

試したソースコードは以下に格納しています。

https://github.com/mashharuki/HatsProtocolSample

README にあるように環境変数を設定したら早速動かしてみましょう。

  • HatsModules コントラクトのセットアップ

    yarn sample-hats-module setUp
    

    forge install が実行される。

  • HatsModules コントラクトのフォーマットチェック

    yarn sample-hats-module fmt
    
  • HatsModules コントラクトのビルド

    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
      //////////////////////////////////////////////////////////////*/
    }
    
  • HatsModules コントラクトのテスト

    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.
    
  • HatsModules コントラクトをデプロイする

    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 で作成したプロダクトは以下から確認ができます!!!!

https://app.akindo.io/communities/27X3dKjOoFjaX0E4/products/0npJeQPZnh3LAxjD

ありがたいことにファイナリストに選出していただき、GMO と Cabinet からもスポンサー賞を受賞できました!

https://x.com/ethereum_jp/status/1827877338362896390?s=46&t=cfPJHntnDl2Ey_QvRJtl3Q

https://twitter.com/HARUKI05758694/status/1827892706980626706

GitHub のリポジトリは以下の通りです!!!

HatsProtol の他に Splits、INTMAX Wallet SDK、ENS、RainbowKit Viem、Wagmi、メタトランザクションの仕組みを採用していて非常に参考になると思います!!

https://github.com/hackdays-io/toban

OSS で開発を続けていこうと思いますので興味のある方はぜひご連絡ください!!!

ここまで読んでいただきありがとうございました!!!!!

GitHubで編集を提案

Discussion