POSTED a month ago

Truffle: Adding a frontend with react box

Earlier in the series, we took a look at how to setup Truffle and use it to compile, deploy and interact with our Bounties.sol smart contract.

This article will walk through the steps required to add a simple react.js front end to our Bounties dApp so that users can interact with our smart contract using their web browser.

Source code for this tutorial can be found here.

Truffle Box

Truffle boxes are helpful boilerplate code, pre-configured to help you get up and running quickly developing your dApp.

In this article, we'll be using the react box which is essentially an example truffle project which has been merged with an ejected create react app to create a barebones solidity and react example dApp.

There are also other truffle boxes with boilerplate for other front-end frameworks such as:

You can read more about truffle boxes here.

Prerequisites

NODEJS 7.6+

Since web3.js and truffle executions are asynchronous, we'll be using async/await to simplify our test code. You’ll have to upgrade to Node 7.6 or higher.

TRUFFLE

$ npm install -g truffle

Read more on installing truffle here.

Development Blockchain: Ganache-CLI

In order to deploy our smart contract, we’re going to need an Ethereum environment to deploy to. For this, we will use Ganache-CLI to run a local development environment

$ npm install -g ganache-cli

Metamask compatible web browser

Chrome or Firefox

Unboxing Truffle React

To use a truffle box, we simply run the truffle unbox command:

$ truffle unbox react

Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!

Commands:

  Compile:              truffle compile
  Migrate:              truffle migrate
  Test contracts:       truffle test
  Test dapp:            npm test
  Run dev server:       npm run start
  Build for production: npm run build

This creates a new truffle project with an example SimpleStorage dApp example:

  • config/: contains configuration for the webapp application build including webpack config
  • contracts/: store original codes of the smart contract. We will place our Bounties.sol file here.
  • migrations/: instructions for deploying the smart contract(s) in the “contracts” folder.
  • public/: store static web files
  • scripts/: react build and start scripts
  • test/: tests for your smart contract(s), truffle supports tests written in both Javascript and Solidity, well learn about writing tests in the next article
  • truffle.js: configuration file.
  • truffle-config.js: configuration document for windows user.

To test everything is working we'll need to update the truffle.js file so that we can deploy the contract and test the dApp.

Add the following extract to the truffle.js file inside the module.exports section to configure our local development environment:

networks: {
    development: {
      network_id: "*",
      host: 'localhost',
      port: 8545
    }
}

Your truffle.js file should look like this:

In a separate terminal start ganache-cli:

$ ganache-cli

Ganache CLI v6.1.3 (ganache-core: 2.1.2)

Available Accounts
==================
(0) 0xf76f2626937df45f5bd615872f33add8f4ca5d5d
(1) 0xc4aae7e1d25963a454211b42f0ab7542dcc1abb8
(2) 0x20f23b739cc9133fd6f9aa068ad5f75e37e83e3b
(3) 0xba3bfdf6628ee4ff163df572faffbad86e7c605c
(4) 0x5b9751043a0d203b618dc8252ededba673362f82
(5) 0xb3ab7896997057dd040fc3a8b6bc9bf60b3f5cc0
(6) 0x8beb7b2bdb96cea4f789eff5182b7c4c64b2ea50
(7) 0x211ee29584f25256f303463b776ede6088f5dccf
(8) 0x49f2849f418643ccac8fdb27ce45de9070ac5cec
(9) 0x28814e158cf5596df78c32951809e0ffccd11030

Private Keys
==================
(0) e4c690778581c79e845981226507e8fd0d2852d76ae1825faec81c578f70d988
(1) b682d5638d969babcdad976ef87a22cb4e66bbb834dd3e9c71ed476bc12c8654
(2) fd6338cbf1b75394441680c50923e233d73c753afb7659516810a8ae38f948a5
(3) 3704987017ed0a08bf4942e34001ec162da909976e338a3dc47cc8becda991f3
(4) 2ba1e9af08641d85097f2c8e273c8df5648264d6bcd2d55fd2d3a188c875d224
(5) 2da1d96d0600c5c2cce7adf1d2851c343f1fbdce214493127fcf09a3d73d90e8
(6) e925893f93f25d4754dbfddb03a935dae9bdb966adecb3bd4bac05ba5bcf3117
(7) 9bc897b1775c9ea4d8fe57bbb0e42aa6bbf9da43de7ee73b1f3cb6948c3a5df3
(8) 1232ca9fa7bae16f4dbf225a220fcf9a4eae6e42dc933b212aa78c7f004d0c38
(9) 020c5813080277bae9e7717e849ba2647a703a27a72253d77a25457b966f7127

HD Wallet
==================
Mnemonic:      cupboard spawn carpet person shield knee orange neglect plunge onion acid say
Base HD Path:  m/44'/60'/0'/0/{account_index}

Gas Price
==================
20000000000

Gas Limit
==================
6721975

Listening on localhost:8545

Now back in truffle, we will need to first deploy the SimpleStorage.sol smart contract by runningtruffle migrate

$ truffle migrate

Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xb16699259a3912d2122ba01328bd20b26384bab8ffdf86aba003469fac690fbd
  Migrations: 0xdc83389f68d87527198f196e992dc779704f9429
Saving successful migration to network...
  ... 0xbd0e22cd8b553f7affe77a52ed7a2653b5eccb50019b7442522fc2509b5130f3
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying SimpleStorage...
  ... 0xe51b3c5de76d11a0c3680f01b84f683d6a598bdf9e1309e4e5b19d44ba127eb4
  SimpleStorage: 0xddc10ff26e66cbdf2fb9fbc6ace0394378680ee1
Saving successful migration to network...
  ... 0x8480bb95a9c09cb95dc20530cd9dd93ee9d9318a9903362c87b1ef8ad5554d3d
Saving artifacts...

Then start the react application by running npm run start

$ npm run start


Compiled successfully!

The app is running at:

  http://localhost:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

You should see the following when you visit http://localhost:3000 in your browser:

Setup Project For Our Bounties dApp

Now that we have the example project up and running, we can now start adapting it for our bounties dApp, we'll need to complete the following steps:

  1. Install react-bootstrap dependancies we'll be using for our UI
  2. Replace the SimpleStorage.sol smart contract with our Bounties.sol smart contract
  3. Update the migrations configuration to deploy our Bounties.sol smart contract
  4. Update App.js with logic required to interact with our Bounties.sol smart contract

So first, let's install our react-bootstrap dependancies:

$ npm install react-bootstrap --save
$ npm install --save bootstrap@3.3.7 --save
$ npm install react-bootstrap-table --save

Next, delete the contracts/SimpleStorage.sol file and replace it with the Bounties.sol file we developed previously:

Next, update the migration file 2_deploy_contracts.js to deploy our Bounties.sol smart contract:

var Bounties = artifacts.require("./Bounties.sol");

module.exports = function(deployer) {
  deployer.deploy(Bounties);
};

Now we have everything set up, we can now start updating our main application logic within the src/App.js file.

NOTE: Usually with react.js we would structure our app with components and use a state management library such as redux, however for the purpose of this tutorial we will just be focusing on the web3 components required for this dApp.

So in order to interact with our Bounties.sol smart contract, our App.js file will need to import the ABI (Application Binary Interface) which will be generated by Truffle during compilation. Currently src/App.js is configured to import the SimpleStorage.json file, let's update this:

From:

import React, { Component } from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

To:

import React, { Component } from 'react'
import BountiesContract from '../build/contracts/Bounties.json'
import getWeb3 from './utils/getWeb3'

You will also notice that we are importing getWeb3 which we need to instantiate a web3.js object that we can use to interact with an Ethereum node using Javascript. Let's take a look at this file:

import Web3 from 'web3'

let getWeb3 = new Promise(function(resolve, reject) {
  // Wait for loading completion to avoid race conditions with web3 injection timing.
  window.addEventListener('load', function() {
    var results
    var web3 = window.web3

    // Checking if Web3 has been injected by the browser (Mist/MetaMask)
    if (typeof web3 !== 'undefined') {
      // Use Mist/MetaMask's provider.
      web3 = new Web3(web3.currentProvider)

      results = {
        web3: web3
      }

      console.log('Injected web3 detected.');

      resolve(results)
    } else {
      // Fallback to localhost if no web3 injection. We've configured this to
      // use the development console's port by default.
      var provider = new Web3.providers.HttpProvider('http://127.0.0.1:9545')

      web3 = new Web3(provider)

      results = {
        web3: web3
      }

      console.log('No web3 instance injected, using Local web3.');

      resolve(results)
    }
  })
})

export default getWeb3

The above extract returns a promise which when the window loads will check if a web3 provider has already been loaded by the browser:

  • If so, it will return a web3 object with the current web3 provider
  • If not, it will return a web3 object with a HttpProvider for a node located at http://127.0.0:9545

NOTE: Usually if we were to use our local development environment, our node would be running on port 8545 and not 9545. However, in this tutorial, we'll be running our app in the browser, and rely on the browser to supply us with our web3 object so no need to update this.

Web3 Provider

A Web3 Provider tells our web3.js instance which ethereum node to send our RPC instructions too.

Our application will be running in the browser and so we need a way for users to sign transactions with their private key so that they can be sent to an ethereum node for processing.

For this we'll need a way to manage a user private key(s), in the browser, without rolling our own wallet application.

There are already services that handle this for you. The most popular of these is Metamask.

Metamask injects their web3 provider into the browser in the global JavaScript object web3. So our getWeb3 function above will return the Metamask web3 enabled provider when its loaded in a browser with Metamask installed. We'll install and setup Metamask later, when we are ready to test our app.

Component Will mount

Let's take a look at the componentWillMount() function :

componentWillMount() {
    // Get network provider and web3 instance.
    // See utils/getWeb3 for more info.

    getWeb3
    .then(results => {
      this.setState({
        web3: results.web3
      })

    // Instantiate contract once web3 provided.
      this.instantiateContract()
    })
    .catch(() => {
      console.log('Error finding web3.')
    })
}

componentWillMount() is a react lifecycle method which is called just before the initial render, we use this react lifecycle event to initiate our web3 instance by calling getWeb3 and also instatiating our contract instance object by calling this.instantiateContract(). This ensures our contract instance and web3 objects are ready for when our application renders.

Instantiate Contract

We'll need to update this function to instantiate our Bounties contract instead of the example SimpleStorage contract, override the instantiateContract() function with the following extract:

   async instantiateContract() {
    /*
     * SMART CONTRACT EXAMPLE
     *
     * Normally these functions would be called in the context of a
     * state management library, but for convenience I've placed them here.
     */

     const contract = require('truffle-contract')
     const bounties = contract(BountiesContract)

     bounties.setProvider(this.state.web3.currentProvider)


     let instance = await bounties.deployed()
     this.setState({ bountiesInstance: instance })
  }

Here we import truffle-contract this makes it easy to interact with the contracts by providing better control flow and using promises. You can read more about truffle-contract here.

We then use our BountiesContract compiled JSON we imported earlier to instantiate a bounties truffle contract:

const bounties = contract(BountiesContract)

We need to tell our bounties truffle contract object which provider to use so it knows which node to send transactions to. We can do this by calling the setProvider method with the provider from our web3 object we previously saved in our state.

bounties.setProvider(this.state.web3.currentProvider)

Great now our bounties truffle contract is wired up, we can use it to get our deployed bounties instance and save it in our state:

let instance = await bounties.deployed()
this.setState({ bountiesInstance: instance })

Since we're adding a new state variable bountiesInstance we need to update our constructor to define the initial value of this state variable, we can also remove the storageValue variable since we won't be using it.

constructor(props) {
super(props)

    this.state = {
      bountiesInstance: undefined,
      web3: null
    }
}

Issuing a Bounty

Great, now we have everything setup so when the application renders we will have a web3 instance using the metamask provider to sign transactions and send them to an ethereum network. We also have a bountiesInstance web3 object which is configured to interact with our deployed Bounties.sol smart contract on the same ethereum network.

We're now ready to issue a bounty, for this we'll need to render a form which will allow the user to:

  • Specify some requirements as a text
  • Set the bounty amount in ETH
  • Input a deadline in epoch seconds
  • A button to submit the data to our smart contract
  • We'll also add a link so that the user can track the progress of their submitted transaction on etherscan

We'll update the render() react lifecycle method to display our form to the user:

render() {
    return (
      <div className="App">
              <Grid>
              <Row>
              <a href={this.state.etherscanLink} target="_blank">Last Transaction Details</a>
              </Row>
              <Row>
              <Panel>
              <Panel.Heading>Issue Bounty</Panel.Heading>
              <Form onSubmit={this.handleIssueBounty}>
                  <FormGroup
                    controlId="fromCreateBounty"
                  >
                    <FormControl
                      componentClass="textarea"
                      name="bountyData"
                      value={this.state.bountyData}
                      placeholder="Enter bounty details"
                      onChange={this.handleChange}
                    />
                    <HelpBlock>Enter bounty data</HelpBlock><br/>

                <FormControl
                      type="text"
                      name="bountyDeadline"
                      value={this.state.bountyDeadline}
                      placeholder="Enter bounty deadline"
                      onChange={this.handleChange}
                    />
                    <HelpBlock>Enter bounty deadline in seconds since epoch</HelpBlock><br/>

                <FormControl
                      type="text"
                      name="bountyAmount"
                      value={this.state.bountyAmount}
                      placeholder="Enter bounty amount"
                      onChange={this.handleChange}
                    />
                    <HelpBlock>Enter bounty amount</HelpBlock><br/>
                    <Button type="submit">Issue Bounty</Button>
                  </FormGroup>
              </Form>
              </Panel>
              </Row>
              </Grid>
            </div>
    );
}

Above we render the following components:

Etherscan link

A hyperlink whose href attribute is controlled by the state variable etherscanLink

Form Create Bounty

A form where the submit button is handled by the function handleIssueBounty

Bounty Data Text Area

A text area field whose value is controlled by the state variable bountyData and its onChange callback is handled by the function handleChange

Bounty Deadline Input Field

A text input field whose value is controlled by the state variable bountyDeadline and its onChange callback is handled by the function handleChange

Bounty amount input field

A text input field whose value is controlled by the state variable bountyAmount and its onChange callback is handled by the function handleChange

Handle Change

We need to add the callback handleChange to update our form input data as it is updated by the user, add the function handleChange to App.js as follows:

// Handle form data change

handleChange(event)
{
    switch(event.target.name) {
        case "bountyData":
            this.setState({"bountyData": event.target.value})
            break;
        case "bountyDeadline":
            this.setState({"bountyDeadline": event.target.value})
            break;
        case "bountyAmount":
            this.setState({"bountyAmount": event.target.value})
            break;
        default:
            break;
    }
}

This function simply checks which input object was updated, and then updates the value in our component state.

Handle Issue Bounty

We add the issueBounty callback to handle the event which happens when the user submits the form. This function should take the current form input values from the component state, and use the bountiesInstance object to construct and send an issueBounty transaction with the form inputs as arguments.

// Handle form submit

async handleIssueBounty(event)
{
    if (typeof this.state.bountiesInstance !== 'undefined') {
      event.preventDefault();
      let result = await this.state.bountiesInstance.issueBounty(this.state.bountyData,this.state.bountyDeadline,{from: this.state.web3.eth.accounts[0], value: this.state.web3.toWei(this.state.bountyAmount, 'ether')})
      this.setLastTransactionDetails(result)
    }
}

A note on our transaction parameters from and value. Earlier in the series we learned:

  • from: should be set to the address of the bounty issuer (or user sending the transaction)
  • value: should be set to the amount of ETH to send to the contract in Weis

Since we are using Metamask we can set the from field to this.state.web3.eth.accounts[0] since metamask updates this to the currently selected account, everytime the user changes the account in the UI.

A wei is the smallest sub-unit of Ether — there are 10^18 wei in one ether. We can use web3 to convert our amount in ETH to weis before we send it in our transaction: this.state.web3.toWei(this.state.bountyAmount, 'ether')

Set Transaction Details

The function setLastTransactionDetails simply take the result of the transaction and updates the current etherscan link so the user is able to view the transaction in etherscan:

setLastTransactionDetails(result)
    {
    if(result.tx !== 'undefined')
    {
      this.setState({etherscanLink: etherscanBaseUrl+"/tx/"+result.tx})
    }
    else
    {
      this.setState({etherscanLink: etherscanBaseUrl})
    }
}

We'll need to also add a const etherscanBaseUrl which should be equal to the etherscan url of the environment we'll be deplying to rinkeby

const etherscanBaseUrl = "https://rinkeby.etherscan.io"

Update Constructor

Since we're adding new state variables etherscanLink, bountyData, bountyDeadline, and bountyAmount we need to update our constructor to define the initial values of these state variables.

We also need to bind our handleIssueBounty and handleChange callbacks to our component.

constructor(props) {
    super(props)

    this.state = {
      storageValue: 0,
      bountiesInstance: undefined,
      bountyAmount: undefined,
      bountyData: undefined,
      bountyDeadline: undefined,
      etherscanLink: "https://rinkeby.etherscan.io",
      web3: null
    }

    this.handleIssueBounty = this.handleIssueBounty.bind(this)
    this.handleChange = this.handleChange.bind(this)
}

Since we're using react-boostrap components for our input form we'll need to import the react-bootstrap components by adding this line to our imports section:

import {Form, FormGroup, FormControl, Button, HelpBlock, Grid, Row, Panel} from 'react-bootstrap'

Also add the boostrap css:

import 'bootstrap/dist/css/bootstrap.min.css';

Our App.js file should now look like this.

Deploy

We'll be deploying our bounties dApp to a public test network using Infura so lets ensure we have that setup first:

Infura

In order to send transactions to a public network, you need access to a network node. Infura is a public hosted Ethereum node cluster, which provides access to its nodes via an API

https://infura.io

If you do not already have an Infura account, the first thing you need to do is register for an account.

Once logged in, create a new project to generate an API key, this allows you to track the usage of each individual dApp you deploy.

Once your project is created, select the environment we will be deploying to, in this case Rinkeby, from the Endpoint drop down and copy the endpoint URL for future reference:

Make sure you save this token and keep it private!

HDWallet Provider

Infura, for security reasons, does not manage your private keys.We need to add the Truffle HDWallet Provider so that Truffle can sign deployment transactions before sending them to an Infura node.

https://github.com/trufflesuite/truffle-hdwallet-provider

We can install the HDWallet Priovider via npm

npm install truffle-hdwallet-provider --save

Generate Mnemonic

To configure the HDWallet Provider we need to provide a mnemonic which generates the account to be used for deployment.

If you already have a mnemonic, feel free to skip this part.

You can generate a mnemonic using an online mnemonic generator.

https://iancoleman.io/bip39

In the BIP39 Mnemonic code form:

  1. Select “ETH — Ethereum” from the “Coin” drop down
  2. Select a minimum of “12” words
  3. Click the “Generate” button to generate the mnemonic
  4. Copy and save the mnemonic located in the field “BIP39”, remember to keep this private as it is the seed that can generate and derive the private keys to your ETH accounts

  1. Scroll down the page to the Derived Addresses section and copy and save the Address this will be your Ethereum deployment account.

NOTE: Your private key will be displayed here, please keep this private.

Above the address we’ll be using is: 0x56fB94c8C667D7F612C0eC19616C39F3A50C3435

Configure Truffle

Now we have all the pieces set up, we need to configure truffle to use the HDWallet Provider to deploy to the Rinkeby environment. To do this we will need to edit the truffle.js configuration file.

First let's create a secrets.json file, this file will store your mnemonic and Infura API key so that it can be loaded by the hdwallet provider.

NOTE: Remember not to check this file into any public repository!

Next, copy the following extract to the truffle.js configuration file:

const HDWalletProvider = require('truffle-hdwallet-provider');
const fs = require('fs');

let secrets;

if (fs.existsSync('secrets.json')) {
 secrets = JSON.parse(fs.readFileSync('secrets.json', 'utf8'));
}

module.exports = {
  networks: {
    development: {
      network_id: "*",
      host: 'localhost',
      port: 8545
    },
    rinkeby: {
      provider: new HDWalletProvider(secrets.mnemonic, "https://rinkeby.infura.io/v3/"+secrets.infuraApiKey),
      network_id: '4'
    }
  }
};

The above as we discussed earlier in the series, configures truffle to deploy to an environment, rinkeby, using our mnemonic to derive the deployment private key and Infura as the deployment node.

Fund Your Account

We’re almost ready to deploy! However we need to make sure we have enough funds in our account to complete the transaction. We can fund our Rinkeby test account using the Rinkeby ETH faucet:

To request ETH from the faucet we need to complete the following steps:

  1. Post publicly our Ethereum deployment address from one of the following social network accounts: Twitter, Google+or Facebook, in this example we’ll be using Twitter

  2. Copy the link to the social media post

  1. Paste the link into the Rinkeby ETH faucet and select the amount of ETH to be sent

  1. Check the Rinkeby etherscan for the status of the transaction

    https://rinkeby.etherscan.io/address/ ETHEREUM DEPLOYMENT ADDRESS>

Deploy

To deploy simply run the truffle migrate command whilst specifying the network to deploy to. The networks are defined in the truffle.js configuration file we configured earlier in this article:

$ truffle migrate --network rinkeby
Compiling ./contracts/Bounties.sol...
Writing artifacts to ./build/contracts

Using network 'rinkeby'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x968d8f02e925c980148358dd7ce7101a0e97571af8a9a3aca0ef4d49c3b86c57
  Migrations: 0xfbb58f81a3e6c37b584dea4f4d37ec9d0b12d44d
Saving successful migration to network...
  ... 0xd2ed8ec3ab34626efd4fdf8303e19441d5841d721abf58e99807d5f2d5ab6c4a
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Bounties...
  ... 0xae1ed52cc12fdd209b94f1fd809d45d6b99f11562ae855d3394e5a15537da965
  Bounties: 0xd79b0d0c89a475f37368d36f98d4d0167a1a310d
Saving successful migration to network...
  ... 0x9a45c7ed0d5ce54a4591095b88df9385778594fcbe7d33b9ee1eed5aa77007cc
Saving artifacts...

Metamask

Before we can launch our dApp, we need to ensure we have Metamask enabled in our browser.

Metamask is a browser extension for Chrome and Firefox that lets users manage their Ethereum accounts and private keys, and provides an interface which users can use to interact with web applications which have web3 enabled.

Once installed in the browser, a user can interact with any browser dApp (website which has web3 enabled).

You can read more about Metamask here.

Install Metamask

  • Visit https://metamask.io
  • Select the option to install the extension in your browser, this should take you to the browser extension store

  • In the store, select the option to add the extension to the browser

  • Accept the permissions to install the Metamask extension

  • Once installed, open the extension in the browser extension tab and you will be prompted to create an account, if you already have an account you can import it here using your seed phrase

  • Whilst creating your account, Metamask will take you through accepting terms and conditions and then will prompt you to save your seed phrase and force you to re-enter it to sure you remember it

  • Your seed phrase is essentially the mnemonic which generates your accounts public/private key pair. Be sure to keep this safe as its the only way to recover you Metamask account if you forget your password or need to import your account because you updated your browser or bought a new laptop

  • Once done you'll be logged into Metamask with Metamask point at the Ethereum mainnet, so let's ensure we switch the environment to rinkbey test net before we begin

Fund Account

So now we have a new account set up in Metamask we'll want to fund it! We can do this by using the rinkeby faucetwe used earlier in the tutorial.

We just need to copy our accounts address to send ETH to which we can find in Metamask here:

Run the app!

Right we're now ready to run the app:

$ npm run start

Compiled successfully!

The app is running at:

  http://localhost:3000/

Note that the development build is not optimized. To create a production build, use npm run build.

Our dApp will be available at http://localhost:3000 and should look like this:

Issue a bounty

Right so if all is working we should be able to issue a bounty by filling out the details in this form and submitting via the issue Bounty button.

Let's submit the following data:

  • Bounty details: “Some requirements to receive 1 ETH”
  • Bounty deadline: 1691452800 - (August 8th 2023)
  • Bounty amount: 1 (Remember our app will covert this to weis before sending)

When you hit the “issue Bounty” button you should expect to see a Metamask popup similar to above. This is a transaction confirmation screen, you have the option to:

  • Cancel the transaction (you do not want to proceed with issuing the bounty)
  • Confirm the transaction (Metamask will sign the transaction with your private key and send it to a node in the ethereum network via Infura)
  • You can see we are sending 1 ETH or $270.75 at the time of writing this article
  • You also have options to set the gas fee, which can affect how long the transaction will take to confirm

Awesome, we're now able to issue a bounty in the frontend, however, once the transaction is confirmed, our UI has absolutely no idea. The saving grace is that Metamask will inform the user. However, we still need to show the user the details of the bounty was correctly added. Similarly, other users need to be able to see which bounties are currently available to fulfil!

Subscribing to events

To keep users updated, we're going to add a table which will display all the bounties which have been created.

Add the following extract to our render lifecycle in App.js:

<Row>
<Panel>
<Panel.Heading>Issued Bounties</Panel.Heading>
<BootstrapTable data={this.state.bounties} striped hover>
  <TableHeaderColumn isKey dataField='bounty_id'>ID</TableHeaderColumn>
  <TableHeaderColumn dataField='issuer'>Issuer</TableHeaderColumn>
  <TableHeaderColumn dataField='amount'>Amount</TableHeaderColumn>
  <TableHeaderColumn dataField='data'>Bounty Data</TableHeaderColumn>
</BootstrapTable>
</Panel>
</Row>

The above defines a table which uses the component state variable bounties which would be an array of json objects with the following dataFields to be displayed:

  • bonuty_id
  • issuer
  • amount
  • data

Earlier in the series when developing our smart contract we defined a BountyIssued event which emitted the dataFields in question:

event BountyIssued(uint bounty_id, address issuer, uint amount, string data);

This event is emitted every time a new bounty is created in our issueBounty function:

bounties.push(Bounty(msg.sender, _deadline, _data, BountyStatus.CREATED, msg.value));
emit BountyIssued(bounties.length - 1,msg.sender, msg.value, _data);
return (bounties.length - 1);

Using web3.js we can subscribe to these events in our web app and use this to populate our bounties array.

Add Event Listener

To subscribe to events we'll add a new function to App.js named addEventListener:

addEventListener(component) {

    var bountyIssuedEvent = this.state.bountiesInstance.allEvents({fromBlock: 0, toBlock: 'latest'})

    bountyIssuedEvent.watch(async function(err, result) {
      if (err) {
        console.log(err)
        return
      }

    if(result.args)
      {
        if(result.event === "BountyIssued")
        {
          var newBountiesArray = component.state.bounties.slice()
          newBountiesArray.push(result.args)
          component.setState({ bounties: newBountiesArray })
        }
      }

    })
}

The addEventListener method does the following:

  • Setting up a web3.js events object which will subscribe to all events from block 0 (the beginning of blockchain) to the latest
  • NOTE: Due to the web3.js 1.0 new event subscription model not yet being supported by Metamask, we're using a workaround which is to subscribe to all events.
  • Next, we use the .watch function to add a callback which we'll use to process each event we receive
  • When we receive an event we simply copy the current bounties array and push the event args into it and set that as our new bounties state.

A few more things, in our instantiateContract function we'll add a line at the end to start out events listener:

this.addEventListener(this)

Since we added a new state variable bounties we'll need to update our initial state in our contructor:

storageValue: 0,
bountiesInstance: undefined,
bountyAmount: undefined,
bountyData: undefined,
bountyDeadline: undefined,
etherscanLink: "https://rinkeby.etherscan.io",
*bounties**: [],*
web3: null

Our table is also using some react-bootstrap-table components so we'll need to import those and also import the react-boostrap-table css.

var ReactBsTable  = require('react-bootstrap-table');
var BootstrapTable = ReactBsTable.BootstrapTable;
var TableHeaderColumn = ReactBsTable.TableHeaderColumn;

import 'react-bootstrap-table/dist/react-bootstrap-table-all.min.css';

Your App.js file should now look like this.

We're now ready to relaunch our app, actually since its hot loading we shouldn't have to. Your app should now look like this in the browser:

Awesome, that's it! Now when you issue a bounty, the details of the bounty will be available in the table once the transaction has been processed.

Data Storage With IPFS

IPFS

Earlier in the series, we briefly introduced IPFS. To recap IPFS (InterPlanetary File System) is a peer to peer protocol for distributing files. Think of it as a filesystem using the ideas behind BitTorrent and Git where data is content-addressable and immutable.

You can learn more about IPFS here.

The requirements and evidence data fields of our issueBounty and fulfil bounty functions currently accept arbitrary length strings. Baring in mind the more data we save on the Ethereum network the more expensive our transaction, a user wanting to send a very long explanation in their requirements would be penalised since their transaction would be more expensive.

Quite a large portion of dApp development and design will centre around balancing the tradeoffs between security and decentralisation and the transaction cost to the user.

In any case anyway, we can reduce the transaction cost to the user is a plus!

Storing requirements in IPFS

So when issuing a bounty, also when fulfiling one. We can use IPFS to store the requirements text which would be of arbitrary length, and this would return us an id (hash of the content) which we can use to look the data up. This id or hash is always of fixed length and we would send this to the smart contract for reference instead of the requirements. This would mean that the length of the requirements input would no longer increase the cost of our issue bounty transaction!

Install IPFS-MINI

ipfs-miniis a Javascript wrapper built around the ipfs Javascript API

You can read more about IPFS-MINI here

Install ipfs-mini via npm:

$ npm install --save ipfs-mini

Update Our App to Use IPFS

First, let's create the following ipfs helper for our app, copy the following extract in a new file src/utils/IPFS.js

const IPFS = require('ipfs-mini');
const ipfs = new IPFS({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' });

export const setJSON = (obj) => {
    return new Promise((resolve, reject) => {
        ipfs.addJSON(obj, (err, result) => {
            if (err) {
                reject(err)
            } else {
                resolve(result);
            }
        });
    });
}

export const getJSON = (hash) => {
    return new Promise((resolve, reject) => {
        ipfs.catJSON(hash, (err, result) => {
            if (err) {
                reject(err)
            } else {
                resolve(result)
            }
        });
    });
}

The helper is a simple first creates an IPFS instance which connects to the IPFS node provided by Infura running at ipfs.infura.io:5001

It then gives us 2 functions:

  1. setJSON: which takes a JSON object as an argument and add its to IPFS returning the id or ipfsHash
  2. getJSON: which takes an ipfsHash or id and returns the JSON object which is references

We can now update our App.js file to make use of these two functions.

First, let's import our helper functions:

import { setJSON, getJSON } from './utils/IPFS.js'

Next, let's update our table definition to include a new field to display the ipfs document, also update the data field to the. name bountyData since we'll want to get the data from ipfs before displaying it.

<BootstrapTable data={this.state.bounties} striped hover>
  <TableHeaderColumn isKey dataField='bounty_id'>ID</TableHeaderColumn>
  <TableHeaderColumn dataField='issuer'>Issuer</TableHeaderColumn>
  <TableHeaderColumn dataField='amount'>Amount</TableHeaderColumn>
  *<TableHeaderColumn dataField='ipfsData'>Bounty Data</TableHeaderColumn>*
  *<TableHeaderColumn dataField='bountyData'>Bounty Data</TableHeaderColumn>*
</BootstrapTable>

Next, let's update our handleIssueBounty callback, here we need to add the bountyData to ipfs using the setJSON function and then use the result of this as the data argument to our issueBounty function.

async handleIssueBounty(event)
    {
    if (typeof this.state.bountiesInstance !== 'undefined') {
      event.preventDefault();
      const ipfsHash = await setJSON({ bountyData: this.state.bountyData });
      let result = await this.state.bountiesInstance.issueBounty(ipfsHash,this.state.bountyDeadline,{from: this.state.web3.eth.accounts[0], value: this.state.web3.toWei(this.state.bountyAmount, 'ether')})
      this.setLastTransactionDetails(result)
    }
}

Next, let's update our addEventListener callback, here we first get the JSON data from ipfs using the id or ipfsHash which will be present in the data field of our event. We then update the results args with 2 new fields before we add them to the new bounties array:

  • bountyData: the original requirements input from the bounty issuer
  • ipfsData: a link to the ipfs document containing the requirements
addEventListener(component) {

    var bountyIssuedEvent = this.state.bountiesInstance.allEvents({fromBlock: 0, toBlock: 'latest'})

    bountyIssuedEvent.watch(async function(err, result) {
      if (err) {
        console.log(err)
        return
      }

    if(result.args)
      {
        if(result.event === "BountyIssued")
        {
          var newBountiesArray = component.state.bounties.slice()

      //First get the data from ipfs and add it to the result
          var ipfsJson = {}
          try{
            ipfsJson = await getJSON(result.args.data);
          }
          catch(e)
          {

      }
          if(ipfsJson.bountyData !== undefined)
          {
            result.args['bountyData'] = ipfsJson.bountyData;
            result.args['ipfsData'] = ipfsBaseUrl+"/"+result.args.data;
          }
          newBountiesArray.push(result.args)
          component.setState({ bounties: newBountiesArray })
        }
      }

    })
}

Last but not least we need to define our const ipfsBaseUrl which is the url for a public IPFS gateway:

const ipfsBaseUrl = "https://ipfs.infura.io/ipfs";

Your App.js file should now look like this

Run Our dApp

We're now ready to relaunch our app

$ npm run start

Issue a bounty

Once running lets issue another bounty

Let's submit the following data:

  • Bounty details: “Some requirements that wiil earn will you 1.5 ETH”
  • Bounty deadline: 1691452800 - (August 8th 2023)
  • Bounty amount: 1.5 (Remember our app will covert this to weis before sending)

Once the transaction is confirmed and processed, our app should look like this:

That's all folk! You have successfully built and deployed a bounty dApp to the rinkeby development environment. The dApp which uses ipfs to store bounty requirements, and we've developed a front end to allow a user to issue a bounty!

Try it yourself

You'll have noticed that our front end does not currently support:

  • Cancelling a bounty
  • Fulfiling a bounty
  • Accepting a fulfilment

Try adding UI components so users can use these features.