Index eventsLink to this section
TL;DRLink to this section
- SqueekCore logs record settled work and block progress; SqueekMining and SqueekToken logs record funded rewards, activations and claims.
- Decode with the ABI of the implementation active at that address and block.
BatchSettledadvances the certified frontier even when no round recorded work.- Store block hashes and log order so a reorg can roll back derived state, not just raw logs.
Before you startLink to this section
Complete Verify the deployment and start at INDEX_FROM_BLOCK from Deployments. Decode with the published ABIs linked there. The graph launches fresh: there is no earlier ledger to continue from.
Applications log at the proxy addresses. A matching topic does not prove the emitter, so filter by verified address and keep each proxy's implementation history, because an upgrade can change the ABI.
StepsLink to this section
Fetch and order logsLink to this section
- Fetch logs for the four verified proxies in bounded Ethereum block ranges.
- Store chain ID, block number and hash, transaction hash and index, and log index. Use
(chainId, blockHash, transactionHash, logIndex)as the occurrence identity. - Process in block, transaction index and log index order.
- After an upgrade event, verify the new implementation before choosing its ABI. Stop the projection if the code or layout is unknown.
Know which events each action emitsLink to this section
| Action | Events, in order |
|---|---|
| Settlement | Zero or more SqueekCore RoundRecorded, then one BatchSettled |
| Mint | For each Squeek, SqueekNFT Transfer from zero then SqueekMining PositionRegistered; then one SqueekNFT batch event (PublicMinted, AllowlistMinted, ProtocolReserveMinted or PublicRemainderMinted) |
| Checkpoint | SqueekMining WeightActivated and Checkpointed per stretch, then one SqueekToken mint Transfer and CheckpointMinted |
| Claim | SqueekMining RewardsClaimed; a SqueekToken Transfer only when the payout is nonzero |
| Merge | SqueekNFT burn Transfer for the donor, then SqueekMining PositionsMerged |
| NFT transfer | SqueekNFT Transfer only; SqueekMining gets no callback |
Index ownership from SqueekNFT Transfer events, not SqueekMining events. A reverted transaction emits nothing; a reorg can remove events from a receipt you already saw.
Index SqueekCoreLink to this section
- Start physical work and block units at zero and difficulty at
CoreInitialized.initialWorkPerThousandBlocks. - For each
RoundRecorded, price its work with the previous position's difficulty, then check the emitted totals and the emitted next difficulty. - Advance the certified frontier on every
BatchSettled, including batches with zero active rounds. Reconcile its final totals andactiveRoundCountwith the transaction'sRoundRecordedevents, which all precede it. - Derive completed blocks from the change in
cumulativeBlockUnits / 1_000_000_000. There is no per-block event, and one settlement can complete several blocks.
// decode-round.ts — `log` is a raw log already filtered to the verified SqueekCore proxy
import { decodeEventLog, parseAbi, type Log } from "viem";
const abi = parseAbi([
"event RoundRecorded(uint64 indexed roundId, uint64 work, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint32 workPerThousandBlocks, bytes32 dataHash)",
]);
export function decodeRound(log: Log) {
const { args } = decodeEventLog({ abi, data: log.data, topics: log.topics, strict: true });
return {
round: args.roundId,
physicalWork: args.cumulativeWork,
completedBlocks: args.cumulativeBlockUnits / 1_000_000_000n,
blockProgress: args.cumulativeBlockUnits % 1_000_000_000n,
nextDifficulty: BigInt(args.workPerThousandBlocks),
};
}
The emitted difficulty prices the next active round, not this one. Follow the price-then-steer rule with exact integer arithmetic.
Index SqueekMining and SqueekTokenLink to this section
- Record
PositionRegisteredfor mints andPositionsMergedfor merges; together with SqueekNFTTransferthey give the live position set. - For each checkpoint, record every
Checkpointedstretch andWeightActivated, then the single SqueekToken mintTransferandCheckpointMinted. - Count new supply once, from the mint
Transferor fromCheckpointed.amount;CheckpointMinteddescribes the same SQK. - Record
RewardsClaimedfor payouts. Its SQKTransferexists only whenamount > 0. - Reconcile
Checkpointed.mintedwith SqueekTokentotalSupply()at the same block.
Replay and reconcileLink to this section
Replay both contracts from the deployment block to one confirmed block, then compare the result with each contract's own state at that block. A mismatch means a missed log, a wrong decoder, or a reorg; never adjust the projection to match.
- Fetch
RoundRecordedandBatchSettledfrom SqueekCore andCheckpointedfrom SqueekMining in bounded ranges, oldest first. - Require each
BatchSettled.previousSettledThroughRoundto equal the frontier you hold, so no settlement is skipped. - Sum
activeRoundCountacross batches; it must equal the number ofRoundRecordedevents. - Sum
Checkpointed.amount; it must equal SqueekMiningstate().mintedand SqueekTokentotalSupply().
// replay.ts
import { parseAbi, parseAbiItem } from "viem";
import { blockNumber, client, required } from "./setup";
import { verifyGraph } from "./verify";
const { core, mining, token } = await verifyGraph();
const start = BigInt(required("INDEX_FROM_BLOCK"));
const roundRecorded = parseAbiItem("event RoundRecorded(uint64 indexed roundId, uint64 work, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint32 workPerThousandBlocks, bytes32 dataHash)");
const batchSettled = parseAbiItem("event BatchSettled(bytes32 indexed digest, address indexed reporter, uint64 indexed settledThroughRound, uint256 nonce, uint64 previousSettledThroughRound, uint16 activeRoundCount, uint64 cumulativeWork, uint96 cumulativeBlockUnits)");
const checkpointed = parseAbiItem("event Checkpointed(uint64 indexed afterRound, uint64 indexed throughRound, uint64 completedBlocks, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint256 amount, uint256 minted, uint64 lastActivationRound, uint256 totalWeight, uint256 rewardIndex)");
let frontier = 0n, blockUnits = 0n, activeRounds = 0n, recorded = 0n, minted = 0n;
for (let from = start; from <= blockNumber; from += 1_000n) {
const to = from + 999n < blockNumber ? from + 999n : blockNumber;
const range = { fromBlock: from, toBlock: to, strict: true } as const;
const [rounds, batches, checkpoints] = await Promise.all([
client.getLogs({ address: core, event: roundRecorded, ...range }),
client.getLogs({ address: core, event: batchSettled, ...range }),
client.getLogs({ address: mining, event: checkpointed, ...range }),
]);
recorded += BigInt(rounds.length);
for (const { args } of batches) {
if (args.previousSettledThroughRound !== frontier) throw new Error(`Missing settlement before round ${args.settledThroughRound}`);
frontier = args.settledThroughRound;
blockUnits = args.cumulativeBlockUnits;
activeRounds += BigInt(args.activeRoundCount);
}
for (const { args } of checkpoints) minted += args.amount;
}
const views = parseAbi([
"function state() view returns ((uint64 settledThroughRound, uint64 cumulativeWork, uint96 cumulativeBlockUnits, uint64 blockHeight, uint96 blockProgress, uint32 workPerThousandBlocks, uint256 authorizationNonce, bool paused))",
"function totalSupply() view returns (uint256)",
]);
const miningState = parseAbi([
"function state() view returns ((uint256 minted, uint256 paid, uint256 totalWeight, uint256 rewardIndex, uint64 closedThroughBlock, uint64 lastActivationRound, uint256 weightActivationCursor, uint256 weightActivationCount, bool bootstrapped, uint64 processedThroughRound, uint64 closedCumulativeWork, uint96 closedCumulativeBlockUnits, uint32 closedWorkPerThousandBlocks))",
]);
const [coreState, mined, supply] = await Promise.all([
client.readContract({ address: core, abi: views, functionName: "state", blockNumber }),
client.readContract({ address: mining, abi: miningState, functionName: "state", blockNumber }),
client.readContract({ address: token, abi: views, functionName: "totalSupply", blockNumber }),
]);
const checks = {
frontier: frontier === coreState.settledThroughRound,
blockUnits: blockUnits === coreState.cumulativeBlockUnits,
activeRounds: activeRounds === recorded,
minted: minted === mined.minted && minted === supply,
};
if (Object.values(checks).includes(false)) throw new Error(`Projection disagrees with contract state: ${JSON.stringify(checks)}`);
console.log({ blockNumber, frontier, completedBlocks: blockUnits / 1_000_000_000n, recorded, mintedBaseUnits: minted });
A production indexer stores each log with its block hash and applies the same checks continuously instead of replaying from the start.
Handle reorgsLink to this section
Keep unconfirmed head data separate from confirmed projections. When a stored block hash no longer matches the chain, roll derived state back to the common ancestor and replay from there. Deduplicating raw log IDs alone leaves derived totals wrong.
Expected resultLink to this section
| Event | Projection effect |
|---|---|
CoreInitialized |
Stream, actors, genesis, work cap and initial difficulty; totals start at zero |
RoundRecorded |
One active round's work and hash, with the resulting physical total, earned total and next difficulty |
BatchSettled |
New certified frontier, consumed nonce, active count, and final totals |
PositionRegistered, PositionsMerged |
New and merged positions with their pending power and cooldowns |
Checkpointed, WeightActivated |
Funded stretches with completed blocks and amounts; power changes applied |
SqueekToken mint Transfer, CheckpointMinted |
One aggregate mint per checkpoint |
RewardsClaimed |
Payout to the owner and the remainder left on the lowest ID |
| Pause, Reporter and upgrade events | Availability, signer and implementation history |
Exact field lists are on each contract page: SqueekCore, SqueekMining, SqueekToken and SqueekNFT.
Common failuresLink to this section
| Failure | Cause | Fix |
|---|---|---|
| Wrong topics or tuple widths | Decoding with an earlier ABI | Select the ABI per implementation and block |
| Earned totals drift | Pricing a round with its own emitted difficulty | Use the previous position's difficulty |
| Block count is off | Dividing physical work by current difficulty | Use block units |
| Frontier lags | Ignoring zero-record BatchSettled |
Advance on every BatchSettled |
| Supply doubled | Counting CheckpointMinted and its Transfer separately |
Count one of them |
| Gaps read as zero work | No RoundRecorded outside a certified interval |
Treat uncertified rounds as unknown |
| Totals wrong after a reorg | Deduplicating logs without rolling back | Roll back to the common ancestor and replay |
A dataHash commits to the round's supporting records; it is not those records or proof of physical causation.
Verify the resultLink to this section
At one canonical block, compare your replayed frontier, physical total, earned total and difficulty with SqueekCore.state(), and selected endpoints with roundPage(r, r, 0). Compare your minted and paid totals with SqueekMining state() and SqueekToken totalSupply().
BatchSettled.nonce is the nonce that settlement consumed, not the next one. Read state() at the same block before preparing a new authorization.