Tracking Transactions with Lifecycle
beta

In this guide we will create a simple React application that will use dfuse’s Transaction State Tracker API to keep track of the state of an Ethereum transaction in real time. To do that, we will be using react hooks .

0. Completed Example

If you prefer to skip forward and run the completed project, run:



# clone and install the example project

git clone github.com/dfuse-io/docs
cd docs/tutorials/eth/lifecycle
yarn install
yarn start

Note

Installing the React Dev Tools plugin for your browser is optional, but is very useful for seeing what goes on in the application.

1. Create React App

Use the https://github.com/facebook/create-react-app to set up your development environment so that you can use the latest JavaScript features. You’ll need to have Node >= 8.10 and npm >= 5.6 on your machine. To create a project, run:



# get create-react-app: https://github.com/facebook/create-react-app

npx create-react-app lifecycle
cd lifecycle
npm start

then open (http://localhost:3000/ )

2. Get your API key

Get an API key

  1. Create your account on app.dfuse.io
  2. Click “Create New Key” and give it a name, a category. In the case of a web key give it an “Origin” value.
See Authentication for further details

3. Add the dfuse Client Library

The simplest way to get started with dfuse and JavaScript/TypeScript development is to use the dfuse JS client library.



# https://www.npmjs.com/package/@dfuse/client

npm install --save @dfuse/client

4. Setup dfuse Client

Import the necessary functions from dfuse/client at the top of src/App.js.



import React, { useState } from 'react';
import { createDfuseClient } from "@dfuse/client"
import './App.css';

Initialize the dfuse client using the API key you created in the second step. Let’s create the dfuseClient right after the function App() declaration.



    const dfuseClient = createDfuseClient({
        apiKey: "<YOUR_API_KEY_HERE>",
        network: "mainnet.eth.dfuse.io"
    });

5. Craft the GraphQL query

To get a real-time feed of transaction state changes, we need to craft a GraphQL subscription query. A GraphQL subscription will continuously stream responses and allows you to pick and choose the fields you want to return in the messages.

Note

See Getting Started with GraphQL for more information about GraphQL.

Our GraphQL query will use the transactionLifecycle with a hash filter to retrieve the state changes of the transaction.

Tip

Do not worry! This query may seem intimidating, but it is broken down using the Lifecycle Concept.


    let streamTransactionQuery = `
     subscription($hash: String!){
      transactionLifecycle(hash: $hash){
        previousState
        currentState
        transitionName
        transition{
          __typename

        ... on TrxTransitionInit {
            transaction {
            ...TransactionFragment
            }
            blockHeader {
            ...BlockHeaderFragment
            }
            trace {
            ...TransactionTraceFragment
            }
            confirmations
            replacedById
          }

        ...on TrxTransitionPooled {
            transaction {
            ...TransactionFragment
            }
          }

        ...on TrxTransitionMined {
            blockHeader {
            ...BlockHeaderFragment
            }
            trace {
            ...TransactionTraceFragment
            }
            confirmations
          }

        ...on TrxTransitionForked {
            transaction {
            ...TransactionFragment
            }
          }

        ...on TrxTransitionConfirmed {
            confirmations
          }

        ...on TrxTransitionReplaced {
            replacedById
          }

        }
      }
    }

    fragment TransactionFragment on Transaction {
      hash
      from
      to
      nonce
      gasPrice
      gasLimit
      value
      inputData
      signature {
        v
        s
        r
      }
    }

    fragment TransactionTraceFragment on TransactionTrace {
      hash
      from
      to
      nonce
      gasPrice
      gasLimit
      value
      inputData
      signature {
        v
        s
        r
      }
      cumulativeGasUsed
      publicKey
      index
      create
      outcome
    }

    fragment BlockHeaderFragment on BlockHeader {
      parentHash
      unclesHash
      coinbase
      stateRoot
      transactionsRoot
      receiptRoot
      logsBloom
      difficulty
      number
      gasLimit
      gasUsed
      timestamp
      extraData
      mixHash
      nonce
      hash
    }`;

6. Setup our Hooks

Lets setup a few hooks that will help us keep track of our transaction states and render our component. We use react state hook to accomplish this.

  • transactionHash: keeps track of the transaction’s hash
  • transitions: array that stores all the received transaction transitions
  • state: stores the current state of the GraphQL subscription
  • error: stores our errors


    const [transactionHash, setTransactionHash] = useState('');
    const [transitions, setTransitions] = useState([]);
    const [state, setState] = useState("initialize");
    const [error, setError] = useState("");

7. Get a Transaction State

Create an async function fetchTransaction that will use the dfuse JS client to execute the GraphQL query we crafted above and initialize a few state variables.


async function fetchTransaction() {
setState("streaming"); // sets the state of our query to "streaming"
setError(""); // clears any errors that may have been logged before
setTransitions([]); // clears the transitions when starting a new search
var currentTransitions = []; // local variable to store transition in callback function
var count = 0; // reset transition count
...
}

Use the dfuse client with the GraphQL query and set our transaction hash as a variable.


async function fetchTransaction() {
setState("streaming"); // sets the state of our query to "streaming"
setError(""); // clears any errors that may have been logged before
setTransitions([]); // clears the transitions when starting a new search
var currentTransitions = []; // local variable to store transition in callback function
var count = 0; // reset transition count

    const stream = await dfuseClient.graphql(streamTransactionQuery, (message) => {
        ...
    },{
        variables: {
            hash:  transactionHash
        }
    });
    await stream.join();  // awaits stream completion, which is never for this operation

}

The message returned from the GraphQL stream can have 3 different types that need to be handled in our code:

  • error: This is an error returned by the stream. We simply store it in our state.
  • data: This contains the transition state tracker payload. We create a newTransition object and store that in our transitions array.
  • complete: This message occurs when the stream is closed. We update our stream state.


    async function fetchTransaction() {
        setState("streaming");
        setError("");
        setTransitions([]);
        var currentTransitions = [];
        var count = 0;

        const stream = await dfuseClient.graphql(streamTransactionQuery, (message) => {

            if (message.type === "error") {
                setError(message.errors[0]['message'])
            }

            if (message.type === "data") {
                var newTransition = {
                    key: `transition-${count}`,
                    transition: message['data']['transactionLifecycle']['transitionName'],
                    from: message['data']['transactionLifecycle']['previousState'],
                    to: message['data']['transactionLifecycle']['currentState'],
                    data: message['data']
                };
                count++;
                currentTransitions = [...currentTransitions, newTransition]
                setTransitions(currentTransitions.reverse());
            }

            if (message.type === "complete") {
                setState("completed");
            }
        },{
            variables: {
                hash:  transactionHash
            }
        });

        await stream.join() // awaits stream completion, which is never for this operation
    }

8. Render Function

Build the render method for this component. It will include an input for the transaction hash, and handles the different possible states of our component.



    return (
        <div className="App">
            <div className="form">
                <p>Enter a transaction hash</p>
                <input type={"text"} value={transactionHash} onChange={(e) => setTransactionHash(e.target.value)} className={'trx-id'} /> <br/>
                <button className="submit" onClick={() => fetchTransaction()}>Search Transaction</button>
            </div>
            <div className="data">
                {   (error !== "") && (<div className='error'>{ error }</div>) }
                {   (error === "") &&
                    ((state === "streaming") ||  (state === "completed")) &&
                    (
                        <div>
                            <label className="state">{state}</label>
                            <div>
                                {
                                    transitions.map((transition) => (
                                        <div className="transition" key={transition.key}>
                                            <strong>Transition:</strong> {transition.transition} <br/>
                                            <strong>Previous State:</strong> {transition.from} <br/>
                                            <strong>Current State:</strong> {transition.to} <br/>
                                            <pre key={transition.key}>  { JSON.stringify(transition.data, null, 1) } </pre>
                                        </div>
                                    ))
                                }
                            </div>
                        </div>
                    )
                }
                {   (state !== "streaming") &&
                    (
                        <div>Enter a transaction hash to begin</div>
                    )
                }
            </div>
        </div>
    );
}

9. Prettifying it with CSS

Add some CSS to style this HTML a bit. Replace the contents of src/App.css with the following:



.App {
    text-align: left;
    width:1080px;
    margin:auto auto;
    display: flex;
    flex-direction: row;
}

.App .form {
    padding-top:50px;

    text-align: center;
}

.App .data {
    padding:50px;
    width: 100%;
}

.App .data pre {
    padding:10px;
    white-space: pre-wrap;
    white-space: -moz-pre-wrap;
    white-space: -o-pre-wrap;
    word-wrap: break-word;
}


.trx-id {
    padding: 18.5px 14px;
    height: 1.1875em;
    background: none;
    box-sizing: content-box;
    border:thin #878787 solid;
    width:300px;
    margin-bottom:10px;
}

.submit {
    color: #fff;
    height: 40px;
    font-size: 16px;
    box-shadow: none;
    line-height: 16px;
    padding-top: 10px;
    padding-left: 40px;
    border-radius: 20px;
    padding-right: 40px;
    padding-bottom: 10px;
    text-transform: none;
    background-color: #ff4660;
}

.transition {
    padding:7px;
    border-radius: 2px;
    background: #f8f8fa;
    border: thin solid #f8f9fa;
    margin-top: 10px;
    margin-bottom: 10px;

}
.error {
    color: #721c24;
    background-color: #f8d7da;
    border-color: #f5c6cb;
    padding:20px;
    width: 100%;
}

.state {
    color: #fff;
    background-color: #17a2b8;
    display: inline-block;
    padding: .25em .4em;
    font-size: 75%;
    font-weight: 700;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: .25rem;
}

10. Full Working Example

The source code for this tutorial is available on GitHub . Below are the code files discussed on this page.



import React, { useState } from 'react';
import { createDfuseClient } from "@dfuse/client"
import './App.css';
function App() {    const dfuseClient = createDfuseClient({
        apiKey: "<YOUR_API_KEY_HERE>",
        network: "mainnet.eth.dfuse.io"
    });    let streamTransactionQuery = `
     subscription($hash: String!){
      transactionLifecycle(hash: $hash){
        previousState
        currentState
        transitionName
        transition{
          __typename

        ... on TrxTransitionInit {
            transaction {
            ...TransactionFragment
            }
            blockHeader {
            ...BlockHeaderFragment
            }
            trace {
            ...TransactionTraceFragment
            }
            confirmations
            replacedById
          }

        ...on TrxTransitionPooled {
            transaction {
            ...TransactionFragment
            }
          }

        ...on TrxTransitionMined {
            blockHeader {
            ...BlockHeaderFragment
            }
            trace {
            ...TransactionTraceFragment
            }
            confirmations
          }

        ...on TrxTransitionForked {
            transaction {
            ...TransactionFragment
            }
          }

        ...on TrxTransitionConfirmed {
            confirmations
          }

        ...on TrxTransitionReplaced {
            replacedById
          }

        }
      }
    }

    fragment TransactionFragment on Transaction {
      hash
      from
      to
      nonce
      gasPrice
      gasLimit
      value
      inputData
      signature {
        v
        s
        r
      }
    }

    fragment TransactionTraceFragment on TransactionTrace {
      hash
      from
      to
      nonce
      gasPrice
      gasLimit
      value
      inputData
      signature {
        v
        s
        r
      }
      cumulativeGasUsed
      publicKey
      index
      create
      outcome
    }

    fragment BlockHeaderFragment on BlockHeader {
      parentHash
      unclesHash
      coinbase
      stateRoot
      transactionsRoot
      receiptRoot
      logsBloom
      difficulty
      number
      gasLimit
      gasUsed
      timestamp
      extraData
      mixHash
      nonce
      hash
    }`;    const [transactionHash, setTransactionHash] = useState('');
    const [transitions, setTransitions] = useState([]);
    const [state, setState] = useState("initialize");
    const [error, setError] = useState("");    async function fetchTransaction() {
        setState("streaming");
        setError("");
        setTransitions([]);
        var currentTransitions = [];
        var count = 0;

        const stream = await dfuseClient.graphql(streamTransactionQuery, (message) => {

            if (message.type === "error") {
                setError(message.errors[0]['message'])
            }

            if (message.type === "data") {
                var newTransition = {
                    key: `transition-${count}`,
                    transition: message['data']['transactionLifecycle']['transitionName'],
                    from: message['data']['transactionLifecycle']['previousState'],
                    to: message['data']['transactionLifecycle']['currentState'],
                    data: message['data']
                };
                count++;
                currentTransitions = [...currentTransitions, newTransition]
                setTransitions(currentTransitions.reverse());
            }

            if (message.type === "complete") {
                setState("completed");
            }
        },{
            variables: {
                hash:  transactionHash
            }
        });

        await stream.join() // awaits stream completion, which is never for this operation
    }    return (
        <div className="App">
            <div className="form">
                <p>Enter a transaction hash</p>
                <input type={"text"} value={transactionHash} onChange={(e) => setTransactionHash(e.target.value)} className={'trx-id'} /> <br/>
                <button className="submit" onClick={() => fetchTransaction()}>Search Transaction</button>
            </div>
            <div className="data">
                {   (error !== "") && (<div className='error'>{ error }</div>) }
                {   (error === "") &&
                    ((state === "streaming") ||  (state === "completed")) &&
                    (
                        <div>
                            <label className="state">{state}</label>
                            <div>
                                {
                                    transitions.map((transition) => (
                                        <div className="transition" key={transition.key}>
                                            <strong>Transition:</strong> {transition.transition} <br/>
                                            <strong>Previous State:</strong> {transition.from} <br/>
                                            <strong>Current State:</strong> {transition.to} <br/>
                                            <pre key={transition.key}>  { JSON.stringify(transition.data, null, 1) } </pre>
                                        </div>
                                    ))
                                }
                            </div>
                        </div>
                    )
                }
                {   (state !== "streaming") &&
                    (
                        <div>Enter a transaction hash to begin</div>
                    )
                }
            </div>
        </div>
    );
}
export default App;


.App {
    text-align: left;
    width:1080px;
    margin:auto auto;
    display: flex;
    flex-direction: row;
}

.App .form {
    padding-top:50px;

    text-align: center;
}

.App .data {
    padding:50px;
    width: 100%;
}

.App .data pre {
    padding:10px;
    white-space: pre-wrap;
    white-space: -moz-pre-wrap;
    white-space: -o-pre-wrap;
    word-wrap: break-word;
}


.trx-id {
    padding: 18.5px 14px;
    height: 1.1875em;
    background: none;
    box-sizing: content-box;
    border:thin #878787 solid;
    width:300px;
    margin-bottom:10px;
}

.submit {
    color: #fff;
    height: 40px;
    font-size: 16px;
    box-shadow: none;
    line-height: 16px;
    padding-top: 10px;
    padding-left: 40px;
    border-radius: 20px;
    padding-right: 40px;
    padding-bottom: 10px;
    text-transform: none;
    background-color: #ff4660;
}

.transition {
    padding:7px;
    border-radius: 2px;
    background: #f8f8fa;
    border: thin solid #f8f9fa;
    margin-top: 10px;
    margin-bottom: 10px;

}
.error {
    color: #721c24;
    background-color: #f8d7da;
    border-color: #f5c6cb;
    padding:20px;
    width: 100%;
}

.state {
    color: #fff;
    background-color: #17a2b8;
    display: inline-block;
    padding: .25em .4em;
    font-size: 75%;
    font-weight: 700;
    line-height: 1;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    border-radius: .25rem;
}