Skip to content

Events & indexing

Everything an indexer needs. LootFi's own backend is built on exactly these events and nothing else, so what is described here is sufficient to reconstruct full protocol state from the chain alone.

Start block: 19419826. Earlier wastes time on empty ranges; later silently misses tokens. Addresses: Deployments.

Event signatures

LootSkins

solidity
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

event Detokenized(address indexed owner, uint256[] tokenIds);
event MinterUpdated(address indexed minter, bool allowed);
event BaseURIUpdated(string baseURI);
event WithdrawFeeUpdated(uint256 fee);
event FeesWithdrawn(address indexed to, uint256 amount);

LootMarket

solidity
event OrderFulfilled(
    bytes32 indexed orderHash,
    address indexed maker,
    address indexed taker,
    address collection,
    uint256 tokenId,
    address currency,
    uint256 price,
    uint256 royaltyAmount,
    uint256 platformFee
);
event OrderCancelled(address indexed maker, uint256 indexed nonce);
event MinimumNonceUpdated(address indexed maker, uint256 minimumNonce);
event CurrencyAllowed(address indexed currency, bool allowed);
event CollectionAllowed(address indexed collection, bool allowed);
event PlatformFeeUpdated(uint16 bps);
event FeeRecipientUpdated(address indexed recipient);

Detokenized.tokenIds is NOT indexed

It sits in the data field, deliberately. Indexing an array parameter stores only its hash, which would make the ids permanently unrecoverable. Consequences:

  • You cannot filter by a specific token id in a topic — decode the data.
  • Filter by owner (topic 1), then decode uint256[] from the data.

What each event means

EventMeaningUpdate
Transfer from 0x0MintToken exists; owner set
Transfer to 0x0BurnToken destroyed
Transfer otherwiseMoveNew owner. Invalidate any live order whose maker is no longer the owner.
DetokenizedWithdrawal startedAuthoritative burn record with the full id list
OrderFulfilledSale settledOrder → filled; a Transfer accompanies it
OrderCancelledOne nonce cancelledThat order is dead
MinimumNonceUpdatedBulk cancelEvery order from that maker below the watermark is dead
ApprovalForAll(_, market, false)Approval revokedThat maker's orders are unfillable until re-approved
PlatformFeeUpdated / FeeRecipientUpdatedFee changeRe-read parameters

Deriving order status from events alone

An order is dead if any of these hold, and none require contract calls:

  1. OrderFulfilled with its orderHash
  2. OrderCancelled(maker, nonce)
  3. MinimumNonceUpdated(maker, n) where n > nonce
  4. A Transfer moved the token away from maker
  5. ApprovalForAll(maker, market, false) and no per-token approval remains
  6. block.timestamp > expiry — time, not an event, so sweep for it

Items 4 and 5 are the ones naive indexers miss, and they are the common case: a seller who moves or unlists an item never emits a marketplace event at all.

Indexing correctly

Bound your ranges and persist a cursor.

ts
const CONFIRMATIONS = 3n;
const BATCH = 2000n;

let cursor = await loadCursor();                       // starts at 19419826n
const head = await publicClient.getBlockNumber();
const safeHead = head - CONFIRMATIONS;                 // never index into the tip

while (cursor < safeHead) {
  const to = cursor + BATCH > safeHead ? safeHead : cursor + BATCH;

  const logs = await publicClient.getLogs({
    address: [LOOT_SKINS, LOOT_MARKET],
    fromBlock: cursor + 1n,
    toBlock: to,
  });

  for (const log of logs) await handle(log);           // must be idempotent
  await saveCursor(to);                                // AFTER the handlers
}

Three properties matter:

  1. Stay behind the tip. Indexing the newest block means reorgs rewrite history under you. Three confirmations is a reasonable floor.
  2. Deduplicate on txHash-logIndex. That pair is unique per log and is the only key you need to make replays safe.
  3. Advance the cursor only after handlers succeed. If a handler throws, leave the cursor where it is and retry the range — an under-advanced cursor costs a replay, an over-advanced one loses data permanently.

Decoding Detokenized

ts
const logs = await publicClient.getLogs({
  address: LOOT_SKINS,
  event: parseAbiItem("event Detokenized(address indexed owner, uint256[] tokenIds)"),
  args: { owner },                 // topic filter works on owner only
  fromBlock, toBlock,
});

for (const log of logs) {
  const { owner, tokenIds } = log.args;   // tokenIds decoded from data
}

Reconstructing full state

Everything below is derivable from logs alone:

  • Which tokens exist — mints minus burns, from Transfer.
  • Who owns what — fold Transfer in block order.
  • Sale history and volumeOrderFulfilled, with royalty and fee already broken out so seller proceeds need no state read.
  • Which listings are live — you also need the off-chain signatures, which are not on-chain. You can only prove an order is dead, never that one exists.
  • Item stats — from tokenURI, not events. See Metadata format.

LootFi is not affiliated with, endorsed by, or sponsored by Valve Corporation. Counter-Strike and Steam are trademarks of Valve Corporation.