Hats Protocolを理解する2!!
はじめに
この記事は以下の記事の続きになります!!
前回まで Hats Module SDKの概要をまとめていました。
今回はその続きで Hats Signer Gate SDK 、 Hats Account SDK についてまとめた記事になります!!
Hats Signer Gate SDK
Hats Signer Gate(HSG) は、特定のHatを着用しているアドレスに対してマルチシグ署名権を付与するコントラクトです。
これにより、オンチェーン組織(DAOなど)が、個々のユーザーに対して制限付きの署名権限と責任を取り消し可能に委任することができます。
Multi Hats Signer Gate(MHSG) は、HSGの改良版で、複数のHatを有効な署名者Hatとして設定することができます。
このSDKは、HSGおよびMHSGのインスタンスを作成し、操作するためのオープンソースのJavaScriptクライアントです。ブラウザとNode.jsの両方で動作するように設計されています。
Hats Signer Gateの詳細な概要については、こちらをご覧ください。
-
以下のコマンドで必要なライブラリをインストールする。
yarn add @hatsprotocol/hsg-sdk viem
実際には以下のようにインスタンスを作成する。
import { HatsSignerGateClient } from "@hatsprotocol/hsg-sdk"; const hatsSignerGateClient = new HatsSignerGateClient({ publicClient, walletClient, });
-
-
deployHatsSignerGateAndSafe
新しいHSGと新しいSafeを作成し、それらをすべて接続します。
/** * @param account: Account | Address; // Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプはAccount) * @param ownerHatId: bigint; // HSGのオーナーHatのID * @param signersHatId: bigint; // HSGの署名者HatのID * @param minThreshold: bigint; // HSGの最小閾値 * @param targetThreshold: bigint; // HSGの目標閾値 * @param maxSigners: bigint; // HSGの最大署名者数 */ const createHsgResult = await hatsSignerGateClient.deployHatsSignerGateAndSafe( { account, ownerHatId, signersHatId, minThreshold, targetThreshold, maxSigners, });
-
deployHatsSignerGate
新しいHSGをデプロイし、それを既存のSafeに関連付けます。既存のSafeに接続するには、Safeの所有者がそれをモジュールおよびガードとして有効にする必要があります。
HatsSignerGateをSafeに接続する前に、canAttachHSGToSafeを呼び出して結果がtrueであることを確認してください。そうしないと、Safeが永久にロックされる可能性があります。
/** * @param account: Account | Address; // Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプはAccount) * @param ownerHatId: bigint; // HSGのオーナーHatのID * @param signersHatId: bigint; // HSGの署名者HatのID * @param safe: Address; // 署名者が参加する既存のGnosis Safe * @param minThreshold: bigint; // HSGの最小閾値 * @param targetThreshold: bigint; // HSGの目標閾値 * @param maxSigners: bigint; // HSGの最大署名者数 */ const createHsgResult = await hatsSignerGateClient.deployHatsSignerGate( { account, ownerHatId, signersHatId, safe, minThreshold, targetThreshold, maxSigners, });
-
deployMultiHatsSignerGateAndSafe
新しいMHSGと新しいSafeを作成し、それらをすべて接続します。
/** * @param account: Account | Address; // Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプはAccount) * @param ownerHatId: bigint; // MHSGのオーナーHatのID * @param signersHatIds: bigint[]; // MHSGの署名者HatのIDs * @param minThreshold: bigint; // MHSGの最小閾値 * @param targetThreshold: bigint; // MHSGの目標閾値 * @param maxSigners: bigint; // MHSGの最大署名者数 */ const createMhsgResult = await hatsSignerGateClient.deployMultiHatsSignerGateAndSafe( { account, ownerHatId, signersHatIds, minThreshold, targetThreshold, maxSigners, });
-
deployMultiHatsSignerGate
新しいMHSGをデプロイし、それを既存のSafeに関連付けます。既存のSafeに接続するには、Safeの所有者がそれをモジュールおよびガードとして有効にする必要があります。
MultiHatsSignerGateをSafeに接続する前に、canAttachMHSGToSafeを呼び出して結果がtrueであることを確認してください。そうしないと、Safeが永久にロックされる可能性があります。
/** * @param account: Account | Address; // Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプはAccount) * @param ownerHatId: bigint; // MHSGのオーナーHatのID * @param signersHatIds: bigint[]; // MHSGの署名者HatのIDs * @param safe: Address; // 署名者が参加する既存のGnosis Safe * @param minThreshold: bigint; // MHSGの最小閾値 * @param targetThreshold: bigint; // MHSGの目標閾値 * @param maxSigners: bigint; // MHSGの最大署名者数 */ const createMhsgResult = await hatsSignerGateClient.deployMultiHatsSignerGate( { account, ownerHatId, signersHatIds, safe, minThreshold, targetThreshold, maxSigners, });
-
-
HSGインスタンスと対話するための機能です。
-
-
hsgClaimSigner
Signer権限を要求するメソッド
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウント用のアドレス、その他のタイプ用のアカウント)。 * @param {Address} hsgInstance - HSGのインスタンスアドレス。 * @return {Object} - トランザクションのステータスとハッシュ。 */ const claimSignerResult = await hatsSignerGateClient.hsgClaimSigner({ account, hsgInstance, });
-
hsgIsValidSigner
アカウントがSinersHatを着用しているかどうかを確認するメソッド
/** * @param {Address} hsgInstance - HSGのインスタンスアドレス。 * @param {Address} address - 確認するアドレス。 * @return {boolean} - 有効なサイナーであればtrue、そうでなければfalse。 */ const isValid = await hatsSignerGateClient.hsgIsValidSigner({ hsgInstance, address });
-
claimedAndStillValid
アカウントがSigner権利を所有しており、なおかつ有効であるかどうかを確認するメソッド
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @param {Address} address - 確認するアドレス。 * @return {boolean} - 安全の所有者の1人であり、かつ有効な場合はtrue、それ以外はfalse。 */ const claimedAndValid = await hatsSignerGateClient.claimedAndStillValid({ instance, address });
-
validSignerCount
Signer'sHatを着用している有効なSafeオーナーの数を集計します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @return {bigint} - Safeにおける有効なサイナーの数。 */ const count = await hatsSignerGateClient.validSignerCount({ instance });
-
reconcileSignerCount
Signer'sHatを着用している有効なSafeオーナーの数を集計し、必要に応じてSafeの閾値を更新します。ただし、無効なSafeオーナーを削除することはありません。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウント用のアドレス、その他のタイプ用のアカウント)。 * @param {Address} instance - HSGのインスタンスアドレス。 * @return {Object} - トランザクションのステータスとハッシュ。 */ const res = await hatsSignerGateClient.reconcileSignerCount({ account, instance, });
-
removeSigner
無効なSignerをSafeから削除し、適切に閾値を更新します。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウント用のアドレス、その他のタイプ用のアカウント)。 * @param {Address} instance - HSGのインスタンスアドレス。 * @param {Address} signer - 無効な場合に削除するアドレス。 * @return {Object} - トランザクションのステータスとハッシュ。 */ const res = await hatsSignerGateClient.removeSigner({ account, instance, signer, });
-
-
-
hsgSignersHatId
HSGのSigner's Hat IDを取得します。
/** * @param {Address} hsgInstance - HSGのインスタンスアドレス。 * @return {bigint} - HSGのサイナーズハットID。 */ const signersHat = await hatsSignerGateClient.hsgSignersHatId({ hsgInstance });
-
getSafe
HSGにアタッチされたSafeを取得します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @return {Address} - アタッチされたSafeのアドレス。 */ const safe = await hatsSignerGateClient.getSafe({ instance });
-
getMinThreshold
HSGの最小閾値を取得します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @return {bigint} - インスタンスの最小閾値。 */ const minThreshold = await hatsSignerGateClient.getMinThreshold({ instance });
-
getTargetThreshold
HSGの目標閾値を取得します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @return {bigint} - インスタンスの目標閾値。 */ const targetThreshold = await hatsSignerGateClient.getTargetThreshold({ instance });
-
getMaxSigners
HSGの最大Signers数を取得します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @return {bigint} - インスタンスの最大サイナー数。 */ const maxSigners = await hatsSignerGateClient.getMaxSigners({ instance });
-
getOwnerHat
HSGのowner Hatを取得します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @return {bigint} - インスタンスのオーナーハット。 */ const ownerHat = await hatsSignerGateClient.getOwnerHat({ instance });
-
-
-
MHSGインスタンスを操作するメソッド
-
-
mhsgClaimSigner
署名者の権利をSafeで要求します。
/** * @param {Account | Address} account - Viemのアカウント(JSON-RPCアカウントの場合はAddress、それ以外の場合はAccount)。 * @param {Address} mhsgInstance - MHSGのインスタンスアドレス。 * @param {bigint} hatId - 署名者の権利を主張するためのHat ID。これは有効なSigners Hatでなければならない。 */ const claimSignerResult = await hatsSignerGateClient.mhsgClaimSigner({ account, mhsgInstance, hatId, });
-
mhsgIsValidSigner
アカウントがSigners Hatを着用し、署名者の権利を主張しているかどうかを確認します。
/** * @param {Address} mhsgInstance - MHSGのインスタンスアドレス。 * @param {Address} address - 確認するアドレス。 */ const isValid = await hatsSignerGateClient.mhsgIsValidSigner({ mhsgInstance, address });
-
claimedAndStillValid
アカウントが署名者の権利を主張しており、まだ有効であるかどうかを確認します。
/** * @param {Address} instance - HSGのインスタンスアドレス。 * @param {Address} address - 確認するアドレス。 */ const claimedAndValid = await hatsSignerGateClient.claimedAndStillValid({ instance, address });
-
validSignerCount
Signers Hatを着用している有効なSafeの所有者の数を数えます。
/** * @param {Address} instance - MHSGのインスタンスアドレス。 */ const count = await hatsSignerGateClient.validSignerCount({ instance });
-
reconcileSignerCount
Signers Hatを着用しているSafeの所有者の数を数え、必要に応じてSafeの閾値を更新します。ただし、無効なSafeの所有者は削除しません。
/** * @param {Account | Address} account - Viemのアカウント(JSON-RPCアカウントの場合はAddress、それ以外の場合はAccount)。 * @param {Address} instance - MHSGのインスタンスアドレス。 */ const res = await hatsSignerGateClient.reconcileSignerCount({ account, instance, });
-
removeSigner
無効な署名者をSafeから削除し、適宜閾値を更新します。
/** * @param {Account | Address} account - Viemのアカウント(JSON-RPCアカウントの場合はAddress、それ以外の場合はAccount)。 * @param {Address} instance - MHSGのインスタンスアドレス。 * @param {Address} signer - 無効な署名者の場合に削除するアドレス。 */ const res = await hatsSignerGateClient.removeSigner({ account, instance, signer, });
-
-
MHSGインスタンスの基本的なプロパティを取得します。
-
mhsgIsValidSignersHat
指定されたHatが有効なSigners Hatであるかを確認します。
/** * @param {Address} mhsgInstance - MHSGのインスタンスアドレス。 * @param {bigint} hatId - 確認するHat ID。 */ const isValid = await hatsSignerGateClient.mhsgIsValidSignersHat({ mhsgInstance, hatId });
-
getSafe
MHSGに関連付けられたSafeを取得します。
/** * @param {Address} instance - MHSGのインスタンスアドレス。 */ const safe = await hatsSignerGateClient.getSafe({ instance });
-
getMinThreshold
MHSGの最小閾値を取得します。
/** * @param {Address} instance - MHSGのインスタンスアドレス。 */ const minThreshold = await hatsSignerGateClient.getMinThresholde({ instance });
-
getTargetThreshold
MHSGの目標閾値を取得します。
/** * @param {Address} instance - MHSGのインスタンスアドレス。 */ const targetThreshold = await hatsSignerGateClient.getTargetThreshold({ instance });
-
getMaxSigners
/** * @param {Address} instance - MHSGのインスタンスアドレス。 */ const maxSigners = await hatsSignerGateClient.getMaxSigners({ instance });
-
getOwnerHat
/** * @return {bigint} - インスタンスの最大署名者数。 */ const ownerHat = await hatsSignerGateClient.getOwnerHat({ instance });
-
-
MHSGインスタンスのOwner Hatを持つユーザーにのみが実行できるメソッド
-
mhsgAddSignerHats
新しい承認済みSigners Hatsを追加します。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプの場合はAccount)。 * @param {Address} mhsgInstance - MHSGインスタンスのアドレス。 * @param {bigint[]} newSignerHats - 承認済みのSigners Hatsとして追加するHat IDの配列。 * @return {{ status: "success" | "reverted", transactionHash: `0x${string}` }} - 取引が成功した場合は"success"、取引がリバートされた場合は"reverted"を返します。transactionHashは取引のハッシュです。 */ const res = await hatsSignerGateClient.mhsgAddSignerHats({ account, mhsgInstance, newSignerHats, });
-
setTargetThreshold
新しいターゲットしきい値を設定し、適切な場合にはSafeのしきい値を変更します。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプの場合はAccount)。 * @param {Address} instance - HSGインスタンスのアドレス。 * @param {bigint} targetThreshold - 設定する新しいターゲットしきい値。 * @return {{ status: "success" | "reverted", transactionHash: `0x${string}` }} - 取引が成功した場合は"success"、取引がリバートされた場合は"reverted"を返します。transactionHashは取引のハッシュです。 */ const res = await hatsSignerGateClient.setTargetThreshold({ account, instance, targetThreshold, });
-
setMinThreshold
新しい最小しきい値を設定します。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプの場合はAccount)。 * @param {Address} instance - HSGインスタンスのアドレス。 * @param {bigint} minThreshold - 設定する新しい最小しきい値。 * @return {{ status: "success" | "reverted", transactionHash: `0x${string}` }} - 取引が成功した場合は"success"、取引がリバートされた場合は"reverted"を返します。transactionHashは取引のハッシュです。 */ const res = await hatsSignerGateClient.setMinThreshold({ account, instance, minThreshold, });
-
setOwnerHat
新しいOwner Hatを設定します。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプの場合はAccount)。 * @param {Address} instance - MHSGインスタンスのアドレス。 * @param {bigint} newOwnerHat - 設定する新しいOwner Hat。 * @param {Address} hatsContractAddress - 新しいOwner HatのHats.solコントラクトアドレス。 * @return {{ status: "success" | "reverted", transactionHash: `0x${string}` }} - 取引が成功した場合は"success"、取引がリバートされた場合は"reverted"を返します。transactionHashは取引のハッシュです。 */ const res = await hatsSignerGateClient.setOwnerHat({ account, instance, newOwnerHat, hatsContractAddress, });
-
-
-
SDKにはHSGおよびMHSGインスタンスの書き込み操作を呼び出すための単一のハンドラーも含まれています。これにより、HSG/MHSGのやり取りを、Hats Modulesとのやり取りと同様に、HSGおよびMHSGのメタデータオブジェクトと単一の書き込み関数ハンドラーを使用して処理することが可能になります。
-
-
callInstanceWriteFunction
HSG/MHSGインスタンスの書き込み関数を呼び出します。
/** * @param {Account | Address} account - Viemアカウント(JSON-RPCアカウントの場合はAddress、その他のタイプの場合はAccount)。 * @param {HsgType} type - 'HSG' または 'MHSG'。 * @param {Address} instance - MHSG/HSGインスタンスのアドレス。 * @param {WriteFunction} func - 呼び出す書き込み関数。WriteFunction型のオブジェクトとして提供されます。 * @param {unknown[]} args - 関数を呼び出すための引数。WriteFunctionArg型のオブジェクトとして提供されます。 * @return {{ status: "success" | "reverted", transactionHash: `0x${string}` }} - 取引が成功した場合は"success"、取引がリバートされた場合は"reverted"を返します。transactionHashは取引のハッシュです。 */ const res = await hatsSignerGateClient.callInstanceWriteFunction({ account, type, instance, func, args, });
-
getInstanceParameters
HSGまたはMHSGインスタンスのライブパラメータを取得します。
/** * @param {Address} instance - インスタンスのアドレス。 * @return {{ label: string, value: unknown, solidityType: string, displayType: string }[]} - 各パラメータの情報を含むオブジェクトの配列。labelはパラメータの名前や説明、valueはインスタンスコントラクトから返されたパラメータの値、solidityTypeはパラメータのSolidity型、displayTypeはUIで適切なコンポーネントをレンダリングするためのタイプです。 */ const params = await hatsSignerGateClient.getInstanceParameters(instance);
-
-
HSGおよびMHSGのメタデータオブジェクトには、それぞれのABI、書き込み関数、および各関数に関連するメタデータやカスタムロールが含まれています。
-
getMetadata
HSGまたはMHSGのメタデータオブジェクトを取得します。
/** * @param {HsgType} type - "HSG" または "MHSG"。 * @return {HsgMetadata} - HsgMetadata型のオブジェクト。 */ const metadata = await hatsSignerGateClient.getMetadata(type);
-
-
-
HsgMetadata
HSGまたはMHSGのメタデータオブジェクトを表します。
{ customRoles: Role[]; // HSG/MHSGカスタムロール writeFunctions: WriteFunction[]; // HSG/MHSG書き込み関数 abi: Abi; // HSG/MHSG ABI }
-
Role
カスタムHSG/MHSGロール。各ロールには帽子が関連付けられており、その帽子の着用者がコントラクトの特定の関数を呼び出す権限を持ちます。
{ id: string; // ロールのID name: string; // ロールの名前 criteria: string; // ロールの帽子を取得するために使用されるコントラクト関数の名前 hatAdminsFallback?: boolean; // 'true' の場合、ロールのcriteria関数がゼロを返すとき、そのロールはターゲット帽子の管理者に付与されます。 }
-
WriteFunction
HSG/MHSG書き込み関数。各書き込み関数は、ロールの着用者がコントラクトの関数を呼び出す権限を持つロールに関連付けられています。
{ roles: string[]; // 関数を呼び出す権限を持つロールのID functionName: string; // コントラクト内の関数名 label: string; // エンドユーザーに表示する関数名 description: string; // エンドユーザーに表示する関数の説明 primary?: boolean; // 'true' の場合、この関数は関連するロールの主要関数です。フロントエンドは、この情報を使用して各ロールの関数をより目立たせて表示できます。 args: WriteFunctionArg[]; // 関数の引数 }
-
WriteFunctionArg
HSG/MHSG書き込み関数の引数。
{ name: string; // 引数の名前 description: string; // 引数の説明 type: string; // 引数のSolidity型、例:'uint256' displayType: string; // 引数に適したUIコンポーネントを生成するためのフィールド optional?: boolean; // 'true' に設定すると、この入力はオプションであることを示します。 }
-
-
Hats Account SDK
HatsAccountは、Hats Protocolの各ハットにスマートコントラクトアカウントを提供します。
各ハットには、ERC6551規格に準拠し、ERC6551Registryファクトリーを介してデプロイされる複数のHatsAccountのバリエーションが存在します。
HatsAccountにより、各ハットは以下の機能を持つことができます:
- ETH、ERC20、ERC721、ERC1155トークンの送信
- マルチシグの署名者として、ERC1271互換のメッセージに署名
- DAOのメンバーになり、提案を行ったり、投票したりする(例:Moloch DAO)
- 他のコントラクトの関数を呼び出す
- okenboundのサンドボックスコンセプトを利用して、他のコントラクトに
Delegatecall
- アドレスベースのオンチェーンアクセス制御スキームにおける権限の付与
最初と最後を除くこれらのアクションはすべて、ハットの着用者によって行われ、セキュリティモデルはHatsAccountのバリエーションによって決まります。
このSDKは、Hats Accountインスタンスの作成や操作を行うためのオープンソースのJavaScriptクライアントであり、ブラウザとNode.jsの両方で動作するように設計されています。
1 of N Hats Account
HatsAccount1ofN は、典型的な1-of-nセキュリティモデルを反映したHatsAccountの一つです。
このモデルでは、HatsAccount1ofNインスタンスの帽子をかぶるどの個人でも、そのHatsAccountを完全にコントロールすることができます。複数の個人が同じ帽子をかぶっている場合、それぞれが独立して完全なコントロール権を持ちます。
HatsProtocol版のマルチシグって感じですかね。
-
Hats Account SDKを使う時はまず以下のライブラリをインストールします。
yarn add @hatsprotocol/hats-account-sdk viem
クライアントインスタンスは以下のように生成すれば良いみたいです!!
これまでのSDKと同じですね!
import { HatsAccount1ofNClient } from "@hatsprotocol/hats-account-sdk"; const hatsAccount1ofNClient = new HatsAccount1ofNClient({ publicClient, walletClient, });
-
-
createAccount
1 of N Hats Account の新しいインスタンスを作成します。
/** * @param {Account | Address} account - Viem アカウント(JSON-RPC アカウントの場合は Address、それ以外のタイプの場合は Account)。 * @param {bigint} hatId - アカウントを作成する対象の帽子ID。 * @param {bigint} salt - 任意の数値("ソルト"として使用)。 * @return {Object} - 作成結果のオブジェクト。 */ const createHatsAccountResult = await hatsAccount1ofNClient.createAccount({ account, hatId, salt, });
-
predictAccountAddress
1 of N Hats Account インスタンスのアドレスを予測します。
/** * @param {bigint} hatId - アカウントを作成する対象の帽子ID。 * @param {bigint} salt - 任意の数値("ソルト"として使用)。 * @return {string} - 予測されるアカウントアドレス。 */ const predictedAccount = await hatsAccount1ofNClient.predictAccountAddress({ hatId, salt, });
-
-
Hats Account インスタンスから操作を実行します。これらの関数を呼び出せるのは、そのインスタンスの帽子を持つユーザーのみです。
-
execute
/** * @param {Account | Address} account - Viem アカウント(JSON-RPC アカウントの場合は Address、それ以外のタイプの場合は Account)。 * @param {Address} instance - Hats Account インスタンスのアドレス。 * @param {Operation} operation - 操作の実行データを含む Operation オブジェクト。 * @return {Object} - 実行結果のオブジェクト。 */ const executionResult = await hatsAccount1ofNClient.execute({ account, instance, operation, });
-
executeBatch
一連の操作を一括で実行します。
/** * @param {Account | Address} account - Viem アカウント(JSON-RPC アカウントの場合は Address、それ以外のタイプの場合は Account)。 * @param {Address} instance - Hats Account インスタンスのアドレス。 * @param {Operation[]} operations - 各操作の実行データを含む Operation オブジェクトの配列。 * @return {Object} - 実行結果のオブジェクト。 */ const executionResult = await hatsAccount1ofNClient.executeBatch({ account, instance, operations, });
-
-
-
Operation
Hats Account の操作に関する実行データ。
{ to: Address; // 操作のターゲットアドレス value: bigint; // ターゲットに送信されるEtherの値 data: Hex; // エンコードされた操作のコールデータ operation: OperationType; // 実行する操作の種類を示す OperationType の値 }
-
OperationType
実行する操作を示す列挙型。
{ Call, // 通常のコール操作 DelegateCall, // デリゲートコール操作 }
-
CreateAccountResult
アカウント作成の結果。
{ status: "success" | "reverted"; // 成功した場合は "success"、失敗した場合は "reverted" transactionHash: Address; // トランザクションのハッシュ値 newAccount: Address; // 新しく作成されたアカウントのアドレス }
-
ExecutionResult
Hats Account の実行結果。
{ status: "success" | "reverted"; // 成功した場合は "success"、失敗した場合は "reverted" transactionHash: Address; // トランザクションのハッシュ値 }
-
今回はここまでになります!!
読んでいただきありがとうございました!!
Discussion