Skip to content

Documentation / @etherfold/core / checkTxInclusion

Function: checkTxInclusion() ​

checkTxInclusion<ABI>(lastSync, queries, finality): Record<string, TxInclusionVerdict>

Defined in: packages/core/src/utils/txInclusion.ts:125

Does the indexed state already account for these transactions?

The question an app has to answer before it applies an optimistic update on top of indexed state: if the state already contains the transaction's effects and the app adds its own prediction on top, a non-idempotent update (a counter, a balance, an append) is counted twice.

Why the caller's receipt cannot answer it ​

The obvious approach is to compare the receipt's block against how far the indexer has got. It does not work, and neither half of it works:

  • A HEIGHT is a local opinion about a chain, not an identity. The app's wallet node and the indexer's node are different nodes; "block 100" is not a shared name for a block.
  • The receipt's BLOCK HASH is an identity, but it is the wrong one: after a reorg the same transaction can be re-included in a different block, so the app can hold a receipt saying h1 while the indexer legitimately processed the transaction in h2. Comparing hashes then reports "not indexed" for a transaction that IS indexed, which is precisely the double-count.

So the question is about the indexer's own chain and only the indexer can answer it. What makes that cheap here is that it already holds the answer: LastSync.unconfirmedBlocks keeps the reorg-eligible window as whole blocks WITH their events, and every event carries its transactionHash. Nothing is stored for this, no processor declares anything for it, and the set maintains itself: a reorged-out block leaves the window, so a retracted transaction stops being reported as included and starts again if it is re-included.

A hit must also be BEHIND the cursor ​

Being in the window is not the same as having been applied. feed builds the new window up front and then walks lastToBlock forward batch by batch, so a transaction can be in the published window several batches before the processor has seen its events. A hit above lastToBlock is therefore reported absent/ahead-of-cursor rather than included.

What it cannot tell you ​

  • Only transactions that emitted events this indexer indexes can hit. The window is sparse (generateStreamToAppend keeps a block only when it carries events), so a transaction that emitted nothing indexed is indistinguishable from one never seen. For the case this exists to serve that is not a restriction: an app optimistically updates because it expects the events its processor handles.
  • absent means "not in the window", so the caller must not ask about a transaction older than the window. A transaction the app itself just submitted cannot be, which is the intended use. Pass minedAtBlock to close even that gap.
  • below-window rests on the finality assumption, and on no other: below latestBlock - finality the two nodes are assumed to agree on the chain, so heights are comparable again. That is the same assumption the indexer already makes when it prunes a block out of its window, not a new one.

Type Parameters ​

ABI ​

ABI extends Abi

Parameters ​

lastSync ​

LastSync<ABI> | undefined

the cursor the state was computed at. Read it from the SAME snapshot as the state you are reconciling: a cursor read separately can be ahead of the rows it is compared against.

queries ​

readonly TxInclusionQuery[]

the transactions to ask about, which is the app's own pending set and therefore small

finality ​

number

the finality depth the indexer is configured with (IndexerGeneration.finalityDepth), which is what bounds the window

Returns ​

Record<string, TxInclusionVerdict>

one verdict per query, keyed by the txHash string as given