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 d9fdee7..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 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/03-websocket-events.md b/docs/03-api-reference/03-websocket-events.md index 88b5744..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. -- **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`. -- **On-chain events emitted by a transaction** (the new collection ID after `createCollection`, the listing ID after `createListing`, etc.) — read them from the same `GetTransaction` response via `extrinsic { events }`. See [Working with Events](/05-enjin-platform/03-working-with-events.md) for the full flow and payload examples. +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. -Everything an event stream would push is already readable by polling — this page only concerns the delivery mechanism. +```json +{ + "idempotencyKey": "287a471e-c868-418c-90a2-2d6f5e993ed4", + "publicKey": "0xd542de6771d0c6f9bc70600c94b6747f280cda654cdbe3e92a7f0c6e94c25fa3" +} +``` diff --git a/docs/05-enjin-platform/03-working-with-events.md b/docs/05-enjin-platform/03-working-with-events.md index c20823f..9b1ef8a 100644 --- a/docs/05-enjin-platform/03-working-with-events.md +++ b/docs/05-enjin-platform/03-working-with-events.md @@ -41,7 +41,7 @@ A few things to know about the fields: The flow after submitting any [`CreateTransaction`](/03-api-reference/02-mutations/01-transaction-mutations.md#createtransaction) mutation is: -1. **Poll** [`GetTransaction(uuid:)`](/03-api-reference/01-queries/01-transactions-queries.md#gettransaction) until `state` is `FINALIZED`. While the transaction is still `PENDING` or awaiting inclusion in a block, `extrinsic` is `null`. +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`. @@ -260,6 +260,6 @@ Events hang off the extrinsic, so any query that returns an `Extrinsic` returns 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. -:::info Real-time event streaming -Push-based event delivery (WebSockets), which removes the need to poll, is planned but not yet available — see [WebSocket Events](/03-api-reference/03-websocket-events.md). +:::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. :::