# Event Schemas

When mx-notifier receives a `SaveBlock` frame from the node, it preprocesses all log events in the block into a flat list of `NormalizedEvent` structs. Each normalized event represents a single log entry emitted by a smart contract during block execution. The hub WebSocket and message broker publishers deliver these events to consumers.

***

## NormalizedEvent

| Field              | Type     | Description                                                                                                            |
| ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `address`          | bytes    | The bech32-decoded address of the contract that emitted the event                                                      |
| `identifier`       | bytes    | The event identifier as raw bytes (e.g., UTF-8 encoding of `"ESDTTransfer"`)                                           |
| `topics`           | \[bytes] | Ordered list of event topics. Typically 2–4 items. Contents vary by event type (see below).                            |
| `data`             | bytes    | Additional unstructured event data. May be empty.                                                                      |
| `original_tx_hash` | string   | Hex hash of the user-submitted transaction that ultimately caused this event                                           |
| `source_tx_hash`   | string   | Hex hash of the transaction or SCR that directly emitted this event. May differ from `original_tx_hash` in SCR chains. |
| `order`            | usize    | Position of this event within the block's full event list. Monotonically increasing per block.                         |

***

## Field notes

### `address`

The raw bytes of the contract address. To display as bech32 (e.g., `erd1qqq...`), apply MultiversX bech32 encoding with HRP `"erd"`.

```javascript
import { Address } from '@multiversx/sdk-core';

const addr = Address.fromBytes(event.address).toBech32();
// → "erd1qqqqqqqqqqqqqpgqx7wfpk..."
```

### `identifier`

Raw bytes of the event identifier string. In practice always valid UTF-8. To work with it as a string:

```javascript
const id = Buffer.from(event.identifier).toString('utf8');
// → "ESDTTransfer"
```

### `topics`

Each entry in `topics` is a raw byte array. The meaning of each position is defined by the contract and event type — see the per-event-type tables below. The hub WebSocket transmits topics as base64-encoded strings.

```javascript
// Decode a topics entry to a string (for token identifiers)
const tokenId = Buffer.from(event.topics[0], 'base64').toString('utf8');
// → "WEGLD-bd4d79"

// Decode a topics entry to a BigInt (for nonces and amounts)
const amount = BigInt('0x' + Buffer.from(event.topics[2], 'base64').toString('hex'));
```

### `original_tx_hash` vs `source_tx_hash`

MultiversX contracts can generate Smart Contract Results (SCRs) — asynchronous messages sent to other contracts. An SCR is a transaction-like entity with its own hash. When a contract call generates an SCR and that SCR triggers a second contract that emits a log event:

* `original_tx_hash` = hash of the user's original transaction (the root of the call chain)
* `source_tx_hash` = hash of the specific SCR or transaction that directly emitted this event

For simple transactions with no SCRs, both fields are equal.

{% hint style="info" %}
Use `original_tx_hash` to group all events from a single user action. Use `source_tx_hash` to trace the exact execution step that produced the event.
{% endhint %}

***

## Topics layout by event type

### ESDTTransfer (fungible token transfer)

| Position | Content             | Encoding                                          |
| -------- | ------------------- | ------------------------------------------------- |
| 0        | Token identifier    | UTF-8 bytes (e.g., `WEGLD-bd4d79`)                |
| 1        | Token nonce         | Big-endian uint64 bytes (always `0` for fungible) |
| 2        | Amount              | Big-endian big integer bytes                      |
| 3        | Destination address | Raw 32-byte address                               |

### ESDTNFTTransfer (NFT / SFT transfer)

| Position | Content             | Encoding                     |
| -------- | ------------------- | ---------------------------- |
| 0        | Token identifier    | UTF-8 bytes                  |
| 1        | Token nonce         | Big-endian uint64 bytes      |
| 2        | Amount              | Big-endian big integer bytes |
| 3        | Destination address | Raw 32-byte address          |

### ESDTNFTCreate (NFT mint)

| Position | Content                    | Encoding                     |
| -------- | -------------------------- | ---------------------------- |
| 0        | Token identifier           | UTF-8 bytes                  |
| 1        | Token nonce (newly minted) | Big-endian uint64 bytes      |
| 2        | Initial supply             | Big-endian big integer bytes |

### swapTokensFixedInput / swapTokensFixedOutput (DEX swap)

| Position | Content              | Encoding                     |
| -------- | -------------------- | ---------------------------- |
| 0        | Token-in identifier  | UTF-8 bytes                  |
| 1        | Token-in nonce       | Big-endian uint64 bytes      |
| 2        | Token-in amount      | Big-endian big integer bytes |
| 3        | Token-out identifier | UTF-8 bytes                  |

{% hint style="info" %}
DEX swap events may carry additional topics beyond position 3 (token-out nonce, token-out amount, caller address). The exact layout depends on the DEX contract version. Use the `data` field for extended swap metadata.
{% endhint %}

### addLiquidity / removeLiquidity

| Position | Content                 | Encoding                     |
| -------- | ----------------------- | ---------------------------- |
| 0        | First token identifier  | UTF-8 bytes                  |
| 1        | First token nonce       | Big-endian uint64 bytes      |
| 2        | First token amount      | Big-endian big integer bytes |
| 3        | Second token identifier | UTF-8 bytes                  |

***

## Decoding amounts and nonces

Token nonces and amounts are big-endian byte arrays with no fixed length (leading zero bytes are stripped). Decode them as arbitrary-precision integers:

```javascript
function decodeTopicBigInt(base64Topic) {
  const bytes = Buffer.from(base64Topic, 'base64');
  if (bytes.length === 0) return 0n;
  return BigInt('0x' + bytes.toString('hex'));
}

const nonce  = decodeTopicBigInt(event.topics[1]);  // → 42n
const amount = decodeTopicBigInt(event.topics[2]);  // → 1000000000000000000n
```

***

## Example: ESDTTransfer normalized event

The following shows a normalized event for a transfer of 1 WEGLD (`1 * 10^18` base units) from a DEX contract to a user address.

```json
{
  "address": "<32-byte contract address, base64>",
  "identifier": "RVNEVE5GVFRyYW5zZmVy",
  "topics": [
    "V0VHTEQtYmQ0ZDc5",
    "AA==",
    "De0B6B3A7640000",
    "<32-byte destination address, base64>"
  ],
  "data": "",
  "original_tx_hash": "a3f1c8e2d9b047630f4e2c5d8a91b7f0e6d2c4b8a0f3e7d1c9b5a2f8e4d6c0b3",
  "source_tx_hash": "a3f1c8e2d9b047630f4e2c5d8a91b7f0e6d2c4b8a0f3e7d1c9b5a2f8e4d6c0b3",
  "order": 17
}
```

Decoded field values:

| Field        | Decoded value                            |
| ------------ | ---------------------------------------- |
| `address`    | `erd1qqqqqqqqqqqqqpgq...` (DEX contract) |
| `identifier` | `ESDTTransfer`                           |
| `topics[0]`  | `WEGLD-bd4d79`                           |
| `topics[1]`  | `0` (nonce = 0 for fungible token)       |
| `topics[2]`  | `1000000000000000000` (1 WEGLD)          |
| `topics[3]`  | `erd1...` (recipient address)            |
| `order`      | 17th event in the block                  |

***

## Event ordering

The `order` field is a block-scoped monotonically increasing integer. Events with lower `order` values were emitted earlier in block execution. Within a single user transaction, all events — including those from SCR-triggered sub-calls — are contiguous in `order`.

Event order is stable across redelivery and retries. Consumers can combine `order` with the block nonce to build a globally ordered event log.
