Searching Transactions
beta

In this guide we will create a simple React application that will use dfuse’s Search API and query language to find specific Ethereum transactions. To do that, we will be using react hooks .

Ethereum Search Demo

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/search
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 search
cd search
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 show multiple search results, we can either use a GraphQL query or subscription query. A GraphQL subscription will continuously stream responses while a GraphQL query allows for pagination. We will use a GraphQL query to return the first 10 results of our search.

Note

We are using a long query to request all the available data.

With GraphQL, you can choose to request as little or as much data as needed. Therefore you can shrink down the query to only 6 lines and only request the transactionHash if you prefer.

See Getting Started with GraphQL for more information.

Tip

See Search Query Langauge to learn more about what you can search.


  const FlatCallFragment = `fragment FlatCallFragment on Call {
    index
    depth
    parentIndex
    callType
    from
    to
    value(encoding:WEI)
    gasConsumed
    inputData
    returnData
    logs {
      address
      topics
      data
    }
    balanceChanges{
      reason
      address
      oldValue(encoding:WEI)
      newValue(encoding:WEI)
    }
    storageChanges{
      key
      address
      oldValue
      newValue
    }
  }
  `;

  const searchTransactionsQuery = `query ($query: String!, $indexName:TRANSACTIONS_INDEX_NAME!, $lowBlockNum: Int64, $highBlockNum: Int64, $sort: SORT!, $cursor: String!, $limit: Int64!){
    searchTransactions(query: $query, indexName: $indexName, lowBlockNum: $lowBlockNum, highBlockNum: $highBlockNum, sort: $sort,  cursor: $cursor, limit: $limit) {
      pageInfo {
        startCursor
        endCursor
      }
      edges {
        undo
        cursor
        node {
          value(encoding:WEI)
          hash
          nonce
          gasLimit
          gasUsed
          gasPrice(encoding:WEI)
          to
          block {
            number
            hash
            header {
              timestamp
            }
          }
          flatCalls {
            ...FlatCallFragment
          }
        }
      }
    }
  }

  ${FlatCallFragment}`;

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.

  • query: keeps track of the search query
  • transactions: array that stores all the received transactions
  • state: stores the current state of the GraphQL subscription
  • error: stores our errors


  const [query, setQuery] = useState('');
  const [transactions, setTransactions] = useState([]);
  const [state, setState] = useState('initialize');
  const [error, setError] = useState('');

7. Search Transactions Function

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


async function searchTransactions() {
  setState('searching'); // sets the state of our query to "searching"
  setError(""); // clears any errors that may have been logged before
  setTransactions([]); // clears the transactions when starting a new search
  var currentResults = []; // local variable to store transactions in callback function
  ...
}

Use dfuse client with the GraphQL query and set the following variables:

  • query: query string to tell the API what you want to search for
  • indexName: (CALLS | LOGS) type of data to search for
  • lowBlockNum: lower range of block number to search within
  • highBlockNum: higher range of block number to search within
  • limit: limit of results to return
  • sort: (ASC | DESC) ascending or desending direction to search in
  • cursor: chain-wide pointer to an exact location that allows you to resume your search at


  async function searchTransactions() {
    setTransactions([]);
    setState('searching');
    setError('');
    const parsedSQE = parseSQE(query);
    try {
      const response = await dfuseClient.graphql(searchTransactionsQuery, {
        variables: {
          query: parsedSQE.query,
          indexName: 'CALLS',
          lowBlockNum: '0',
          highBlockNum: '-1',
          sort: 'DESC',
          limit: '10',
          cursor: ''
        }
      });

      if (response.errors) {
        throw response.errors;
      }

      const edges = response.data.searchTransactions.edges || [];
      if (edges.length <= 0) {
        setError('Oops nothing found');
        return;
      }
      setTransactions(edges.map(edge => edge.node));
      setState('completed');
    } catch (errors) {
      setError(JSON.stringify(errors));
      setState('completed');
    }

    dfuseClient.release();
  }

8. Render Function

Build the render method for this component. It includes an input for the search query string, and handles the different possible states of our component.



  return (
    <div className='App'>
      <div className='form'>
        <p>Search Ethereum Data</p>
        <input
          type={'text'}
          value={query}
          onChange={e => setQuery(e.target.value)}
          className={'trx-id'}
          placeholder='Enter search query'
        />{' '}
        <br />
        <button className='submit' onClick={() => searchTransactions()}>
          Run search
        </button>
        <hr />
        <a
          href='https://docs.dfuse.io/guides/core-concepts/search-query-language/'
          target='_blank'
        >
          Search Language Reference
        </a>
        <br />
        <a
          href='https://docs.dfuse.io/ethereum/public-apis/reference/search/terms/'
          target='_blank'
        >
          Ethereum Search Terms
        </a>
      </div>
      <div className='data'>
        {state !== 'searching' && <p>Enter a search query to begin</p>}
        {error !== '' && <div className='error'>{error}</div>}
        <br />
        {error === '' && (state === 'searching' || state === 'completed') && (
          <div>
            <label className='state'>{state}</label>
            <div>
              {transactions.map((transaction, index) => (
                <div className='transaction' key={index}>
                  <pre key={index}>{JSON.stringify(transaction, null, 1)}</pre>
                  <hr />
                </div>
              ))}
            </div>
          </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'
  });  const FlatCallFragment = `fragment FlatCallFragment on Call {
    index
    depth
    parentIndex
    callType
    from
    to
    value(encoding:WEI)
    gasConsumed
    inputData
    returnData
    logs {
      address
      topics
      data
    }
    balanceChanges{
      reason
      address
      oldValue(encoding:WEI)
      newValue(encoding:WEI)
    }
    storageChanges{
      key
      address
      oldValue
      newValue
    }
  }
  `;

  const searchTransactionsQuery = `query ($query: String!, $indexName:TRANSACTIONS_INDEX_NAME!, $lowBlockNum: Int64, $highBlockNum: Int64, $sort: SORT!, $cursor: String!, $limit: Int64!){
    searchTransactions(query: $query, indexName: $indexName, lowBlockNum: $lowBlockNum, highBlockNum: $highBlockNum, sort: $sort,  cursor: $cursor, limit: $limit) {
      pageInfo {
        startCursor
        endCursor
      }
      edges {
        undo
        cursor
        node {
          value(encoding:WEI)
          hash
          nonce
          gasLimit
          gasUsed
          gasPrice(encoding:WEI)
          to
          block {
            number
            hash
            header {
              timestamp
            }
          }
          flatCalls {
            ...FlatCallFragment
          }
        }
      }
    }
  }

  ${FlatCallFragment}`;  const [query, setQuery] = useState('');
  const [transactions, setTransactions] = useState([]);
  const [state, setState] = useState('initialize');
  const [error, setError] = useState('');
  function parseSQE(input) {
    return {
      query: input.trim()
    };
  }
  async function searchTransactions() {
    setTransactions([]);
    setState('searching');
    setError('');
    const parsedSQE = parseSQE(query);
    try {
      const response = await dfuseClient.graphql(searchTransactionsQuery, {
        variables: {
          query: parsedSQE.query,
          indexName: 'CALLS',
          lowBlockNum: '0',
          highBlockNum: '-1',
          sort: 'DESC',
          limit: '10',
          cursor: ''
        }
      });

      if (response.errors) {
        throw response.errors;
      }

      const edges = response.data.searchTransactions.edges || [];
      if (edges.length <= 0) {
        setError('Oops nothing found');
        return;
      }
      setTransactions(edges.map(edge => edge.node));
      setState('completed');
    } catch (errors) {
      setError(JSON.stringify(errors));
      setState('completed');
    }

    dfuseClient.release();
  }  return (
    <div className='App'>
      <div className='form'>
        <p>Search Ethereum Data</p>
        <input
          type={'text'}
          value={query}
          onChange={e => setQuery(e.target.value)}
          className={'trx-id'}
          placeholder='Enter search query'
        />{' '}
        <br />
        <button className='submit' onClick={() => searchTransactions()}>
          Run search
        </button>
        <hr />
        <a
          href='https://docs.dfuse.io/guides/core-concepts/search-query-language/'
          target='_blank'
        >
          Search Language Reference
        </a>
        <br />
        <a
          href='https://docs.dfuse.io/ethereum/public-apis/reference/search/terms/'
          target='_blank'
        >
          Ethereum Search Terms
        </a>
      </div>
      <div className='data'>
        {state !== 'searching' && <p>Enter a search query to begin</p>}
        {error !== '' && <div className='error'>{error}</div>}
        <br />
        {error === '' && (state === 'searching' || state === 'completed') && (
          <div>
            <label className='state'>{state}</label>
            <div>
              {transactions.map((transaction, index) => (
                <div className='transaction' key={index}>
                  <pre key={index}>{JSON.stringify(transaction, null, 1)}</pre>
                  <hr />
                </div>
              ))}
            </div>
          </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;
}