diff --git a/docs/01-getting-started/04-using-the-enjin-platform.md b/docs/01-getting-started/04-using-the-enjin-platform.md index 173f590..632970b 100644 --- a/docs/01-getting-started/04-using-the-enjin-platform.md +++ b/docs/01-getting-started/04-using-the-enjin-platform.md @@ -321,7 +321,7 @@ There are two ways to receive the transaction status and information: - [Receive Transaction Information Using the Platform User Interface](#receive-transaction-information-using-the-platform-user-interface) - [Receive Transaction Information Using the Enjin API / SDKs](#receive-transaction-information-using-the-enjin-api--sdks) -For real-time, push-based notifications, see [WebSocket Events](/03-api-reference/03-websocket-events.md) — planned, not yet available. +You can also receive real-time, push-based notifications over WebSocket instead of polling — see [WebSocket Events](/03-api-reference/03-websocket-events.md). ### Receive Transaction Information Using the Platform User Interface @@ -333,7 +333,7 @@ Once the state reaches `FINALIZED`, the transaction has been included on-chain. ### Receive Transaction Information Using the Enjin API & SDKs -To receive transaction information programmatically, use the `GetTransaction` query. Pass the `uuid` returned by `CreateTransaction` and request whichever fields you need — `state`, `extrinsicHash`, `action`, and so on. +To receive transaction information programmatically, use the `GetTransaction` query. Pass the `uuid` returned by `CreateTransaction` and request whichever fields you need — `state`, `action`, the `extrinsic` object (hash, on-chain outcome, emitted events), and so on. **Query:** @@ -349,7 +349,9 @@ query GetTransactionStatus($uuid: String!) { uuid action state - extrinsicHash + extrinsic { + hash + } createdAt updatedAt } @@ -369,7 +371,7 @@ query GetTransactionStatus($uuid: String!) { curl --location 'https://platform.enjin.io/graphql' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ' \ --d '{"query":"query { GetTransaction(network: CANARY, chain: MATRIX, uuid: \"12f22f91-82df-4b60-a90c-bf72b508a17c\") { uuid action state extrinsicHash } }"}' +-d '{"query":"query { GetTransaction(network: CANARY, chain: MATRIX, uuid: \"12f22f91-82df-4b60-a90c-bf72b508a17c\") { uuid action state extrinsic { hash } } }"}' ``` @@ -387,7 +389,9 @@ fetch('https://platform.enjin.io/graphql', { uuid action state - extrinsicHash + extrinsic { + hash + } } } `, @@ -409,7 +413,9 @@ axios.post('https://platform.enjin.io/graphql', { uuid action state - extrinsicHash + extrinsic { + hash + } } } `, @@ -434,7 +440,9 @@ query GetTransactionStatus($uuid: String!) { uuid action state - extrinsicHash + extrinsic { + hash + } } } ''' @@ -463,7 +471,9 @@ print(response.json()) "uuid": "12f22f91-82df-4b60-a90c-bf72b508a17c", "action": "MultiTokens.create_collection", "state": "FINALIZED", - "extrinsicHash": "0xd43b83...014b1365", + "extrinsic": { + "hash": "0xd43b83...014b1365" + }, "createdAt": "2026-05-07T16:36:33Z", "updatedAt": "2026-05-07T16:36:51Z" } diff --git a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md index 3835c1a..01adb39 100644 --- a/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md +++ b/docs/01-getting-started/05-using-enjin-api/01-how-to-use-graphql.md @@ -38,7 +38,7 @@ And would traditionally return all of the available data related to the call, li "data": { "MintToken": { "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", - "extrinsicHash": null, + "extrinsic": null, "action": "MultiTokens.mint", "state": "PENDING", "encodedData": "...", diff --git a/docs/01-getting-started/06-using-wallet-daemon.md b/docs/01-getting-started/06-using-wallet-daemon.md index 2c4cc33..df6cb95 100644 --- a/docs/01-getting-started/06-using-wallet-daemon.md +++ b/docs/01-getting-started/06-using-wallet-daemon.md @@ -16,7 +16,7 @@ The Enjin Wallet Daemon is an outbound-only signer for Enjin Platform transactio 2. **Poll for pending transactions -** The wallet daemon repeatedly polls the Enjin Platform API. It does not need inbound network access, so the machine holding your should not expose public ports. 3. **Sign and broadcast -** The daemon signs each pending transaction with your private key and sends the signed payload back to the platform. 4. **Confirm on-chain state -** Enjin Platform monitors the chain and updates the transaction state when the extrinsic is included or finalized. -5. **Read the result -** Poll [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) for the final `state` and `extrinsicHash`. To read emitted events, see [Working with Events](/05-enjin-platform/03-working-with-events.md). +5. **Read the result -** Poll [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) for the final `state` and the `extrinsic` object (hash, on-chain outcome). To read emitted events, see [Working with Events](/05-enjin-platform/03-working-with-events.md). :::info Network-agnostic One daemon instance can sign for Enjin Matrixchain, Enjin Relaychain, Canary Matrixchain, and Canary Relaychain. Each network maps to a different SS58 address derived from the same wallet. The daemon prints these addresses when it starts. diff --git a/docs/02-guides/01-platform/02-managing-users/01-sending-wallet-requests.md b/docs/02-guides/01-platform/02-managing-users/01-sending-wallet-requests.md index b7cbd47..4aec80d 100644 --- a/docs/02-guides/01-platform/02-managing-users/01-sending-wallet-requests.md +++ b/docs/02-guides/01-platform/02-managing-users/01-sending-wallet-requests.md @@ -211,7 +211,7 @@ The user stays in control: they can disconnect your application from their Enjin ### Step 3: Confirm the link -Run the `GetLinkedWallet` query with the same `idempotencyKey` you used when creating the linking code. Once the user has approved, it returns the linked wallet; until then (or if the user has disconnected), it returns `null`. +Run the `GetLinkedWallet` query with the same `idempotencyKey` you used when creating the linking code. Once the user has approved, it returns the linked wallet; until then (or if the user has disconnected), it returns `null`. To catch the approval the moment it happens instead of checking repeatedly, subscribe to the [`WalletLinked`](/03-api-reference/03-websocket-events.md#walletlinked) WebSocket event — it fires with this same `idempotencyKey` and the linked wallet's `publicKey`. @@ -358,8 +358,8 @@ print(response.json()) Store the returned `publicKey` against the user's record in your database — it identifies the wallet the user linked, and it's the account you'll target with transaction requests. The hex public key and the SS58-encoded address (`ef...`) are two representations of the same account, and address arguments like `signerAddress` accept either form. You can also call `GetLinkedWallet` with an `address` argument instead of `idempotencyKey` to check whether a specific wallet address is linked to your account. -:::note Polling the link state -There's no push notification for link state yet, so poll `GetLinkedWallet` (e.g. every few seconds while your "link your wallet" screen is open) until it returns data. A `null` response after a successful link means the user has since **disconnected** your application from their wallet app — treat the wallet as unlinked and offer to link again. +:::note Watching the link state +If you poll rather than subscribe to `WalletLinked`, query `GetLinkedWallet` every few seconds while your "link your wallet" screen is open. Either way, there's no event for the user later **disconnecting** your application from their wallet app, so a `null` `GetLinkedWallet` response after a successful link means exactly that — treat the wallet as unlinked and offer to link again. ::: ## Verifying Wallet Ownership {#verifying-wallet-ownership} @@ -747,8 +747,8 @@ The `state` moves through: If the user rejects the request or never responds, the transaction won't proceed. You can also withdraw a request that's still `PENDING` at any time with the [`CancelTransaction(uuid:)`](/03-api-reference/02-mutations/01-transaction-mutations.md#canceltransaction) mutation — for example, when the in-game offer that triggered it expires — which marks the transaction `ABANDONED`. -:::note Polling the transaction state -Real-time push notifications for transaction state aren't covered in the docs yet, so poll `GetTransaction` while a request is outstanding, the same way you polled `GetLinkedWallet` during linking. +:::note Watching the transaction state +Subscribe to the [`TransactionStateChanged`](/03-api-reference/03-websocket-events.md#transactionstatechanged) WebSocket event to be notified of each state change in real time, or poll `GetTransaction` while a request is outstanding. ::: :::info Explore More Arguments diff --git a/docs/02-guides/01-platform/03-advanced-mechanics/07-hot-cold-inventories.md b/docs/02-guides/01-platform/03-advanced-mechanics/07-hot-cold-inventories.md index 0bedcec..3704657 100644 --- a/docs/02-guides/01-platform/03-advanced-mechanics/07-hot-cold-inventories.md +++ b/docs/02-guides/01-platform/03-advanced-mechanics/07-hot-cold-inventories.md @@ -121,7 +121,7 @@ query ConfirmMove { } ``` -See [Working with Events](/05-enjin-platform/03-working-with-events.md) for the full finalization-and-events workflow. (Real-time push events that remove the need to poll are [planned](/03-api-reference/03-websocket-events.md).) +See [Working with Events](/05-enjin-platform/03-working-with-events.md) for the full finalization-and-events workflow, or subscribe to real-time [WebSocket events](/03-api-reference/03-websocket-events.md) to remove the need to poll. For a **self-custodial** cold wallet, your server can't sign the melt — instead, the player approves it in their own Enjin Wallet app via a wallet request. diff --git a/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md b/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md index c8b3a99..f224eef 100644 --- a/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md +++ b/docs/02-guides/01-platform/04-software-development-kit/01-getting-started.md @@ -132,5 +132,5 @@ client.reset(); With an authenticated client you can start sending requests. See [GraphQL Requests](/02-guides/01-platform/04-software-development-kit/02-graphql-requests.md) to learn how to build queries and mutations, select the fields you want back, and handle responses. :::tip Real-time events -The Enjin Platform doesn't yet expose real-time WebSocket events. Until it does, the pattern for tracking a submitted transaction is to poll the `GetTransaction` query by its UUID until it reaches a final state — see the [Enjin Farmer server implementation breakdown](/02-guides/01-platform/05-enjin-farmer-sample-game/01-overview.md#server-implementation-breakdown) for a worked example. +To track a submitted transaction, subscribe to the platform's real-time [WebSocket events](/03-api-reference/03-websocket-events.md) (e.g. `TransactionStateChanged`), or poll the `GetTransaction` query by its UUID until it reaches a final state — see the [Enjin Farmer server implementation breakdown](/02-guides/01-platform/05-enjin-farmer-sample-game/01-overview.md#server-implementation-breakdown) for a worked polling example. ::: diff --git a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-overview.md b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-overview.md index 4101023..e53dc0e 100644 --- a/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-overview.md +++ b/docs/02-guides/01-platform/05-enjin-farmer-sample-game/01-overview.md @@ -32,7 +32,7 @@ The project consists of four main components that work together: Before you begin, please keep the following in mind: * **Demonstration Purpose:** This is a simplified example designed to showcase a basic integration. It is **not suitable for a production environment** as is. - * **Polling, not subscriptions:** The Enjin Platform API doesn't yet expose [WebSocket events](/03-api-reference/03-websocket-events.md), so after submitting a transaction the server polls the `GetTransaction` query until it finalizes. Real-time event streaming is planned; once available it can simplify listening for finalization and for tokens arriving from external sources like the marketplace. + * **Polling, not subscriptions:** To keep the sample simple, after submitting a transaction the server polls the `GetTransaction` query until it finalizes. A production integration can subscribe to the platform's real-time [WebSocket events](/03-api-reference/03-websocket-events.md) (e.g. `TransactionStateChanged`) instead of polling. * **Wallet Funding:** New managed wallets start empty. So they can pay the network fees for melting and transferring, this sample has the server automatically drip a small amount of cENJ (1 ENJ by default) from the daemon wallet to each new managed wallet. In a real-world application you'd typically use a [Fuel Tank](/02-guides/01-platform/02-managing-users/04-using-fuel-tanks.md) to subsidize transactions for all your users instead. * **On-chain actions aren't instant:** This sample melts and mints on-chain whenever items change hands, which takes seconds to finalize — fine for a farming demo, but unplayable for real-time action. For a production pattern that keeps item use instant while preserving on-chain ownership, see [Hot & Cold Inventories](/02-guides/01-platform/03-advanced-mechanics/07-hot-cold-inventories.md). @@ -189,7 +189,7 @@ On startup — before serving any requests — the server runs [`PrepareCollecti var resp = await _client.SendMutation(mutation); ``` -2. **Wait for finalization:** `CreateTransaction` returns a transaction UUID. The server then [polls `GetTransaction`](https://github.com/enjin/platform-sample-game-server/blob/64949d25394526ef478b81c06a5d1e36375e455e/Services/EnjinService.cs#L608) by that UUID until `State` is `FINALIZED` (it throws if the transaction ends up `FAILED`, `ABANDONED`, or `TIMEOUT`). The new collection's ID is then recovered by querying `GetCollections` and matching on the `name` attribute. In a real-world application you'd instead listen for the collection-creation event rather than query for it — see [WebSocket Events](/03-api-reference/03-websocket-events.md). +2. **Wait for finalization:** `CreateTransaction` returns a transaction UUID. The server then [polls `GetTransaction`](https://github.com/enjin/platform-sample-game-server/blob/64949d25394526ef478b81c06a5d1e36375e455e/Services/EnjinService.cs#L608) by that UUID until `State` is `FINALIZED` (it throws if the transaction ends up `FAILED`, `ABANDONED`, or `TIMEOUT`). The new collection's ID is then recovered by querying `GetCollections` and matching on the `name` attribute. In a real-world application you'd instead read the new collection's ID from the transaction's emitted events rather than query for it — see [Working with Events](/05-enjin-platform/03-working-with-events.md). 3. **Create resource tokens:** For each entry in `Enjin.ResourceTokens`, the server [checks whether the token already exists](https://github.com/enjin/platform-sample-game-server/blob/64949d25394526ef478b81c06a5d1e36375e455e/Services/EnjinService.cs#L200) with a `GetToken` query and, if not, [creates it](https://github.com/enjin/platform-sample-game-server/blob/64949d25394526ef478b81c06a5d1e36375e455e/Services/EnjinService.cs#L219) with a `CreateToken` input. diff --git a/docs/03-api-reference/01-queries/01-transactions-queries.md b/docs/03-api-reference/01-queries/01-transactions-queries.md index 48886ff..8ebf9b5 100644 --- a/docs/03-api-reference/01-queries/01-transactions-queries.md +++ b/docs/03-api-reference/01-queries/01-transactions-queries.md @@ -12,7 +12,7 @@ import TabItem from '@theme/TabItem'; `https://platform.enjin.io/graphql` ::: -Use these queries to look up the state of transactions you've submitted (`GetTransaction` / `GetTransactions`) and to read the underlying blocks and extrinsics they ended up in (`GetBlock` / `GetBlocks`, `GetExtrinsic` / `GetExtrinsics`). +Use these queries to look up the state of transactions you've submitted (`GetTransaction` / `GetTransactions`) and to read the underlying blocks and extrinsics they ended up in (`GetBlock` / `GetBlocks`, `GetExtrinsic` / `GetExtrinsics`) — including the on-chain [events](/05-enjin-platform/03-working-with-events.md) each extrinsic emitted. ## GetTransaction @@ -32,7 +32,11 @@ query GetTransaction { action state encodedData - extrinsicHash + extrinsic { + id + hash + success + } createdAt updatedAt } @@ -49,7 +53,11 @@ query GetTransaction { "action": "MultiTokens.create_collection", "state": "FINALIZED", "encodedData": "0x280dc56f04985e66eaff2d50e6635942b20efb5690191c5da56adc3a2720e64b8bf534d050...", - "extrinsicHash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", + "extrinsic": { + "id": "402865-2", + "hash": "0xbafe459e8248b802f3aef98d2e4a695bbb238899edf40519b082366e3ff8b98f", + "success": true + }, "createdAt": "2026-05-13T05:20:52+00:00", "updatedAt": "2026-05-13T05:21:26+00:00" } @@ -61,6 +69,8 @@ query GetTransaction { See [the state argument](/03-api-reference/04-important-arguments.md#state) for the full list of transaction states. +The `extrinsic` object is `null` until the transaction has been included in a block and indexed by the platform. Once populated, it exposes the extrinsic's `id` (`blockNumber-index`), `hash`, on-chain `success` flag, `pallet` / `method`, `signer`, containing `block`, and the `events` it emitted — see [Working with Events](/05-enjin-platform/03-working-with-events.md) for reading event payloads. + ## GetTransactions Returns a paginated list of transactions. Pagination is cursor-based — pass the previous response's `nextCursor` to fetch the next page. You can filter by `uuids`, `idempotencyKeys`, or `extrinsicHashes`. @@ -78,7 +88,9 @@ query GetTransactions { uuid action state - extrinsicHash + extrinsic { + hash + } createdAt } perPage @@ -98,14 +110,14 @@ query GetTransactions { "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", "action": "MultiTokens.create_collection", "state": "FINALIZED", - "extrinsicHash": "0xbafe459e...", + "extrinsic": { "hash": "0xbafe459e..." }, "createdAt": "2026-05-13T05:20:52+00:00" }, { "uuid": "b80aed52-3171-50b3-a4d0-87366b771b02", "action": "MultiTokens.mint", "state": "BROADCAST", - "extrinsicHash": "0x12ab34cd...", + "extrinsic": null, "createdAt": "2026-05-13T05:24:11+00:00" } ], @@ -134,10 +146,14 @@ query GetBlock { ) { number hash - validator { - address - } + validator createdAt + extrinsics { + id + pallet + method + success + } } } ``` @@ -149,8 +165,16 @@ query GetBlock { "GetBlock": { "number": 402865, "hash": "0xf0b3cee1c36a99e24aaef7da06aec2ecd79a599c19ad4ae6fb4b40b3e497a322", - "validator": { "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, - "createdAt": "2026-05-13T05:20:48+00:00" + "validator": "0x60820310ea5b09bef944c50a7f3ae82166304dd637263b1ba7e0fa0bab1f3f7b", + "createdAt": "2026-05-13T05:20:48+00:00", + "extrinsics": [ + { + "id": "402865-2", + "pallet": "MultiTokens", + "method": "mint", + "success": true + } + ] } } } @@ -158,13 +182,11 @@ query GetBlock { -:::warning GetBlock is currently unavailable -`GetBlock` is temporarily disabled — every call returns an `Internal server error`. The platform team is aware of the issue and a fix is planned. For event-level lookups in the meantime, see [Working with Events](/05-enjin-platform/03-working-with-events.md). -::: +`validator` is the block validator's public key (hex). Each entry in `extrinsics` also exposes the [events](/05-enjin-platform/03-working-with-events.md) it emitted — see [GetExtrinsic](#getextrinsic) below for the full extrinsic shape. ## GetBlocks -Returns a list of blocks. Filter by `ids` (block numbers) or `hashes`. +Returns a list of blocks. Pass `ids` (block numbers) or `hashes` — one of the two is required. @@ -204,13 +226,9 @@ query GetBlocks { -:::warning Block.extrinsics and Block.events return null -The `extrinsics` and `events` sub-fields on `Block` are defined in the schema but currently return `null` on every request. For event-level lookups in the meantime, see [Working with Events](/05-enjin-platform/03-working-with-events.md). -::: - ## GetExtrinsic -Returns a single extrinsic by `hash`. +Returns a single extrinsic by `hash`, including the on-chain events it emitted — useful when you have an extrinsic hash (e.g. from a block explorer or an external system) rather than a platform transaction `uuid`. @@ -233,6 +251,13 @@ query GetExtrinsic { block { number } + events { + id + name + collectionId + tokenId + data + } } } ``` @@ -249,7 +274,16 @@ query GetExtrinsic { "success": true, "nonce": 132, "signer": { "address": "efRC9jw5LeZFqmaWBBDxZRTyaLP9dLAqixy32tSnqW9wCsb6y" }, - "block": { "number": 402865 } + "block": { "number": 402865 }, + "events": [ + { + "id": "1234567-2", + "name": "MultiTokens Minted", + "collectionId": "12345", + "tokenId": "12345-1", + "data": { "amount": "1" } + } + ] } } } @@ -257,13 +291,11 @@ query GetExtrinsic { -:::warning GetExtrinsic and GetExtrinsics are currently unavailable -Both queries are temporarily disabled and return `Internal server error`. Use the Subscan workaround described above until the platform fix lands. -::: +See [Working with Events](/05-enjin-platform/03-working-with-events.md) for what each `Event` field carries. ## GetExtrinsics -Returns a list of extrinsics by `hashes`. Same temporary outage as `GetExtrinsic`. +Returns a list of extrinsics by `hashes`. diff --git a/docs/03-api-reference/02-mutations/01-transaction-mutations.md b/docs/03-api-reference/02-mutations/01-transaction-mutations.md index 8cb9a76..de79730 100644 --- a/docs/03-api-reference/02-mutations/01-transaction-mutations.md +++ b/docs/03-api-reference/02-mutations/01-transaction-mutations.md @@ -43,7 +43,9 @@ mutation CreateTransaction { uuid action state - extrinsicHash + extrinsic { + hash + } } } ``` @@ -56,7 +58,7 @@ mutation CreateTransaction { "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", "action": "MultiTokens.create_token", "state": "PENDING", - "extrinsicHash": null + "extrinsic": null } } } @@ -144,7 +146,9 @@ mutation SignTransaction { ) { uuid state - extrinsicHash + extrinsic { + hash + } } } ``` @@ -156,7 +160,9 @@ mutation SignTransaction { "SignTransaction": { "uuid": "a90ded41-4262-40a2-95c0-98255b660bf1", "state": "BROADCAST", - "extrinsicHash": "0xa1b2c3..." + "extrinsic": { + "hash": "0xa1b2c3..." + } } } } diff --git a/docs/03-api-reference/03-websocket-events.md b/docs/03-api-reference/03-websocket-events.md index ed49af4..874acee 100644 --- a/docs/03-api-reference/03-websocket-events.md +++ b/docs/03-api-reference/03-websocket-events.md @@ -1,22 +1,362 @@ --- title: "WebSocket Events" slug: "websocket-events" -description: "Real-time event streaming via the Enjin Platform — planned, not yet available." +description: "Subscribe to real-time Enjin Platform events over WebSocket using the Pusher protocol." --- -:::warning Coming soon -Real-time event streaming is **planned** for the Enjin Platform but is **not yet available**. The exact API shape (subscription endpoint, channel structure, event payload) hasn't been finalized — this page will be filled in with the full reference once support ships. +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +After you submit a transaction, there are two ways to find out what happened to it. You can keep asking the API ("is it finalized yet?") — that's **polling**, and it works, but it wastes requests and adds delay. Or the platform can tell *you* the moment something happens — a transaction changing state, a managed wallet being created, a user linking their wallet. That's what this page covers. + +These notifications are delivered over a **WebSocket** — a connection your application opens once and keeps open, so the server can send it messages at any time. The Enjin Platform sends them through [Pusher](https://pusher.com/), a widely-used message-delivery service with a [documented protocol](https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/) and official client libraries for most languages. + +:::info You don't need a Pusher account +Pusher is just the delivery mechanism. You connect using Enjin's public app key below and authenticate with your regular Enjin Platform API token — there's nothing to sign up for or configure on Pusher's side. ::: -When it ships, this page will document how to subscribe to real-time events emitted by the platform. Typical use cases include: +## How it works + +Your account's events are broadcast on a **channel** — a named stream you subscribe to, e.g. `private-user.b1acd213-2b27-406e-a21b-02307b52eca0`. The `private-` prefix matters: only you are allowed to listen to it, so before Pusher lets you subscribe, you must prove to it that the Enjin Platform knows you. That proof is the `AuthenticatePusherSocket` mutation, and the whole flow is: + +1. **Connect** to the WebSocket URI (see [Connection details](#connection-details)). Pusher immediately sends back a `pusher:connection_established` message containing a `socket_id` — an identifier for this particular connection, e.g. `1234567.1234567`. +2. **Authenticate** by passing that `socket_id` to the [`AuthenticatePusherSocket`](#authenticatepushersocket) mutation, using your normal API token. The platform returns two things: your `channel` name, and an `auth` signature — a token that tells Pusher "the platform vouches for this connection". +3. **Subscribe** to the returned `channel`, presenting the `auth` signature. From this moment, events arrive over the open connection as they happen. + +The `auth` signature is tied to the `socket_id` it was issued for, and every new connection gets a new `socket_id` — so if the connection drops and you reconnect, you repeat steps 2 and 3. + +If that sounds like a lot of bookkeeping: it mostly isn't yours to do. A first-party Pusher library (like [pusher-js](https://github.com/pusher/pusher-js)) handles the connection, the re-authentication on reconnect, and the keepalive for you — you just tell it how to call the mutation. That's the recommended setup, and the quick start below is exactly that. Only if no Pusher library exists for your stack do you need to speak the protocol yourself — see [Subscribing over a raw WebSocket](#subscribing-over-a-raw-websocket). + +## Quick start + +A complete, runnable listener in Node.js. Install the official Pusher client: + +```bash +npm install pusher-js +``` + +Save this as `listen-for-events.mjs`, fill in your API token, and run it with `node listen-for-events.mjs`: + +```javascript +import Pusher from 'pusher-js'; + +const API_TOKEN = ''; + +// Exchanges a socket_id for subscription credentials, using the +// AuthenticatePusherSocket mutation documented below. +async function authenticateSocket(socketId) { + const response = await fetch('https://platform.enjin.io/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_TOKEN}` + }, + body: JSON.stringify({ + query: ` + mutation AuthenticatePusherSocket($id: String!) { + AuthenticatePusherSocket(id: $id) { + auth + channel + } + } + `, + variables: { id: socketId } + }), + }); + const { data } = await response.json(); + return data.AuthenticatePusherSocket; +} + +const pusher = new Pusher('8ab7ab8c519e8f59b635', { + cluster: 'us2', + channelAuthorization: { + // pusher-js calls this whenever the private channel needs + // (re-)authorizing — including automatically after a reconnect. + customHandler: ({ socketId }, callback) => { + authenticateSocket(socketId) + .then(({ auth }) => callback(null, { auth })) + .catch((error) => callback(error, null)); + } + } +}); + +pusher.connection.bind('connected', async () => { + // One extra call up front to learn the channel name for this account. + const { channel } = await authenticateSocket(pusher.connection.socket_id); + const subscription = pusher.subscribe(channel); + console.log(`Connected. Listening on ${channel}`); + subscription.bind_global((event, payload) => { + console.log(event, payload); + }); +}); +``` + +Leave it running and make something happen — for example, create a managed wallet with the [`CreateManagedWallet`](/03-api-reference/02-mutations/04-wallets-mutations.md#createmanagedwallet) mutation (or from the [Platform UI](https://platform.enjin.io)). Within a second or two you'll see: + +``` +Connected. Listening on private-user.b1acd213-2b27-406e-a21b-02307b52eca0 +ManagedWalletRequested { externalId: 'example123' } +ManagedWalletCreated { + externalId: 'example123', + publicKey: '0x8faddcca50c311c6eb3d04ecfedf2ae30c60686cfce8addf55a1013ef706db23' +} +``` + +The full list of events you can receive is in the [Platform events](#platform-events) reference below. + +:::warning Keep your API token server-side +`AuthenticatePusherSocket` requires your platform API token, so the socket should be opened from your backend (or the mutation proxied through it) — never ship the token to a browser or game client. +::: + +## Connection details {#connection-details} + +| Setting | Value | +| --- | --- | +| Pusher app key | `8ab7ab8c519e8f59b635` | +| Cluster | `us2` | +| WebSocket URI | `wss://ws-us2.pusher.com/app/8ab7ab8c519e8f59b635?protocol=7` | + +Pusher libraries take the app key and cluster (as in the quick start above); the full WebSocket URI is what you connect to when working without a library. + +## AuthenticatePusherSocket + +```graphql +AuthenticatePusherSocket(id: String!): PusherSocketAuth! +``` + +Takes the `socket_id` of a connected socket and returns the credentials for subscribing to your account's private event channel. The API token you authenticate the GraphQL request with determines which account's channel is authorized. + + + +```graphql +mutation AuthenticatePusherSocket($id: String!) { + AuthenticatePusherSocket(id: $id) { + auth + channel + } +} +``` + +**Variables:** + +```json +{ + "id": "1359883.3809896" +} +``` + + +```bash +curl --location 'https://platform.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer ' \ +-d '{"query":"mutation AuthenticatePusherSocket($id: String!) { AuthenticatePusherSocket(id: $id) { auth channel } }","variables":{"id":"1359883.3809896"}}' +``` + + +```javascript +fetch('https://platform.enjin.io/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + }, + body: JSON.stringify({ + query: ` + mutation AuthenticatePusherSocket($id: String!) { + AuthenticatePusherSocket(id: $id) { + auth + channel + } + } + `, + variables: { id: '1359883.3809896' } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.enjin.io/graphql', { + query: ` + mutation AuthenticatePusherSocket($id: String!) { + AuthenticatePusherSocket(id: $id) { + auth + channel + } + } + `, + variables: { id: '1359883.3809896' } +}, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +mutation AuthenticatePusherSocket($id: String!) { + AuthenticatePusherSocket(id: $id) { + auth + channel + } +} +''' + +variables = {'id': '1359883.3809896'} + +response = requests.post( + 'https://platform.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={ + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } +) +print(response.json()) +``` + + + +Response: + +```json +{ + "data": { + "AuthenticatePusherSocket": { + "auth": "8ab7ab8c519e8f59b635:c81898c22164955191f384677873977d2c8d64e270aae004cc576ee71944def3", + "channel": "private-user.b1acd213-2b27-406e-a21b-02307b52eca0" + } + } +} +``` + +## Subscribing over a raw WebSocket {#subscribing-over-a-raw-websocket} + +If a first-party Pusher library isn't available for your stack, you can speak the protocol directly — it's the same three steps from [How it works](#how-it-works), done by hand. Note that the `data` field of every Pusher protocol frame is a JSON-*encoded string*, so it needs a second parse. + +1. Connect to `wss://ws-us2.pusher.com/app/8ab7ab8c519e8f59b635?protocol=7`. The server sends: + + ```json + { + "event": "pusher:connection_established", + "data": "{\"socket_id\":\"1359883.3809896\",\"activity_timeout\":120}" + } + ``` + +2. Call `AuthenticatePusherSocket` with the `socket_id`, then send a subscribe frame with the returned values: + + ```json + { + "event": "pusher:subscribe", + "data": { + "channel": "private-user.b1acd213-2b27-406e-a21b-02307b52eca0", + "auth": "8ab7ab8c519e8f59b635:c81898c22164955191f384677873977d2c8d64e270aae004cc576ee71944def3" + } + } + ``` + +3. The server confirms with `pusher_internal:subscription_succeeded`, and from then on each platform event arrives as a frame: + + ```json + { + "event": "TransactionStateChanged", + "channel": "private-user.b1acd213-2b27-406e-a21b-02307b52eca0", + "data": "{\"uuid\":\"0af09287-30ff-43c9-a13d-75ad1ea714c3\",\"state\":\"FINALIZED\"}" + } + ``` + +Your client is also responsible for keepalive: if the connection is quiet for longer than the `activity_timeout` reported in `pusher:connection_established`, send `{"event":"pusher:ping","data":{}}` and expect a `pusher:pong` back (the server may likewise ping you). And remember that a reconnect is a new socket — repeat the authenticate-and-subscribe handshake every time. + +## Platform events + +Event payloads are deliberately lightweight: they carry just enough to tell you *what* changed, and you fetch whatever else you need with a follow-up GraphQL query. For example, when `TransactionStateChanged` reports `FINALIZED`, query [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) to read the on-chain outcome and emitted events — see [Working with Events](/05-enjin-platform/03-working-with-events.md) for that flow. + +| Event | Fires when | +| --- | --- | +| [`TransactionCreated`](#transactioncreated) | A transaction is created on the platform | +| [`TransactionStateChanged`](#transactionstatechanged) | A transaction moves to a new lifecycle state | +| [`DaemonConnectionStatusChanged`](#daemonconnectionstatuschanged) | Your Wallet Daemon's connection to the platform changes | +| [`ManagedWalletRequested`](#managedwalletrequested) | A managed wallet creation request is received | +| [`ManagedWalletCreated`](#managedwalletcreated) | A managed wallet's keypair has been derived | +| [`WalletLinked`](#walletlinked) | A user's wallet completes linking to your account | + +:::note Address format +Account addresses in event payloads are formatted as **public keys** (`0x...`), not SS58-encoded addresses. The platform accepts public keys anywhere an address argument is expected, so you can pass them straight back into queries and mutations. +::: + +### TransactionCreated {#transactioncreated} + +Fired when a transaction is created on your account — whether by your application, the Platform UI, or another API client. + +```json +{ + "uuid": "575209c5-6eda-4d8f-82c3-9a7f71249817" +} +``` + +Follow up with [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) for the transaction's action, state, and details. + +### TransactionStateChanged {#transactionstatechanged} + +Fired every time a transaction moves to a new lifecycle [`state`](/03-api-reference/04-important-arguments.md#state) — `PENDING` → `BROADCAST` → `FINALIZED`, or a terminal failure state. This replaces polling `GetTransaction` for state: drive your "pending → confirmed" UI straight off these events, and when the state reaches `FINALIZED`, fetch the emitted on-chain events as described in [Working with Events](/05-enjin-platform/03-working-with-events.md). + +```json +{ + "uuid": "0af09287-30ff-43c9-a13d-75ad1ea714c3", + "state": "FINALIZED" +} +``` + +### DaemonConnectionStatusChanged {#daemonconnectionstatuschanged} + +Fired when your project's [Wallet Daemon](/01-getting-started/06-using-wallet-daemon.md) connects to or disconnects from the platform. Useful for alerting: while the daemon is offline, transactions queue up unsigned. + +```json +{ + "status": "ONLINE" +} +``` + +### ManagedWalletRequested {#managedwalletrequested} + +Fired when a [`CreateManagedWallet`](/03-api-reference/02-mutations/04-wallets-mutations.md#createmanagedwallet) request is received, before the Wallet Daemon has derived the keypair. + +```json +{ + "externalId": "example123" +} +``` + +### ManagedWalletCreated {#managedwalletcreated} + +Fired once the Wallet Daemon has derived the managed wallet's keypair — the point at which the wallet is usable. The payload already carries the wallet's public key, so no `GetManagedWallet` lookup is needed. -- **Reading IDs that the chain assigns at execution time** — for example, picking up the new `listing_id` the moment a `createListing` transaction is finalized, or the new `collection_id` after a `createCollection`, without having to poll or look up Subscan. -- **Reacting to incoming token activity in real time** — unlocking an in-game item the instant a transfer to the player's wallet finalizes, or refreshing a marketplace UI the moment a bid lands on an active auction. -- **Driving transaction state UI without polling** — flipping a "Pending → Broadcast → Finalized" indicator off the platform's own state changes. +```json +{ + "externalId": "example123", + "publicKey": "0x8faddcca50c311c6eb3d04ecfedf2ae30c60686cfce8addf55a1013ef706db23" +} +``` -## Until then +### WalletLinked {#walletlinked} -- **Transaction state** — poll [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) until `state` becomes `FINALIZED`. The transaction's `extrinsicHash` is also exposed once it's broadcast. -- **On-chain events emitted by a transaction** (the new collection ID after `createCollection`, the listing ID after `createListing`, etc.) — open the transaction on Subscan via its `extrinsicHash` and read the **Events** tab. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for the full flow. +Fired when an end user completes linking their wallet to your account (see [Sending Wallet Requests](/02-guides/01-platform/02-managing-users/01-sending-wallet-requests.md) for the flow). The `idempotencyKey` identifies which [`CreateLinkingCode`](/03-api-reference/02-mutations/04-wallets-mutations.md#createlinkingcode) request was fulfilled, and `publicKey` is the wallet the user linked — the moment to associate that address with the user's record in your database, with no need to poll for the link. -An `events` field will also be added to the `Transaction` type so on-chain event payloads can be read alongside the rest of the transaction. Once it ships, this page and the `GetTransaction` reference will be updated with the full event shape and response examples. +```json +{ + "idempotencyKey": "287a471e-c868-418c-90a2-2d6f5e993ed4", + "publicKey": "0xd542de6771d0c6f9bc70600c94b6747f280cda654cdbe3e92a7f0c6e94c25fa3" +} +``` diff --git a/docs/03-api-reference/04-important-arguments.md b/docs/03-api-reference/04-important-arguments.md index dd651fe..1ac1057 100644 --- a/docs/03-api-reference/04-important-arguments.md +++ b/docs/03-api-reference/04-important-arguments.md @@ -34,7 +34,7 @@ A client-supplied key that prevents the same operation from being submitted twic ## extrinsicHash -The cryptographic hash of the signed extrinsic, populated once a transaction is broadcast to the chain. Use it to look up the transaction on a block explorer like Subscan, or to filter transactions via `GetTransactions(extrinsicHashes: [...])`. +The cryptographic hash of the signed extrinsic, populated once a transaction is broadcast to the chain. It's accepted as a lookup argument on `GetTransaction(extrinsicHash:)` / `GetTransactions(extrinsicHashes: [...])` and on `GetExtrinsic(hash:)`. On the `Transaction` type it's read through the `extrinsic` object — `extrinsic { hash }` — alongside the extrinsic's on-chain outcome and [emitted events](/05-enjin-platform/03-working-with-events.md). ## externalId @@ -122,7 +122,7 @@ The lifecycle state of a transaction, returned on the `Transaction` type. Possib - **`PENDING`** — the transaction has been created on the platform and is awaiting signing/broadcast. - **`BROADCAST`** — the signed transaction has been submitted to the blockchain. -- **`FINALIZED`** — the transaction has been included in a block. Check the on-chain extrinsic outcome (e.g. on Subscan) to confirm success vs. failure. +- **`FINALIZED`** — the transaction has been included in a block. Check `extrinsic { success }` to confirm the on-chain outcome, and `extrinsic { events }` for what it emitted — see [Working with Events](/05-enjin-platform/03-working-with-events.md). - **`FAILED`** — the transaction was rejected by the blockchain. - **`ABANDONED`** — the platform couldn't sign or send the transaction (typically encoding issues or insufficient funds). - **`TIMEOUT`** — the transaction was broadcast but not finalized within the timeout window. diff --git a/docs/05-enjin-platform/03-working-with-events.md b/docs/05-enjin-platform/03-working-with-events.md index 7e43158..9b1ef8a 100644 --- a/docs/05-enjin-platform/03-working-with-events.md +++ b/docs/05-enjin-platform/03-working-with-events.md @@ -1,26 +1,265 @@ --- title: "Working with Events" slug: "working-with-events" -description: "How to read on-chain events emitted by transactions on the Enjin Platform." +description: "How to read the on-chain events emitted by transactions through the Enjin Platform API." --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + In the blockchain ecosystem, "events" are the on-chain log entries emitted by extrinsics — they're how the chain reports what actually happened: token transfers, collection creation, marketplace listing IDs, balance changes, and so on. Monitoring them is essential for any application that needs to react to state changes — for example, unlocking an in-game item the moment the user receives the corresponding NFT. -:::warning Coming soon -Reading the on-chain events emitted by a transaction through the Enjin Platform API is **planned** but **not yet available**. An `events` field will be added to the `Transaction` type so you'll be able to read them alongside `state`, `extrinsicHash`, etc. The exact shape isn't finalized — this page will be updated with response examples once support ships. +## How events are exposed + +Once a transaction has been included in a block, the platform exposes the extrinsic that carried it — and every event that extrinsic emitted — through the `extrinsic` field on the `Transaction` type: + +```graphql +{ + extrinsic { # null until the transaction is included in a block + id # "blockNumber-extrinsicIndex", e.g. "11955161-2" + hash # the extrinsic hash + success # whether the extrinsic succeeded on-chain + events { + id # the event ID + name # human-readable name, e.g. "MultiTokens Collection Created" + collectionId # the related collection ID, when the event concerns one + tokenId # the related token ID (canonical "collectionId-tokenId" form) + data # trimmed JSON payload with the event's key values + } + } +} +``` + +A few things to know about the fields: + +- **`name`** is the pallet and event name with spaces — `MultiTokens Collection Created`, `MultiTokens Minted`, `Marketplace Listing Created`, `Balances Transfer`, and so on. Note that on-chain event names are conventionally written in dotted form (`MultiTokens.CollectionCreated`), including elsewhere in these docs — the platform's `name` field is the same event with the words spaced out, so match against the spaced form when filtering. +- **`collectionId` / `tokenId`** are filled in when the event relates to a specific collection or token, so you can match events to your assets without parsing `data`. +- **`data`** carries the event's key values as JSON — the IDs your application usually needs. For example, `MultiTokens Collection Created` carries `{"collectionId": "124587"}`, `MultiTokens Minted` carries `{"amount": "1"}`, and `Marketplace Listing Created` carries the full listing (`id`, `price`, `amount`, `type`). Events whose meaning is fully captured by the other fields have an empty `data` payload. +- The platform surfaces the events relevant to the action itself; low-level bookkeeping events (fee payments and similar) are not included. + +## Reading a transaction's events + +The flow after submitting any [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction) mutation is: + +1. **Wait** for the transaction to reach `FINALIZED` — either poll [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction), or subscribe to the [`TransactionStateChanged`](/03-api-reference/03-websocket-events.md#transactionstatechanged) WebSocket event to be notified the moment the state changes. While the transaction is still `PENDING` or awaiting inclusion in a block, `extrinsic` is `null`. +2. **Check** `extrinsic.success` to confirm the on-chain outcome (a failed extrinsic emits no events). +3. **Read** `extrinsic.events` and pick out the ones you care about by `name`. + + + +```graphql +query GetTransactionEvents($uuid: String!) { + GetTransaction( + network: CANARY + chain: MATRIX + uuid: $uuid + ) { + state + extrinsic { + hash + success + events { + id + name + collectionId + tokenId + data + } + } + } +} +``` + +**Variables:** + +```json +{ + "uuid": "06303d39-6ba9-4c81-8500-55bcca9e9512" +} +``` + + +```bash +curl --location 'https://platform.enjin.io/graphql' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer ' \ +-d '{"query":"query GetTransactionEvents($uuid: String!) { GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { state extrinsic { hash success events { id name collectionId tokenId data } } } }","variables":{"uuid":"06303d39-6ba9-4c81-8500-55bcca9e9512"}}' +``` + + +```javascript +fetch('https://platform.enjin.io/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + }, + body: JSON.stringify({ + query: ` + query GetTransactionEvents($uuid: String!) { + GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { + state + extrinsic { + hash + success + events { + id + name + collectionId + tokenId + data + } + } + } + } + `, + variables: { uuid: '06303d39-6ba9-4c81-8500-55bcca9e9512' } + }), +}) +.then(response => response.json()) +.then(data => console.log(data)); +``` + + +```javascript +const axios = require('axios'); + +axios.post('https://platform.enjin.io/graphql', { + query: ` + query GetTransactionEvents($uuid: String!) { + GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { + state + extrinsic { + hash + success + events { + id + name + collectionId + tokenId + data + } + } + } + } + `, + variables: { uuid: '06303d39-6ba9-4c81-8500-55bcca9e9512' } +}, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } +}) +.then(response => console.log(response.data)) +.catch(error => console.error(error)); +``` + + +```python +import requests + +query = ''' +query GetTransactionEvents($uuid: String!) { + GetTransaction(network: CANARY, chain: MATRIX, uuid: $uuid) { + state + extrinsic { + hash + success + events { + id + name + collectionId + tokenId + data + } + } + } +} +''' + +variables = {'uuid': '06303d39-6ba9-4c81-8500-55bcca9e9512'} + +response = requests.post( + 'https://platform.enjin.io/graphql', + json={'query': query, 'variables': variables}, + headers={ + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + } +) +print(response.json()) +``` + + + +## Example: picking up a new collection ID + +Some values only exist once the chain executes the transaction — the `collectionId` assigned by `createCollection` is the classic case. Events are how you read them. + +Submitting the collection creation returns a transaction in `PENDING` state, with no extrinsic yet: + +```json +{ + "data": { + "CreateTransaction": { + "uuid": "06303d39-6ba9-4c81-8500-55bcca9e9512", + "action": "MultiTokens.create_collection", + "state": "PENDING", + "extrinsic": null + } + } +} +``` + +Polling `GetTransactionEvents` (the query above) with that `uuid` eventually returns `FINALIZED`, and the new collection's ID appears in the `MultiTokens Collection Created` event — both on the event itself and in its `data` payload: + +```json +{ + "data": { + "GetTransaction": { + "state": "FINALIZED", + "extrinsic": { + "hash": "0xb3a2a733cf24df07fbbbffdf4447959ce66fd78ed46bc78e10c4991004b43b3b", + "success": true, + "events": [ + { + "id": "1234567-2", + "name": "MultiTokens Attribute Set", + "collectionId": "124587", + "tokenId": null, + "data": [] + }, + { + "id": "1234567-3", + "name": "MultiTokens Collection Created", + "collectionId": "124587", + "tokenId": null, + "data": { + "collectionId": "124587" + } + } + ] + } + } + } +} +``` + +The same pattern applies to every chain-assigned value: the token ID in `MultiTokens Token Created` after a mint into a new token, the listing in `Marketplace Listing Created` after [`createListing`](/03-api-reference/02-mutations/06-marketplace-mutations.md#createlisting), the group ID after [`createTokenGroup`](/03-api-reference/02-mutations/07-token-groups-mutations.md#createtokengroup), and so on. -Real-time push-based event streaming is also planned — see [WebSocket Events](/03-api-reference/03-websocket-events.md). +:::tip Reacting to transfers +Transfer-style events (`MultiTokens Transferred`, `MultiTokens Minted`, `Balances Transfer`) carry the related `collectionId` / `tokenId`, which is usually all you need to react in-game — e.g. unlocking the item that corresponds to the token the player just received. ::: -## Reading events today +## Other ways to look events up -Until the `Transaction.events` field ships, the workflow is: +Events hang off the extrinsic, so any query that returns an `Extrinsic` returns them too: -1. **Poll for finalization.** Call [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) until `state` is `FINALIZED`. The response also exposes `extrinsicHash` once the transaction has been broadcast. -2. **Read the events on Subscan.** Open the transaction by its `extrinsicHash` on the [Enjin Matrixchain Subscan explorer](https://matrix.subscan.io/). The **Events** tab there lists every event the extrinsic emitted, including IDs you'll commonly need (new `collection_id`, `token_id`, `listing_id`, etc.). +- [`GetExtrinsic(hash:)`](/03-api-reference/01-queries/01-transactions-queries.md#getextrinsic) — when you have an extrinsic hash rather than a platform transaction `uuid`. +- [`GetBlock` / `GetBlocks`](/03-api-reference/01-queries/01-transactions-queries.md#getblock) — read every extrinsic (and its events) in a given block. -The Enjin dashboard makes this one click — open the [Transactions page](https://platform.enjin.io/transactions), find your transaction, and click its **Extrinsic Hash** link to jump straight to the Subscan event view. +You can also follow your transactions in the [Platform UI](https://platform.enjin.io/transactions): the Transactions page shows each transaction's state and extrinsic hash as it moves on-chain. -:::note Address formats in event payloads -On-chain events represent account addresses as **hex public keys**, not the SS58-encoded `ef…` / `cx…` strings shown in most platform UIs. Use Subscan's [Account Format Transform tool](https://matrix.subscan.io/tools/format_transform) to convert between the two. +:::tip Skip the polling +The platform pushes `TransactionStateChanged` and other events to your application over WebSocket in real time, removing the need to poll — see [WebSocket Events](/03-api-reference/03-websocket-events.md) for the full reference. :::