Track transaction in Real-time
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. We will be using react hooks .

Note

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

1. Create React App

Use the https://github.com/facebook/create-react-app to sets 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 track-trx
cd track-trx
npm start

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

2. Get your api key

Get an API key

  1. Create your account on https://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 a 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 first step. Lets create the dfuseClient right after the function App() declaration.


    const dfuseClient = createDfuseClient({
        apiKey: "web_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        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 Graphql Concept 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 see intimidating but it is broken down in Transaction State Tracker 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: store the current state of the GraphQL subscription
  • error: store our errors


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

7. Get a transaction state

Create an async function fetchTransactionState that will use dfuse JS client to execute the GraphQL query we crafted above.


    async function fetchTransaction() {
        ...
    }

Initialize a few state variables.


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

Use the dfuse client with 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();
    }

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 return 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 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() // continues until disconnect and complete

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. Here 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: "web_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        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() // continues until disconnect and complete
    }

    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;
}