Streaming Transactions
beta

In this guide we will create a simple React application that will use dfuse’s javascript client library and the stream API to easily stream all transfers happening on the Ethereum Mainnet. To do that, we will be using react hooks .

Ethereum Stream 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/stream
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 stream
cd stream
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,
    network,
    streamClientOptions: {
      socketOptions: {
        onClose: onClose,
        onError: onError
      }
    }
  });

5. Craft the GraphQL query

A GraphQL subscription will continuously stream responses from the API. We will use a GraphQL subscription to return the the latest transfers for up to 100 results.

Note

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.

We use the following parameters for the GraphQL query:

  • query: query string to tell the API what you want to search for
  • indexName: (CALLS | LOGS) type of data to search for
  • 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

The query string -value:0 indicates that the results must have non-zero ether values within the transactions.

Tip

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


  const streamTransfersQuery = `subscription($cursor: String) {
      searchTransactions(indexName: CALLS, query: "-value:0", sort: ASC, limit: 100, cursor: $cursor) {
        undo cursor
        node { hash from to value(encoding: ETHER) }
      }
    }`;

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.

  • transfers: array that stores all the received transactions
  • state: stores the current state of the GraphQL subscription
  • error: stores our errors
  • stream: object to listen for events on


  const [transfers, setTransfers] = useState([]);
  const [state, setState] = useState('initialize');
  const [errors, setErrors] = useState([]);
  const [stream, setStream] = useState(undefined);

7. Stream Transfers Function

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

The message returned can have types of error, data, and complete. We handle each case by setting the errors, transfers, or state hooks to tell our app what to display.



  const streamTransfers = async () => {
    setTransfers([]);
    setErrors([]);
    setState('connected');
    setErrors('');
    let currentTransfers = [];
    try {
      const stream = await dfuseClient.graphql(
        streamTransfersQuery,
        async message => {
          if (message.type === 'error') {
            setErrors([
              'An error occurred',
              ...message.errors.map(error => error.message),
              ...errors
            ]);
          }

          if (message.type === 'data') {
            const {
              node: newTransfer,
              cursor
            } = message.data.searchTransactions;

            currentTransfers = [newTransfer, ...currentTransfers];
            setTransfers(currentTransfers);
            stream.mark({ cursor });
          }

          if (message.type === 'complete') {
            setState('completed');
          }
        }
      );
      setStream(stream);
    } catch (errors) {
      setErrors(JSON.stringify(errors));
      setState('completed');
    }
  };

8. Stopping Stream and Disconnects

Define functions to stop the stream, handle streaming client closing and error catching.



  const onStop = async () => {
    setState('completed');
    if (stream === undefined) {
      return;
    }
    try {
      await stream.close();
      setStream(undefined);
    } catch (error) {
      setErrors(
        `Unable to disconnect socket correctly: 
          ${JSON.stringify(error)}
        `
      );
    }
  };

  const onClose = () => {
    setState('completed');
  };

  const onError = error => {
    setErrors(
      `Unable to disconnect socket correctly: 
      ${JSON.stringify(error)}
    `
    );
  };

9. Render Function

Build the render method for this component. It includes a launch and stop button to trigger the functions we defined. It also renders the list of transfers stored in our react hook.



  const renderTransfer = (transfer, index) => {
    const { hash, from, to, value } = transfer;
    return hash ? (
      <code key={index} className='App-transfer'>
        Transfer
        <br />
        {`From: ${from} -> To: ${to}`}
        <br />
        {`Value: ${value} Hash: ${hash}`}
        <hr />
      </code>
    ) : (
      <code key={index} className='App-transfer'>
        {transfer}
      </code>
    );
  };

  const renderTransfers = () => {
    return (
      <div className='App-infinite-container'>
        {transfers.length <= 0
          ? renderTransfer('Nothing yet, start by hitting Launch!')
          : transfers.reverse().map(renderTransfer)}
      </div>
    );
  };

  const renderError = (error, index) => {
    if (error === '') {
      return <br key={index} className='App-error' />;
    }

    return (
      <code key={index} className='App-error'>
        {error}
      </code>
    );
  };

  const renderErrors = () => {
    if (errors.length <= 0) {
      return null;
    }

    return <div className='App-container'>{errors.map(renderError)}</div>;
  };

  return (
    <div className='App'>
      <header className='App-header'>
        <h2>Stream Ethereum Transfers</h2>
        {renderErrors()}
        <div className='App-buttons'>
          <button className='App-button' onClick={streamTransfers}>
            Launch
          </button>
          <button className='App-button' onClick={onStop}>
            Stop
          </button>
        </div>
        <main className='App-main'>
          <p className='App-status'>
            {`Connected: ${
              state === 'connected'
                ? 'Connected (Showing last 100 transfers)'
                : 'Disconnected'
            }`}
          </p>
          {renderTransfers()}
        </main>
      </header>
    </div>
  );
}

10. 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: center;
}

.App-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: white;
}

.App-error {
  display: block;
  color: firebrick;
  font-size: 0.85rem;
  margin: 0.25rem 0;
  text-align: left;
}

.App-main {
  width: 60%;
  margin: 0.25rem auto;
}

.App-infinite-container {
  border: 2px solid #FF4661;
  padding: 0.75rem 0.5rem;
  height: 500px;
  overflow-x: hidden;
  overflow-y: scroll;
}

.App-transfer {
  display: block;
  color: #2d234c;
  font-size: 0.85rem;
  margin: 0.25rem 0;
  text-align: left;
}

.App-button {
  background-color: #F3F3F7;
  border: 2px solid #FF4661;
  color: #FF4661;
  padding: 0.5rem;
  margin: 0.25rem;
  font-size: 1rem;
  font-weight: 700;
}

.App-status {
  color: #FF4661;
  padding: 0.5rem 0;
  margin: 0 0;
  font-size: 1rem;
  text-align: left;
}

11. 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';
const apiKey = process.env.REACT_APP_DFUSE_API_KEY;
const network = process.env.REACT_APP_DFUSE_NETWORK || 'mainnet.eth.dfuse.io';

function App() {  const dfuseClient = createDfuseClient({
    apiKey,
    network,
    streamClientOptions: {
      socketOptions: {
        onClose: onClose,
        onError: onError
      }
    }
  });  const streamTransfersQuery = `subscription($cursor: String) {
      searchTransactions(indexName: CALLS, query: "-value:0", sort: ASC, limit: 100, cursor: $cursor) {
        undo cursor
        node { hash from to value(encoding: ETHER) }
      }
    }`;  const [transfers, setTransfers] = useState([]);
  const [state, setState] = useState('initialize');
  const [errors, setErrors] = useState([]);
  const [stream, setStream] = useState(undefined);  const streamTransfers = async () => {
    setTransfers([]);
    setErrors([]);
    setState('connected');
    setErrors('');
    let currentTransfers = [];
    try {
      const stream = await dfuseClient.graphql(
        streamTransfersQuery,
        async message => {
          if (message.type === 'error') {
            setErrors([
              'An error occurred',
              ...message.errors.map(error => error.message),
              ...errors
            ]);
          }

          if (message.type === 'data') {
            const {
              node: newTransfer,
              cursor
            } = message.data.searchTransactions;

            currentTransfers = [newTransfer, ...currentTransfers];
            setTransfers(currentTransfers);
            stream.mark({ cursor });
          }

          if (message.type === 'complete') {
            setState('completed');
          }
        }
      );
      setStream(stream);
    } catch (errors) {
      setErrors(JSON.stringify(errors));
      setState('completed');
    }
  };  const onStop = async () => {
    setState('completed');
    if (stream === undefined) {
      return;
    }
    try {
      await stream.close();
      setStream(undefined);
    } catch (error) {
      setErrors(
        `Unable to disconnect socket correctly: 
          ${JSON.stringify(error)}
        `
      );
    }
  };

  const onClose = () => {
    setState('completed');
  };

  const onError = error => {
    setErrors(
      `Unable to disconnect socket correctly: 
      ${JSON.stringify(error)}
    `
    );
  };  const renderTransfer = (transfer, index) => {
    const { hash, from, to, value } = transfer;
    return hash ? (
      <code key={index} className='App-transfer'>
        Transfer
        <br />
        {`From: ${from} -> To: ${to}`}
        <br />
        {`Value: ${value} Hash: ${hash}`}
        <hr />
      </code>
    ) : (
      <code key={index} className='App-transfer'>
        {transfer}
      </code>
    );
  };

  const renderTransfers = () => {
    return (
      <div className='App-infinite-container'>
        {transfers.length <= 0
          ? renderTransfer('Nothing yet, start by hitting Launch!')
          : transfers.reverse().map(renderTransfer)}
      </div>
    );
  };

  const renderError = (error, index) => {
    if (error === '') {
      return <br key={index} className='App-error' />;
    }

    return (
      <code key={index} className='App-error'>
        {error}
      </code>
    );
  };

  const renderErrors = () => {
    if (errors.length <= 0) {
      return null;
    }

    return <div className='App-container'>{errors.map(renderError)}</div>;
  };

  return (
    <div className='App'>
      <header className='App-header'>
        <h2>Stream Ethereum Transfers</h2>
        {renderErrors()}
        <div className='App-buttons'>
          <button className='App-button' onClick={streamTransfers}>
            Launch
          </button>
          <button className='App-button' onClick={onStop}>
            Stop
          </button>
        </div>
        <main className='App-main'>
          <p className='App-status'>
            {`Connected: ${
              state === 'connected'
                ? 'Connected (Showing last 100 transfers)'
                : 'Disconnected'
            }`}
          </p>
          {renderTransfers()}
        </main>
      </header>
    </div>
  );
}
export default App;


.App {
  text-align: center;
}

.App-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: white;
}

.App-error {
  display: block;
  color: firebrick;
  font-size: 0.85rem;
  margin: 0.25rem 0;
  text-align: left;
}

.App-main {
  width: 60%;
  margin: 0.25rem auto;
}

.App-infinite-container {
  border: 2px solid #FF4661;
  padding: 0.75rem 0.5rem;
  height: 500px;
  overflow-x: hidden;
  overflow-y: scroll;
}

.App-transfer {
  display: block;
  color: #2d234c;
  font-size: 0.85rem;
  margin: 0.25rem 0;
  text-align: left;
}

.App-button {
  background-color: #F3F3F7;
  border: 2px solid #FF4661;
  color: #FF4661;
  padding: 0.5rem;
  margin: 0.25rem;
  font-size: 1rem;
  font-weight: 700;
}

.App-status {
  color: #FF4661;
  padding: 0.5rem 0;
  margin: 0 0;
  font-size: 1rem;
  text-align: left;
}