In a previous post we covered the inherent limitations of Ethereum nodes and some of the constraints these limitations place on developers, and described how the dfuse Platform can help. In this post, we’ll focus on the complex lifecycle of Ethereum transactions, and the challenges these create when trying to provide a great user experience in your dapp and how dfuse can help to achieve that.
Whenever a transaction is submitted to the Ethereum network, it progresses in a fairly complex way through a sequence of states. Not every state transition moves forward — a transaction can go backward to an earlier state, can be replaced by another transaction, or can be forked out entirely. (We’ll describe the transaction lifecycle in much more detail later in this article.)
Following the journey of transactions in a dapp and presenting your users with a good experience can be challenging. The many dapps built on Ethereum today present their users with attractive but static user experiences, which show the transaction state at a point in time, and have to be refreshed (either by the user hitting Refresh, or by the dapp UI periodically refreshing the page) to get updates. Others provide more dynamic interfaces but can only do this with relatively coarse granularity, and / or at the expense of high network traffic and imposing high load on their underlying blockchain nodes.
In this post we’ll discuss the reasons for this, and then talk about how to provide a modern and fluid user experience in your dapp, with fine-grained transaction state updates, and in a highly network-efficient and server-efficient way.
Dapp Interfaces Today
Every dapp has a need to display information to its users about the underlying blockchain transactions that the dapp is performing — whether those are Ether transfers, token transfers, or smart contract method calls.
Many dapps today have an interface style in which whenever information is displayed to users about its underlying blockchain operations, the information reflects the state of the blockchain at a single point in time.
Users frequently want updated information as their transactions move through their lifecycle — for example to know when a transfer has been completed — and so the dapp either provides a “Refresh” or “Update” button (or automates the same page-refresh for the user), or the user uses their browser’s Refresh button to redisplay the page.
A few dapps go further, and display dynamic transaction updates to the user. They do this by polling through AJAX requests in their page’s background, repeatedly checking their Ethereum node for updates and then post changes to the front-end. This is quite complex, as the dapp has to make many API calls, querying many different data sources (including blocks, the mempool, and network conditions) in order to follow the lifecycle of a transaction through to completion.
This approach also imposes a tradeoff, either the transaction updates are infrequent and coarse-grained which frustrates users and tempts them to hit Refresh anyway to get faster updates, or the dapp has to poll the chain at high frequency, generating massive quantities of network traffic and imposing high loads on the underlying blockchain nodes.
Why Not Event-Driven Interfaces?
The fact that static pages or polling have been the best available options for a dapp developer reflects the nature of the API provided by Ethereum nodes. An event-driven interface that receives transaction updates pushed from the blockchain and reflects them in real time to users would provide a better experience — but standard Ethereum nodes are not designed to provide rich streaming realtime transaction data.
Ethereum nodes do provide a limited event streaming capability. These event streams are available only using the PUB/SUB capability of Ethereum’s JSON-RPC interface (not when using GraphQL). The PUB/SUB interface allows dapps to subscribe to receive notifications of a few event types:
- newHeads — each time a new block header is appended to the chain
- logs — logs that are included in new imported blocks and match the given filter
- newPendingTransactions — the hash for all transactions that are added to the pending state and are signed with a key that is available in the node (which is rarely the case on public nodes)
- syncing — Indicates when the node starts or stops synchronizing
(see here for the details)
These event types are too limited to enable a dapp to follow a transaction through its lifecycle.
The Ethereum Transaction Lifecycle
Ethereum transactions have a complex lifecycle. Each transaction passes through multiple defined states, going through various state transitions as it moves forward (or backward!) through those states.
An Ethereum transaction progresses through a series of states from when it is first submitted to the network until it is (potentially) included in a block on the chain. These states are as follows:
- UNKNOWN: A transaction that has not been seen nor processed by the network would be in the UNKNOWN state.
- PENDING: A transaction is in the PENDING state when it is waiting to be picked and processed by miners. They are in the so-called mempool. Miners usually select transactions with higher gas prices first, so transactions with lower gas prices may remain in the PENDING state for long periods. Transactions with the lowest gas prices may never get picked up, which would result in them getting “stuck” in the PENDING state indefinitely.
- IN_BLOCK: A transaction moves to the IN_BLOCK state when a miner has successfully selected the transaction and mined it within a block. Once IN_BLOCK, a transaction may move back to the PENDING state if the block is forked.
- REPLACED: A transaction can move to the REPLACED state from the PENDING state when either of these events occur:
- Another transaction originating from the same sender with the same nonce enters the IN_BLOCK state, or
- Another transaction originating from the same sender with the same nonce with a 12% higher gas price enters the PENDING state
The following diagram shows these states and the transitions between them.
As shown in the diagram above, the transitions between states also have names.
- POOLED: A transaction in the UNKNOWN state that enters the pool of transactions waiting to be selected by miners is said to be POOLED and enters the PENDING state. It is also possible that a transaction in REPLACED state becomes POOLED again, if the conditions for its replacement are no longer true (ex: in the rare case where a transaction with low gas price that was IN_BLOCK gets forked, and the REPLACED transaction with same nonce+sender with higher gas price was still floating around).
- MINED: A mined transaction is one that has been processed by a miner, creating a block. Once mined, a transaction is said to be in the IN_BLOCK state. Due to the peer-to-peer nature of the Ethereum network, from the perspective of a given node a transaction can move from the UNKNOWN state directly to the IN_BLOCK state without visibly passing through the PENDING state. For the same reason, from the perspective of a given node a transaction may also pass from the REPLACED state to the IN_BLOCK state without passing through the PENDING state.
- REPLACED: A transaction that moves from the PENDING state to the REPLACED state is said to be REPLACED. See the REPLACED state above for the conditions in which this occurs.
- FORKED: A forked transaction occurs when a mined transaction is part of a block that gets reversed by the network. All transactions within the reversed block will subsequently be forked, thus moving them from the IN_BLOCK state to the PENDING state.
- CONFIRMED: A transaction in the IN_BLOCK state is CONFIRMED every time a subsequent, child block gets mined.
As you can see, the lifecycle of an Ethereum transaction can be quite complex, making it challenging for a dapp to follow it accurately and to provide seamless updates to its users.
Effortless Tracking of Transaction States with dfuse
The dfuse Platform provides you with a rich, streaming interface that supports detailed tracking of the lifecycle of Ethereum transactions in realtime. The dfuse Ethereum State Tracker API empowers developers to submit Ethereum transactions and then serves up instant and granular updates over the same channel, as the transaction progresses through its full lifecycle.
Using GraphQL, you can subscribe to the transitions of a specific transaction in real-time, and can specify precisely the data that it wants to receive per transition. The dfuse Platform manages the complexities of tracking the transaction through its various state transitions, and streams events to the dapp in real-time as they occur.
The result is that you don’t need to implement complex background logic to poll repeatedly for updates, nor waste bandwidth and processing on repeated queries. You can simply subscribe to the updates you need, and then reflect those in your UI.
The following animation shows a transaction that goes through a complex lifecycle — it goes through eight state transitions, before finally being included in a block and confirmed.
If you were not using dfuse, your dapp would have to poll the chain repeatedly to catch all the transitions the transaction goes through in order to update your users, and your code would need to be prepared to respond appropriately to each transition.
With dfuse, your dapp only needs to listen for streaming updates on a single connection, as dfuse tracks the twists and turns for you until the fate of the transaction is finalized.
A Modern Platform for Cutting-Edge Dapps
The Lifecycle API is just a small, but important, part of the dfuse Platform. dfuse provides you with a complete modern infrastructure layer for your dapps that is:
- Gives you highly granular, streaming access to blockchain events,
- Supports active webhook-style callbacks,
- all with the highest reliability in the industry.
Try dfuse today. Reach out to us on Twitter or Telegram with any questions/suggestions or to talk about your Ethereum dapp building experience — we’d love to know if you were delighted by the service.