初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
ERCについてひたすらまとめたり、スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!
https://twitter.com/cardene777
https://chaldene.net/
https://qiita.com/cardene
https://zenn.dev/heku
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
また、これからも情報を見逃したくないという方はぜひ以下の購読ボタンを教えてください。
更新があった時、登録しているメールアドレス宛に通知が飛ぶようになります。
今回はAptosのオブジェクトについてまとめていきます。
Aptosでは、Move言語というプログラミング言語を使用してコントラクトを作成します。
この時オブジェクトという仕組みは理解しておく必要があるため、この記事で学んでいってください。
以下のドキュメントをベースにまとめていきます。
https://aptos.dev/standards/aptos-object/
オブジェクト
Moveのオブジェクトモデルは、複雑な型を単一のアドレス内に格納された一連のリソースとして表現することを可能にします。
また、リソースの細かな制御と所有権管理を可能にする豊富な機能モデルを提供します。
オブジェクトモデルは、NFTやトークンなどの複雑なデータ構造を柔軟かつ効率的に表現するために設計されています。
例えば、NFTを実装する場合、以下のようにリソースを組み合わせることができます。
Tokenリソース
NFTの共通データ(名前、シンボル、総供給量など)を格納します。
これらのデータは、すべてのNFTインスタンスで共有されます。
ObjectCoreリソース
NFTのオブジェクト固有のデータ(所有者のアドレス、転送可能性など)を格納します。
このリソースは、オブジェクトの基本的な機能を提供し、イベントストリームの作成に必要なデータも保持します。
カスタムリソース
NFTの追加機能を実装するために、必要に応じてカスタムリソースを定義できます。
例えば、ゲーム内のプレイヤーを表すNFTの場合、
Player
リソースを定義して、プレイヤー固有のデータ(レベル、スコアなど)を格納することができます。
これらのリソースを組み合わせることで、NFTを柔軟に設計できます。Token
リソースは共通データを提供し、ObjectCore
リソースはオブジェクトの基本機能を提供します。
さらに、カスタムリソースを追加することで、NFTに特化した機能を実装できます。
例えば、Player
オブジェクトを定義する場合、以下のようなリソース構成が考えられます。
Tokenリソース
プレイヤーNFTの共通データ(名前、シンボルなど)を格納。
ObjectCoreリソース
プレイヤーNFTの所有者情報や転送可能性を管理。
Playerリソース
プレイヤー固有のデータ(レベル、スコアなど)を格納。
この構成により、Player
オブジェクトはNFTとしての基本機能を持ちつつ、ゲーム内のプレイヤーとしての特殊な機能も実装できます。
オブジェクトモデルを使用することで、データの関心事を分離し、モジュール性と再利用性を高めることができます。
これにより、複雑なデータ構造を持つアプリケーションを柔軟かつ効率的に開発できます。
オブジェクトモデルの主な利点
複雑なデータ構造を単一のエンティティとして管理できる。
リソースレベルでのきめ細かなアクセス制御が可能。
オブジェクトの所有権を明示的に管理できる。
NFTやトークンなどの資産をオブジェクトとして表現できる。
オブジェクトに特化したカスタムリソースを追加できる。
オブジェクトモデルを活用することで、Moveプログラミング言語において、より柔軟性が高く、セキュアで、機能豊富なスマートコントラクトを開発することができます。オブジェクトモデルは、複雑なデータ構造と所有権管理を必要とするアプリケーションに特に適しています。
アカウントリソースモデル
既存のAptosデータモデルでは、Move内のストア機能の使用が重視されています。
ストアにより、任意の構造体がオンチェーンに格納された別の構造体内に存在できるようになります。
その結果、データはどの構造体内でも、どのアドレスでも存在できます。
これは大きな柔軟性を提供しますが、多くの制限があります。
データへのアクセスが保証されない可能性がある
例えば、そのデータが予想しないユーザー定義のリソース内に配置されることがあります(例:ユーザー定義のストアに入れられたNFTをクリエイターがburnしようとする)。
これは、データのユーザーとクリエイターの両方にとって混乱を招く可能性がある。
any
異なるタイプのデータを単一のデータ構造(マップ、ベクターなど)にany
を介して格納できるが、複雑なデータタイプではany
を使用するとMove内で追加のコストがかかります。
これは、各アクセスで逆シリアル化が必要だからである。また、API開発者が特定のany
フィールドがそれが表すタイプを変更すると期待している場合、混乱を招く可能性があります。
シリアル化とは、オブジェクトやデータ構造を、保存や転送に適したフォーマット(バイト列など)に変換することです。逆シリアル化(デシリアライゼーション)は、そのシリアル化されたデータを元のオブジェクトやデータ構造に復元するプロセスです。
例えば、あるプログラミング言語で定義されたオブジェクトをファイルに保存する際、オブジェクトをバイト列に変換(シリアル化)して保存します。後でそのオブジェクトを再利用する際には、バイト列からオブジェクトを復元(逆シリアル化)する必要があります。
逆シリアル化は、以下の手順が行われます。
シリアル化されたデータ(バイト列)を読み取る。
データの型情報を解析し、元のデータ構造を特定する。
型情報に基づいて、バイト列からデータを取り出し、オブジェクトやデータ構造を再構築する。
逆シリアル化は、データの保存や転送において重要な役割を果たします。ただし、以下のような注意点があります。
逆シリアル化には一定のコストがかかる。特に、大量のデータを頻繁に逆シリアル化する場合、パフォーマンスに影響を与える可能性がある。
シリアル化されたデータと逆シリアル化するプログラムのバージョンが一致していないと、データの復元に失敗する可能性がある。
シリアル化されたデータが改ざんされている場合、逆シリアル化が失敗したり、予期しないデータが復元されたりする可能性がある。
Move言語では、
any
型を使用して異なる型のデータを単一のデータ構造に格納できます。ただし、any
型を使用する場合、データへのアクセス時に逆シリアル化が必要になるため、パフォーマンスに影響を与える可能性があります。オブジェクトモデルでは、データの型情報を明示的に管理することで、逆シリアル化のコストを最小限に抑えることを目指しています。これにより、パフォーマンスの向上とデータの整合性の確保が期待できます。
リソースアカウント
リソースアカウントは、独自のストレージを持ち、そのストレージ内のリソースを管理することができます。
これにより、リソースアカウントは、それ自体が所有するデータに対して高い制御力を持つことができます。
ただし、リソースアカウントをオブジェクトの管理に使用する場合、以下のような非効率性が発生します。
オブジェクトごとにリソースアカウントを作成する必要がある
各オブジェクトに個別のリソースアカウントを作成する必要があるため、アカウントの数が多くなり、管理が複雑になる可能性があります。
リソースアカウント間でのデータのやり取りが複雑になる
オブジェクト間の関係性を表現するために、リソースアカウント間でデータをやり取りする必要があります。
これは、データの整合性を保つために複雑なロジックが必要になります。
リソースグループの機能が活用されない
リソースグループは、関連するリソースを1つのユニットとして管理するための機能です。
ただし、リソースアカウントを使用する場合、各オブジェクトが独自のアカウントを持つため、リソースグループの利点が活かされません。
オブジェクトモデルでは、これらの非効率性を解消するために、以下のようなアプローチを取っています。
オブジェクトを単一のアドレスに関連付ける
すべてのオブジェクトを単一のアドレス(オブジェクトアドレス)に関連付けることで、オブジェクトの管理を簡素化します。
リソースグループを活用
オブジェクトに関連するリソースをリソースグループとして管理することで、データの整合性を保ちつつ、効率的なデータ管理を実現します。
オブジェクト間の関係性を明示的に定義
オブジェクト間の関係性を明示的に定義することで、データのやり取りを簡素化し、整合性を保ちます。
オブジェクトモデルは、リソースアカウントの利点(データの自律性)を継承しつつ、オブジェクト管理に特化した効率的な仕組みを提供します。
これにより、複雑なデータ構造を持つアプリケーションを、より簡潔かつ効率的に開発できるようになります。
再帰的なデータ構造
Moveは現在、再帰的なデータ構造を禁止しているため、データを再帰的に構成することはできません。
さらに、経験によると、真の再帰的データ構造はセキュリティの脆弱性につながる可能性があります。
参照
既存のデータをエントリ関数から簡単に参照できません。
例えば、文字列の検証をサポートするには、多くのコード行が必要です。
キーは多くのタイプで構成できるため、テーブルを直接作成しようとすると非現実的になり、エントリ関数内でサポートするために特化すると複雑になります。
イベント
イベントはデータから発行できず、データに関連付けられていないアカウントから発行される可能性があります。
転送ロジック
転送ロジックは、それぞれのモジュールで提供されるAPIに限定され、一般的に、送信者と受信者の両方でリソースをロードする必要があり、不必要なコストのオーバーヘッドが追加されます。
Objectモデルは、既存のAptosデータモデルの制限に対処し、より効率的で柔軟性の高いデータ管理を可能にするために設計されています。
Objectモデルを使用することで、データの自律性、再利用性、コンポーザビリティを向上させ、セキュアで拡張性の高いスマートコントラクトアプリケーションを開発できます。
オブジェクトの構造
オブジェクトは、ObjectGroup
リソースグループ内に格納されます。
これにより、オブジェクト内の他のリソースを同じ場所に配置することで、データの局所性とデータコストの節約を実現できます。
ただし、オブジェクト内のすべてのリソースをObjectGroup
内に配置する必要はなく、オブジェクトの開発者がデータレイアウトを決定します。
オブジェクトリソースグループ
オブジェクトは、単一のアドレス内に格納されるリソースのコンテナです。
これらのリソースは、通常、一緒にアクセスされる関連データを表し、データの局所性とコスト削減のために単一のアドレス内に格納する必要があります。
オブジェクトを作成すると、デフォルトでObjectGroup
リソースグループが付与されます。
#[resource_group(scope = global)]
struct ObjectGroup { }
各オブジェクトには、基本的なプロパティを持つObjectCore
リソースも存在します。
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct ObjectCore has key {
/// グローバルに一意なオブジェクトを保証し、イベントストリームを作成するためにguidで使用される
guid_creation_num: u64,
/// このオブジェクトを所有するアドレス(オブジェクトまたはアカウント)
owner: address,
/// オブジェクトの転送は一般的な操作であり、これにより転送の無効化と有効化が可能になる
/// TransferRefの使用をバイパスする
allow_ungated_transfer: bool,
/// 所有権の転送時に発行されるイベント
transfer_events: event::EventHandle<TransferEvent>,
}
オブジェクトを作成した後、作成者は追加のリソースでオブジェクトを拡張できます。
例えば、取引所は各流動性プールに対してオブジェクトを作成し、プールの流動性を追跡するためのリソースを追加できます。
#[resource_group_member(group = aptos_framework::object::ObjectGroup)]
struct LiquidityPool has key {
token_a: Object<FungibleAsset>,
token_b: Object<FungibleAsset>,
reserves_a: u128,
reserves_b: u128
}
上記のコードでは、token_a
とtoken_b
は他のオブジェクトへの参照です。
具体的には、Object<T>
は、T
リソースを含む特定のアドレスに格納されているオブジェクトへの参照です。
この例では、それらはファンジブル資産(FT)です。LiquidityPool
リソースは、ObjectGroup
リソースグループの一部です。
つまり、LiquidityPool
リソースは、ObjectCore
リソースと同じストレージスロットに格納されます。
これにより、データの読み書きに対するストレージとガスの効率が向上します。
LiquidityPool
リソースは、オブジェクトの構築時に追加できます。
use aptos_framework::object::{Self, Object};
use aptos_framework::fungible_asset::FungibleAsset;
public fun create_liquidity_pool(
token_a: Object<FungibleAsset>,
token_b: Object<FungibleAsset>,
reserves_a: u128,
reserves_b: u128
): Object<LiquidityPool> {
let exchange_signer = &get_exchange_signer();
let liquidity_pool_constructor_ref = &object::create_object_from_account(exchange_signer);
let liquidity_pool_signer = &object::generate_signer(liquidity_pool_constructor_ref);
move_to(liquidity_pool_signer, LiquidityPool {
token_a: token_a,
token_b: token_b,
reserves_a: reserves_a,
reserves_b: reserves_b
});
object::object_from_constructor_ref(liquidity_pool_constructor_ref)
}
取引所モジュールがExtendRef
を保存している場合、作成後に更にリソースを追加することもできます。
オブジェクトの構造は、関連データを効率的に管理し、データの局所性とコスト削減を実現するために設計されています。ObjectGroup
リソースグループを活用することで、関連リソースを同じ場所に配置し、ストレージとガスの効率を最適化できます。
また、オブジェクトの拡張性を維持しつつ、追加のリソースを柔軟に管理できます。
これにより、複雑なデータ構造を持つスマートコントラクトアプリケーションを効率的に開発できます。
オブジェクトのライフサイクル
オブジェクトの作成
オブジェクトは、object
モジュールで提供されているいくつかの異なる関数を使用して作成できます。
/// 新しい名前付きオブジェクトを作成し、ConstructorRefを返す。名前付きオブジェクトは、
/// それらを作成するために使用されるユーザー生成シードを知ることでグローバルに照会できる。
/// 名前付きオブジェクトは削除できない。
public fun create_named_object(creator: &signer, seed: vector<u8>): ConstructorRef;
/// トランザクションハッシュに基づいてランダムな一意のアドレスを生成することで、新しいオブジェクトを作成する。
/// 一意のアドレスは、sha3_256([transaction hash | auid counter | 0xFB])で計算される。
public fun create_object(owner_address: address): ConstructorRef
/// `create_object`と同じだが、作成されるオブジェクトは削除不可能になる。
public fun create_sticky_object(owner_address: address): ConstructorRef
これらの関数は、異なるスキーマでオブジェクトアドレスを生成します。
create_named_object
は、呼び出し元が提供するシードとクリエーターのアドレスからアドレスを生成します。
これは、グローバルに照会できる決定論的なアドレスです。
使用される式は、sha3(creator address + seed + 0xFD)
です。create_object
は、呼び出し元のアドレスと、このトランザクションのトランザクションハッシュとこのトランザクション固有のシーケンス番号をハッシュすることで生成されたauid
からアドレスを生成します。
使用される式は、sha3(creator address + auid counter + 0xFB)
です。create_sticky_object
は、呼び出し元のアドレスと、このトランザクションのトランザクションハッシュとこのトランザクション固有のシーケンス番号をハッシュすることで生成されたauid
からアドレスを生成します。
オブジェクトは削除不可能になります。
使用される式は、sha3(creator address + auid counter + 0xFB)
です。
名前付きオブジェクトは決定論的なアドレスを持つため、削除できないことに注意してください。
これは、悪意のあるユーザーが名前付きオブジェクトと同じシードでオブジェクトを作成し、それを削除することを防ぐためです。
オブジェクトの作成は、アプリケーションの要件に応じて適切な関数を選択することが重要です。
名前付きオブジェクトは、グローバルに照会可能な決定論的なアドレスを提供しますが、削除できません。
一方、create_object
とcreate_sticky_object
は、ランダムなアドレスを生成し、削除の可否を選択できます。
これらの関数を適切に使い分けることで、オブジェクトのライフサイクルを制御し、アプリケーションの要件に合わせたオブジェクト管理を実現できます。
オブジェクトの機能
オブジェクトの作成関数はすべて、格納できない一時的なConstructorRef
を返します。ConstructorRef
は、オブジェクトにリソースを追加することを可能にします。
また、ConstructorRef
は、オブジェクトの管理に使用される他の機能(「参照」)を生成するためにも使用できます。
/// DeleteRefを生成する。これは、グローバルストレージからオブジェクトを削除するために使用できる。
public fun generate_delete_ref(ref: &ConstructorRef): DeleteRef;
/// ExtendRefを生成する。これは、オブジェクトに新しいイベントとリソースを追加するために使用できる。
public fun generate_extend_ref(ref: &ConstructorRef): ExtendRef;
/// TransferRefを生成する。これは、オブジェクトの転送を管理するために使用できる。
public fun generate_transfer_ref(ref: &ConstructorRef): TransferRef;
/// ConstructorRefの署名者を作成する。
public fun generate_signer(ref: &ConstructorRef): signer;
これらの参照は格納され、オブジェクトを管理するために使用できます。
DeleteRef
は、オブジェクトを削除するために使用できます。
use aptos_framework::object::{Object, DeleteRef};
struct DeleteRefStore has key {
delete_ref: DeleteRef,
}
public fun delete_liquidity_pool(liquidity_pool: Object<LiquidityPool>) {
let liquidity_pool_address = object::object_address(liquidity_pool);
// 流動性プールオブジェクトに追加されたすべてのリソースを削除する。
let LiquidityPool {
token_a: _,
token_b: _,
reserves_a: _,
reserves_b: _
} = move_from<LiquidityPool>(liquidity_pool_address);
let DeleteRefStore { delete_ref } = move_from<DeleteRefStore>(liquidity_pool_address);
// オブジェクト自体を削除する。
object::delete_object(delete_ref);
}
ExtendRef
は、前のセクションのLiquidityPool
リソースのように、オブジェクトにリソースを追加するために使用できます。TransferRef
は、ungated_transfer_allowed = true
の場合に所有者による転送を無効にしたり、所有者が関与せずに強制的にオブジェクトを転送したりするために使用できます。
use aptos_framework::object::{Object, TransferRef};
struct TransferRefStore has key {
transfer_ref: TransferRef,
}
public fun disable_owner_transfer(liquidity_pool: Object<LiquidityPool>) {
let liquidity_pool_address = object::object_address(liquidity_pool);
let transfer_ref = &borrow_global_mut<TransferRefStore>(liquidity_pool_address).transfer_ref;
object::disable_ungated_transfer(transfer_ref);
}
public fun creator_transfer(liquidity_pool: Object<LiquidityPool>, new_owner: address) {
let liquidity_pool_address = object::object_address(liquidity_pool);
let transfer_ref = &borrow_global_mut<TransferRefStore>(liquidity_pool_address).transfer_ref;
object::transfer_with_ref(object::generate_linear_transfer_ref(transfer_ref), new_owner);
}
オブジェクト上にリソースが作成されると、作成者モジュールは参照なしでそれらを変更できます。
public entry fun modify_reserves(liquidity_pool: Object<LiquidityPool>) {
let liquidity_pool = &mut borrow_global_mut<LiquidityPool>(liquidity_pool);
liquidity_pool.reserves_a = liquidity_pool.reserves_a + 1000;
}
オブジェクトの機能(参照)は、オブジェクトの管理と操作に重要な役割を果たします。ConstructorRef
は、オブジェクトの作成時にのみ使用でき、他の参照を生成するために使用されます。DeleteRef
、ExtendRef
、TransferRef
は、それぞれオブジェクトの削除、拡張、転送を制御するために使用されます。
これらの参照を適切に使用することで、オブジェクトのライフサイクルを細かく制御し、セキュアで柔軟性の高いオブジェクト管理を実現できます。
また、作成者モジュールは、参照なしでオブジェクト内のリソースを直接変更することもできます。
オブジェクトの機能を活用することで、スマートコントラクトアプリケーションにおいて、オブジェクトの作成、削除、拡張、転送などの操作を安全かつ効率的に行うことができます。
これにより、複雑なビジネスロジックを持つアプリケーションの開発が容易になります。
オブジェクトの参照
オブジェクトの参照は、いつでも生成でき、オブジェクトまたはアカウントの一部としてリソース内に格納できます。
/// ConstructorRef内のアドレスを返す
public fun object_from_constructor_ref<T: key>(ref: &ConstructorRef): Object<T>;
Object<T>
は、オブジェクトのアドレスを囲む参照であり、参照が作成されたときにT
が存在することを保証します。
例えば、流動性プールオブジェクトに対してObject<LiquidityPool>
を作成できます。
存在しないT
でオブジェクト参照を作成しようとすると、実行時にエラーが発生します。
ただし、参照が作成され格納された後は、リソースT
やオブジェクト自体が削除されていないことを保証しません。
つまり、参照の作成時点ではT
の存在が保証されますが、その後の変更については保証されないということです。
以下に、Object<T>
の使用例を示します。
struct MyResource has key {
// オブジェクトの参照を格納するフィールド
liquidity_pool: Object<LiquidityPool>,
}
public fun store_object_reference(liquidity_pool: Object<LiquidityPool>) {
let my_resource = MyResource {
liquidity_pool: liquidity_pool,
};
// MyResourceをグローバルストレージに移動
move_to(signer::address_of(sender()), my_resource);
}
この例では、MyResource
構造体がObject<LiquidityPool>
型のフィールドを持っています。store_object_reference
関数は、Object<LiquidityPool>
を受け取り、それをMyResource
のフィールドに格納して、グローバルストレージに移動します。
Object<T>
は、オブジェクトへの参照を提供し、参照の作成時点でのT
の存在を保証します。
これにより、オブジェクトへの参照を安全に扱うことができます。
ただし、参照の作成後にオブジェクトやリソースが変更される可能性があるため、参照を使用する時は注意が必要です。
オブジェクトの参照を適切に管理することで、オブジェクト間の関係性を表現し、オブジェクトを介したデータのやり取りを行うことができます。
これは、複雑なデータ構造を持つアプリケーションにおいて重要な役割を果たします。
オブジェクトのイベント
オブジェクトには、デフォルトでtransfer_events
が付属しており、オブジェクトが転送されるとこれらのイベントが発行されます。
転送イベントは、ObjectCore
リソース内に格納されます。
さらに、アカウントのリソースと同様に、オブジェクトのリソース内にイベントを追加することもできます。object
モジュールには、オブジェクト用のイベントハンドルを作成するための以下の関数が用意されています。
/// オブジェクトのguidを作成する。通常、イベントに使用される
public fun create_guid(object: &signer): guid::GUID;
/// 新しいイベントハンドルを生成する
public fun new_event_handle<T: drop + store>(object: &signer): event::EventHandle<T>;
これらのイベントハンドルは、オブジェクトに追加されたカスタムリソース内に格納できます。
struct LiquidityPoolEventStore has key {
create_events: event::EventHandle<CreateLiquidtyPoolEvent>,
}
struct CreateLiquidtyPoolEvent {
token_a: address,
token_b: address,
reserves_a: u128,
reserves_b: u128,
}
public entry fun create_liquidity_pool_with_events() {
let exchange_signer = &get_exchange_signer();
let liquidity_pool_constructor_ref = &object::create_object_from_account(exchange_signer);
let liquidity_pool_signer = &object::generate_signer(liquidity_pool_constructor_ref);
let event_handle = object::new_event_handle<CreateLiquidtyPoolEvent>(liquidity_pool_signer);
event::emit<CreateLiquidtyPoolEvent>(event_handle, CreateLiquidtyPoolEvent {
token_a: token_a,
token_b: token_b,
reserves_a: reserves_a,
reserves_b: reserves_b,
});
let liquidity_pool = move_to(liquidity_pool_signer, LiquidityPool {
token_a: token_a,
token_b: token_b,
reserves_a: reserves_a,
reserves_b: reserves_b,
create_events: event_handle,
});
}
この例では、LiquidityPoolEventStore
という構造体を定義しています。
この構造体は、CreateLiquidtyPoolEvent
型のイベントハンドルを格納するフィールドcreate_events
を持っています。
CreateLiquidtyPoolEvent
は、流動性プールの作成に関連するイベントデータを表す構造体です。
これには、トークンAとトークンBのアドレス、およびそれぞれの初期準備金が含まれます。
create_liquidity_pool_with_events
関数は、イベントを発行しながら流動性プールを作成します。
以下の手順で処理が行われます。
get_exchange_signer
関数を呼び出して、取引所の署名者を取得します。object::create_object_from_account
関数を使用して、取引所アカウントから新しいオブジェクトを作成します。object::generate_signer
関数を使用して、作成されたオブジェクトの署名者を生成します。object::new_event_handle
関数を使用して、CreateLiquidtyPoolEvent
型の新しいイベントハンドルを作成します。event::emit
関数を使用して、CreateLiquidtyPoolEvent
イベントを発行します。
イベントデータには、トークンAとトークンBのアドレス、および初期準備金が含まれます。LiquidityPool
構造体を作成し、トークンAとトークンBのアドレス、初期準備金、およびイベントハンドルを格納します。move_to
関数を使用して、作成されたLiquidityPool
構造体をオブジェクトのストレージに移動します。
この例は、オブジェクトにカスタムイベントを追加する方法を示しています。object
モジュールの関数を使用して、オブジェクト固有のイベントハンドルを作成し、カスタムリソース内に格納することができます。
これにより、オブジェクトに関連するイベントを柔軟に定義し、発行することができます。
オブジェクトのイベントを適切に活用することで、オブジェクトの状態変化や重要なアクションを追跡し、アプリケーションのモニタリングや分析に役立てることができます。
また、イベントを通じてオブジェクトの変更をサブスクライブすることで、他のコンポーネントとの連携も可能になります。
最後に
今回はAptosの「オブジェクト」にまとめました。
これからも他のブロックチェーンやサービスについてもまとめていきます。
情報を見逃したくないという方はぜひ以下の購読ボタンを教えてください。
更新があった時、登録しているメールアドレス宛に通知が飛ぶようになります。
また、拡散もしてくれると嬉しいです🙇♂️