Appearance
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 decodeuint256[]from the data.
What each event means
| Event | Meaning | Update |
|---|---|---|
Transfer from 0x0 | Mint | Token exists; owner set |
Transfer to 0x0 | Burn | Token destroyed |
Transfer otherwise | Move | New owner. Invalidate any live order whose maker is no longer the owner. |
Detokenized | Withdrawal started | Authoritative burn record with the full id list |
OrderFulfilled | Sale settled | Order → filled; a Transfer accompanies it |
OrderCancelled | One nonce cancelled | That order is dead |
MinimumNonceUpdated | Bulk cancel | Every order from that maker below the watermark is dead |
ApprovalForAll(_, market, false) | Approval revoked | That maker's orders are unfillable until re-approved |
PlatformFeeUpdated / FeeRecipientUpdated | Fee change | Re-read parameters |
Deriving order status from events alone
An order is dead if any of these hold, and none require contract calls:
OrderFulfilledwith itsorderHashOrderCancelled(maker, nonce)MinimumNonceUpdated(maker, n)wheren > nonce- A
Transfermoved the token away frommaker ApprovalForAll(maker, market, false)and no per-token approval remainsblock.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:
- Stay behind the tip. Indexing the newest block means reorgs rewrite history under you. Three confirmations is a reasonable floor.
- Deduplicate on
txHash-logIndex. That pair is unique per log and is the only key you need to make replays safe. - 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
Transferin block order. - Sale history and volume —
OrderFulfilled, 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.