Output
sf.substreams.sink.database.v1.DatabaseChanges
Inputs

Package ref
uniswap-database-changes-margin-markets-mainnet@v0.0.1Run package
CLI
Run db_out from the command line.
substreams run uniswap-database-changes-margin-markets-mainnet@v0.0.1 db_out -e mainnetsubstreams auth or directly on thegraph.market (see docs).README
Margin MarketsTurns
uniswap-margin-marketsstate messages into database changes, one block-level row per message.
src/ is hand-written, not autogen. The rows are per entity per block and
carry no log identity (tx_hash, caller, ordinal, log_index, ...), which
the autogen Rust generator requires on every message. autogen.config.json
(blockLevel: true) still drives yarn generate-all for events.proto, the
BigQuery schema and events_pb.ts, as for pool-state and tx-digest.
| Index | Table | Ordering key |
|---|---|---|
| 2100 | uniswap_margin_markets_morpho_blue_market_state | morpho_blue:<market_id> |
| 2101 | uniswap_margin_markets_meta_morpho_vault_state | metamorpho:<vault> |
| 2102 | uniswap_margin_markets_meta_morpho_vault_market_state | metamorpho:<vault>:<market_id> |
| 2103 | uniswap_margin_markets_aave_v3_reserve_state | aave_v3:<asset> |
| 2104 | uniswap_margin_markets_aave_v4_spoke_reserve_state | aave_v4:<spoke>:<reserve_id> |
| 2105 | uniswap_margin_markets_compound_v3_comet_state | compound_v3:<comet> |
| 2106 | uniswap_margin_markets_compound_v3_comet_asset_state | compound_v3:<comet>:<asset> |
Every row carries block_num, block_hash, timestamp, index,
global_sequence and ordering_key; the sink adds chain_id and
sink_created_at. global_sequence is a row id, (block << 64) | row index
zero-padded to 39 digits, unique within the block. Fields are sorted by name
and the primary key is the single string <block>:<index>, so a replay is
byte-identical. An unset optional field is left out of the row, so it is NULL
in BigQuery and undefined in TS; a consumer reads it as unknown, not zero.
The sink maps a table to its events.proto message by name, so the table
names above are the event contract, and every column must exist in that
message.
cd .. && ./build-database-changes-spkg.sh mainnet v0.0.1 margin-markets
A row's index depends on what else its block touched. So when a reorg replaces
block N, the new block can give an entity a lower global_sequence than the
replaced block did. A consumer that keeps the latest row per key and writes
only when global_sequence is greater than the stored last_sequence would
then drop the canonical row.
The sink handles a reorg by hash (PubSubPublisher.arbitrateBlock, high
priority stream). When block N arrives with a new hash, it first publishes an
UNDO for each row it cached at N under the old hash, on the row's ordering key.
Every UNDO carries reorg_context.last_valid_global_sequence, which is
((N - 1) << 64) | (2^64 - 1) padded to 39 digits. Then it publishes the new
block's rows. A consumer follows two rules per key:
global_sequence is greater than the stored
last_sequence.last_sequence is greater than the UNDO's
last_valid_global_sequence, set last_sequence to that value and mark the
row stale, so gates fail closed, until an APPLY lands. Otherwise ignore the
UNDO: the undone row never became current.After the UNDO, any row from height N or later passes the guard, whatever its index. A key can stay stale longer than needed: one that the new blocks do not touch waits for a later block, because the rows are snapshots and an UNDO cannot restore the row before it.
Over several blocks the sink undoes height by height, before each new block, so the rule does not cover a key the orphaned branch touched only at a later height. Say orphan N does not touch E and orphan N+1 does. When canonical N arrives, E gets no UNDO, so the replaced N+1 row stays current and canonical N's row for E is dropped by the guard. When canonical N+1 arrives, E's UNDO marks it stale, and it stays stale until a later APPLY. So a multi-block reorg can serve a replaced row until the sink reaches its height, and can lose a canonical row for that key; both end in a stale row, never a wrong current one, once the reorg is fully replayed.
No numbering on the stream side can avoid this. The two blocks at height N are computed independently, and a guard compares numbers, not blocks. Numbering rows by entity instead would give both blocks the same number for an entity, which a strict guard also drops.
Modules
Output
sf.substreams.sink.database.v1.DatabaseChanges
Inputs
What the block changed: running-total deltas, latest field values, first sightings and the touched entities.
Entities announced in the block (markets, oracles, vaults, reserves, hub assets, comet configurations) and the storage slots they own.
Inputs
Store value
string
Update policy
set_if_not_exists
Update policy
set
Update policy
set
Store value
bigint
Update policy
add