본문으로 건너뛰기

이벤트 사용하기

Move code는 Sui에 저장된 object와 상호작용할 수 있다. 일부 애플리케이션은 object의 활동을 추적하고, NFT가 몇 번 mint되었는지 또는 smart contract가 생성한 SUI의 양을 기준으로 application을 업데이트하는 것처럼, 그 활동을 바탕으로 애플리케이션 안의 특정 workflow를 트리거하고 싶어 할 수 있다.

Move는 event를 발생시키는 방식을 통해 활동 모니터링을 지원한다. event를 발생시키는 과정은 3단계로 이루어진다:

  1. 추적하고 싶은 데이터를 포착하는 event struct를 Move module에 정의한다.

  2. 관련 action이 발생할 때 emit 함수를 사용해 event를 발생시킨다.

  3. 커스텀 인덱서를 사용하거나 network를 polling하여 event를 처리한다.

Defining event struct

event struct는 copydrop ability를 가진다. event 안에는 action에 대한 관련 데이터를 포착하는 데 필요한 field를 정의한다.

public struct TestEvent has copy, drop {
message: ascii::String,
value: u64,
}

Using the emit function

모니터링하려는 action이 발생할 때 event를 발생시키려면 sui::event::emit 함수를 사용한다:

public struct TestEvent has copy, drop {
message: ascii::String,
value: u64,
}

Processing events

Move code가 event를 발생시킨 뒤에는 이를 처리해야 한다. 접근 방식은 2가지가 있다:

  • 커스텀 인덱서: real-time 처리를 위해 checkpoint를 stream하고 event를 지속적으로 filter한다.
  • Polling: 발생한 event를 주기적으로 Sui network에서 질의한다. 이 접근 방식은 가져온 데이터를 저장할 database가 필요하다.

Event object structure

event를 처리할 때 각 event object는 다음 attribute를 포함한다:

  • id: transaction digest ID와 event sequence를 담은 JSON object.

  • packageId: event를 발생시키는 package의 object ID.

  • transactionModule: transaction을 수행하는 module.

  • sender: event를 트리거한 Sui network address.

  • type: 발생하는 event의 type.

  • parsedJson: event를 설명하는 JSON object.

  • bcs: Binary canonical serialization 값.

  • timestampMs: millisecond 단위 Unix epoch timestamp.

public fun lock<T: key + store>(obj: T, ctx: &mut TxContext): (Locked<T>, Key) {
let key = Key { id: object::new(ctx) };
let mut lock = Locked {
id: object::new(ctx),
key: object::id(&key),
};

event::emit(LockCreated {
lock_id: object::id(&lock),
key_id: object::id(&key),
creator: ctx.sender(),
item_id: object::id(&obj),
});

dof::add(&mut lock.id, LockedObjectKey {}, obj);

(lock, key)
}

Querying events with RPC

Sui RPC는 온체인 package를 질의하고 사용 가능한 event를 반환하는 queryEvents method를 제공한다. 예를 들어 다음 curl 명령은 Mainnet의 DeepBookV3 package에서 특정 type의 event를 질의한다:

$ curl -X POST https://fullnode.mainnet.sui.io:443 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_queryEvents",
"params": [
{
"MoveModule": {
"package": "0x158f2027f60c89bb91526d9bf08831d27f5a0fcb0f74e6698b9f0e1fb2be5d05",
"module": "deepbook_utils",
"type": "0xdee9::clob_v2::DepositAsset<0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN>"
}
},
null,
3,
false
]
}'
Click to open

Successful response

{
"jsonrpc": "2.0",
"result": {
"data": [
{
"id": {
"txDigest": "8NB8sXb4m9PJhCyLB7eVH4onqQWoFFzVUrqPoYUhcQe2",
"eventSeq": "0"
},
"packageId": "0x158f2027f60c89bb91526d9bf08831d27f5a0fcb0f74e6698b9f0e1fb2be5d05",
"transactionModule": "deepbook_utils",
"sender": "0x8b35e67a519fffa11a9c74f169228ff1aa085f3a3d57710af08baab8c02211b9",
"type": "0xdee9::clob_v2::WithdrawAsset<0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN>",
"parsedJson": {
"owner": "0x704c8c0d8052be7b5ca7174222a8980fb2ad3cd640f4482f931deb6436902627",
"pool_id": "0x7f526b1263c4b91b43c9e646419b5696f424de28dda3c1e6658cc0a54558baa7",
"quantity": "6956"
},
"bcs": "2szz6igTRuGmD7YATo8BEg81VLaei4od62wehadwMXYJv63UzJE16USL9pHFYBAGbwNkDYLCk53d45eFj3tEZK1vDGqtXcqH5US",
"timestampMs": "1691757698019"
},
{
"id": {
"txDigest": "8NB8sXb4m9PJhCyLB7eVH4onqQWoFFzVUrqPoYUhcQe2",
"eventSeq": "1"
},
"packageId": "0x158f2027f60c89bb91526d9bf08831d27f5a0fcb0f74e6698b9f0e1fb2be5d05",
"transactionModule": "deepbook_utils",
"sender": "0x8b35e67a519fffa11a9c74f169228ff1aa085f3a3d57710af08baab8c02211b9",
"type": "0xdee9::clob_v2::OrderFilled<0x2::sui::SUI, 0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN>",
"parsedJson": {
"base_asset_quantity_filled": "0",
"base_asset_quantity_remaining": "1532800000000",
"is_bid": false,
"maker_address": "0x78a1ff467e9c15b56caa0dedfcfbdfe47c0c385f28b05fdc120b2de188cc8736",
"maker_client_order_id": "1691757243084",
"maker_rebates": "0",
"order_id": "9223372036854839628",
"original_quantity": "1614700000000",
"pool_id": "0x7f526b1263c4b91b43c9e646419b5696f424de28dda3c1e6658cc0a54558baa7",
"price": "605100",
"taker_address": "0x704c8c0d8052be7b5ca7174222a8980fb2ad3cd640f4482f931deb6436902627",
"taker_client_order_id": "20082022",
"taker_commission": "0"
},
"bcs": "DcVGz85dWTLU4S33N7VYrhgbkm79ENhHVnp5kBfENEWEeMxHQuvsczg94teh6WHdYtwPqdEsPWdvSJ7ne5qiMxxn3kBm36KLyuuzHV1QdzF45GN8ZU1MDGU4XppiaqcMeRpPPiW8JpUDyeQoobKEV8fMqcyYpDq6KWtZ1WMoGvEDxFKDgFvW9Q7bt1JAzQehRkEKEDZ6dTwfiHw92QuFqczmZ5MKJLYzeysUsSw",
"timestampMs": "1691757698019"
},
{
"id": {
"txDigest": "8b3byDuRojHXqmSz16PsyzfdXJEY5nZBGTM23gMsMAY8",
"eventSeq": "0"
},
"packageId": "0x158f2027f60c89bb91526d9bf08831d27f5a0fcb0f74e6698b9f0e1fb2be5d05",
"transactionModule": "deepbook_utils",
"sender": "0x8b35e67a519fffa11a9c74f169228ff1aa085f3a3d57710af08baab8c02211b9",
"type": "0xdee9::clob_v2::OrderFilled<0x2::sui::SUI, 0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN>",
"parsedJson": {
"base_asset_quantity_filled": "700000000",
"base_asset_quantity_remaining": "0",
"is_bid": false,
"maker_address": "0x03b86e93d80b27763ee1fc2c37e285465dff835769de9462d9ad4ebcf46ac6df",
"maker_client_order_id": "20082022",
"maker_rebates": "634",
"order_id": "9223372036854839643",
"original_quantity": "1000000000",
"pool_id": "0x7f526b1263c4b91b43c9e646419b5696f424de28dda3c1e6658cc0a54558baa7",
"price": "604100",
"taker_address": "0x704c8c0d8052be7b5ca7174222a8980fb2ad3cd640f4482f931deb6436902627",
"taker_client_order_id": "20082022",
"taker_commission": "1058"
},
"bcs": "DcVGz85dWTLU4S33N7VYrhgbkm79ENhHVnp5kBfENEWEjN45pa9U3AkNhxfTRZbaHTQLugLBXttE32hpJKRsbrZGdryXMPmNA8EpHJnVcnYMXZmWXkNXvY1XjEYnAKU4BnhyJ9BQuxRJDXLA4DEu5uWEpWjLPD2ZHuxqHCn7GpUxvxJjHkKjr9jVVfeR6sN2uRhUXkThEDjCekrqaqwidkyXNmTzmZG4fre3eoZ",
"timestampMs": "1691758372427"
}
],
"nextCursor": {
"txDigest": "8b3byDuRojHXqmSz16PsyzfdXJEY5nZBGTM23gMsMAY8",
"eventSeq": "0"
},
"hasNextPage": true
},
"id": 1
}

특정 transaction의 event를 가져오기 위해 include: { events: true }와 함께 getTransaction method를 사용할 수 있다:

import { SuiGrpcClient } from '@mysten/sui/grpc';

const client = new SuiGrpcClient({
baseUrl: 'https://fullnode.mainnet.sui.io:443',
network: 'mainnet',
});

async function getEventsForTransaction(digest: string) {
const result = await client.getTransaction({
digest,
include: { events: true },
});

if (result.$kind === 'Transaction') {
return result.Transaction.events ?? [];
}
return [];
}

Querying events with GraphQL

⚙️Early-Stage Feature

This content describes an alpha/beta feature or service. These early stage features and services are in active development, so details are likely to change.

JSON RPC 대신 GraphQL을 사용해 event를 질의할 수 있다.

Click to open

Event connection

{
events(
filter: {type: "0x3164fcf73eb6b41ff3d2129346141bd68469964c2d95a5b1533e8d16e6ea6e13::Market::ChangePriceEvent<0x2::sui::SUI>"}
) {
nodes {
transactionModule {
name
package {
digest
}
}
sender {
address
}
timestamp
contents {
type {
repr
}
json
}
eventBcs
}
}
}

Querying events in Rust

GitHub의 Sui by Example repo에는 query_events 함수를 사용해 event를 질의하는 방법을 보여주는 code sample이 있다. PACKAGE_ID_CONST가 가리키는 package는 Mainnet에 존재하므로 Cargo를 사용해 이 code를 테스트할 수 있다. 이렇게 하려면 sui-by-example repo를 로컬에 clone하고 Example 05 directions를 따른다.

use sui_sdk::{rpc_types::EventFilter, types::Identifier, SuiClientBuilder};

const PACKAGE_ID_CONST: &str = "0x279525274aa623ef31a25ad90e3b99f27c8dbbad636a6454918855c81d625abc";

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let sui_mainnet = SuiClientBuilder::default()
.build("https://fullnode.mainnet.sui.io:443")
.await?;

let events = sui_mainnet
.event_api()
.query_events(
EventFilter::MoveModule {
package: PACKAGE_ID_CONST.parse()?,
module: Identifier::new("dev_trophy")?,
},
None,
None,
false,
)
.await?;

for event in events.data {
println!("Event: {:?}", event.parsed_json);
}

Ok(())
}

Filtering event queries

질의에서 반환되는 event를 filter하려면 다음 데이터 구조를 사용한다.

RPC

QueryDescriptionJSON-RPC parameter example
All모든 event{"All": []}
Any주어진 filter 중 어느 하나에서 발생한 event{"Any": SuiEventFilter[]}
Transaction지정한 transaction에서 발생한 event{"Transaction":"DGUe2TXiJdN3FI6MH1FwghYbiHw+NKu8Nh579zdFtUk="}
MoveModule지정한 Move module에서 발생한 event{"MoveModule":{"package":"<PACKAGE-ID>", "module":"nft"}}
MoveEventModule지정한 Move module에 정의되어 발생한 event{"MoveEventModule": {"package": "<DEFINING-PACKAGE-ID>", "module": "nft"}}
MoveEventTypeevent의 Move struct 이름{"MoveEventType":"::nft::MintNFTEvent"}
Sendersender address로 질의{"Sender":"0x008e9c621f4fdb210b873aab59a1e5bf32ddb1d33ee85eb069b348c234465106"}
TimeRange[start_time, end_time] 구간에 발생한 event를 반환{"TimeRange":{"startTime":1669039504014, "endTime":1669039604014}}

GraphQL

GraphQL로 질의한 event를 filter하려면 다음 workflow 중 하나를 사용한다.

Click to open

Filter events by sender using a GraphQL query

query ByTxSender {
events(
first: 1
filter: {
sender: "0xdff57c401e125a7e0e06606380560b459a179aacd08ed396d0162d57dbbdadfb"
}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
transactionModule {
name
}
contents {
type {
repr
}
json
}
sender {
address
}
timestamp
eventBcs
}
}
}

TypeScript SDK도 Sui GraphQL service와 상호작용하고 event를 filter하는 데 사용할 수 있다.

Click to open

Filter events using the TypeScript SDK

import { SuiGraphQLClient } from '@mysten/sui/graphql';
import { graphql } from '@mysten/sui/graphql/schema';

const gqlClient = new SuiGraphQLClient({
url: 'https://graphql.mainnet.sui.io/graphql',
network: 'mainnet',
});

const queryEventsByType = graphql(`
query EventsByType($eventType: String!, $first: Int) {
events(filter: { eventType: $eventType }, first: $first) {
nodes {
sendingModule {
name
package { address }
}
type { repr }
sender { address }
contents { json }
timestamp
}
pageInfo {
hasNextPage
endCursor
}
}
}
`);

async function getEventsByType(eventType: string, first: number = 10) {
const result = await gqlClient.query({
query: queryEventsByType,
variables: { eventType, first },
});

return result.data?.events?.nodes ?? [];
}