Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save danazkari/bc534ce1a00235189eb66033a62f4b38 to your computer and use it in GitHub Desktop.
Save danazkari/bc534ce1a00235189eb66033a62f4b38 to your computer and use it in GitHub Desktop.
Smart Contracts with React (how to be the coolest kid on the block).md

Smart Contracts with React (how to be the coolest kid on the block)

Recommended snack and song:

220ml of dark roasted full bodied coffee brewed in french press accompanied by a banana while listening to Liquid Tension Experiment

tl;dr: You can go ahead and clone this repo, it's got the end result of this in-depth tutorial bellow.

Here's what we will be doing for becoming smart-contract heroes! - Install the dev environment plus MetaMask. - Develop a very simple voting smart contract. - Develop a front-end for our DAPP using React and Redux. - Configure and deploy our contract to Ropsten

If you don't know what Solidity is, please go check it out. There's an awesome video library you can watch to get a firm grasp on developing smart contracts with Solidity. This is not a requisite for this blogpost, just thought it was important to share the videos, since their content is pretty solid.

The Dev Environment

We need to install our dev environment, for which we're going to install truffle which will help us get started quickly:

$ npm install -g truffle

And now, for running a local blockchain for our development environment, there are several ways of doing it, but the easiest is to download Ganache, once you download it and execute it, you'll have a fully functional blockchain ethereum network for you to deploy and test your contracts.

Ganache UI

Note your network ID and RPC Server Port, typically the network ID is 5777 and the server hosted in 127.0.0.1:7545.

Let's start playing!

We don't have to start from scratch to develop our app, we're going to use truffle boxes, which are boilerplate projects that include a bunch of functionalities already built in to bootstrap our DAPP ideas:

$ mkdir voting-dapp && cd $_
$ truffle unbox react
$ npm install --save env-cmd history material-ui@next material-ui-icons react react-dom react-redux react-router-dom react-router-redux@next redux redux-promise-middleware truffle-hdwallet-provider

In order to get the environment started the right way, add a file to the root of the application folder called .env:

NODE_PATH=src/
REACT_APP_LOCAL_BLOCKCHAIN_PORT=7545

The first bit, will allow our React application to import modules using an absolute path instead relative, this will avoid the typical import foo from '../../../../foo'; which are hard to read and maintain. And the second one, lets you configure which local network blockchain port to use, it's set up to use the default Ganache network port.

Now, to the package.json file, let's edit the scripts section so it looks like this:

  "scripts": {
    "start": "env-cmd ./.env node scripts/start.js",
    "build": "env-cmd ./.env node scripts/build.js",
    "test": "env-cmd ./.env node scripts/test.js --env=jsdom"
  },

Once we've set up the dependencies and scripts, go ahead and delete contracts/SimpleStorage.sol file and create our own contracts/Voting.sol:

pragma solidity ^0.4.18;

contract Voting {
  mapping (bytes32 => uint8) public votes;
  bytes32[] private candidateList;

  event UpdateCandidates();

  function getCandidateVotes(bytes32 candidate) public view returns (uint8) {
    assert(doesCandidateExist(candidate));

    return votes[candidate];
  }

  function listCandidates() public view returns (bytes32[]) {
    return candidateList;
  }

  function postulateCandidate(bytes32 candidate) public {
    assert(!doesCandidateExist(candidate));

    candidateList.push(candidate);
    UpdateCandidates();
  }

  function voteForCandidate(bytes32 candidate) public {
    assert(doesCandidateExist(candidate));

    votes[candidate] += 1;
    UpdateCandidates();
  }

  function doesCandidateExist(bytes32 candidate) internal view returns (bool) {
    for (uint i = 0; i < candidateList.length; i++) {
      if (candidateList[i] == candidate) {
        return true;
      }
    }
    return false;
  }
}

This is just a sample solidity contract, not the best solidity code out there, but will do the trick.

Now open up migrations/2_deploy_contract.js and replace it's contents with:

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

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

This will instruct the migrate command to always look for our contract and deploy it.

And for testing of our smart contract, delete all files in test/ folder and place test/voting.js file in there:

const Voting = artifacts.require('./Voting.sol');

const toAscii = function(hex) {
    let str = '',
        i = 0,
        l = hex.length;
    if (hex.substring(0, 2) === '0x') {
        i = 2;
    }
    for (; i < l; i+=2) {
        const code = parseInt(hex.substr(i, 2), 16);
        if (code === 0) continue; // this is added
        str += String.fromCharCode(code);
    }
    return str;
};

contract('Voting', (accounts) => {
  it('...should start with an empty candidates list', () => {
    let votingInstance;
    return Voting.deployed()
      .then(instance => {
        votingInstance = instance;

        return votingInstance.listCandidates.call({from: accounts[0]});
      })
      .then(candidateList => assert.equal(candidateList.length, 0, 'The candidate list is not empty.'))
  });

  it('...should postulate a new candidate', () => {
    let votingInstance;
    const candidate = 'John Doe';
    return Voting.deployed()
      .then(instance => {
        votingInstance = instance;
        return votingInstance.postulateCandidate(candidate, {from: accounts[0]})
      })
      .then(() => votingInstance.listCandidates.call({from: accounts[0]}))
      .then(candidateList => candidateList.map(toAscii))
      .then(candidateList => {
        assert.include(candidateList, candidate, 'Candidate is not on the list');
      });
  });

  it('...should vote for candidate', () => {
    let votingInstance;
    const candidate = 'John Doe';

    return Voting.deployed()
      .then(instance => {
        votingInstance = instance;
        return votingInstance.voteForCandidate(candidate, {from: accounts[0]});
      })
      .then(() => votingInstance.getCandidateVotes.call(candidate, {from: accounts[0]}))
      .then(response => response.c[0])
      .then((votes) => {
        assert.equal(votes, 1, 'Candidate does not have votes');
      })
  });

  it('...should get votes for the candidate', () => {
    let votingInstance;
    const candidate = 'John Doe';

    return Voting.deployed()
      .then(instance => {
        votingInstance = instance;
        return votingInstance.votes.call(candidate, {from: accounts[0]});
      })
      .then(() => votingInstance.getCandidateVotes.call(candidate, {from: accounts[0]}))
      .then(response => response.c[0])
      .then((votes) => {
        assert.equal(votes, 1, 'Candidate does not have votes');
      })
  })
});

We also need to configure our deployment scripts by replacing the contents of truffle.js file:

var HDWalletProvider = require('truffle-hdwallet-provider');

var infuraApiKey = 'YOUR GENERATED INFURA API KEY';

var mnemonic = 'YOUR 12 WORDS MNEMONIC';

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  networks: {
    development: {
      host: "127.0.0.1",
      port: 7545,
      network_id: '5777'
    },
    ropsten: {
      provider: function() {
        return new HDWalletProvider(mnemonic, 'https://ropsten.infura.io/' + infuraApiKey);
      },
      network_id: 3,
      gas: 4612388
    }
  }
};

Note: lines 3 and 5 we'll configure later on when we're preparing for Ropsten deployment.

Cool! Now we can start testing our smart contract:

$ truffle test

truffle testing

Since our tests are passing flawlessly, let's push them to our Ganache local blockchain network (make sure Ganache is running first):

$ truffle compile
$ truffle migrate

Deploying locally

The next part is developing the front-end app, and because it's an app with React + Redux + React-Router, there's a whole bunch of files, so instead of copying/pasting every file, type these commands in the terminal (inside your project root of course):

$ mv src src-backup
$ curl -L https://www.dropbox.com/s/zx16pkwyp1u86eq/src.zip\?dl\=1 | bsdtar -xvf-

That should create a backup of the src folder, and then download and unzip the source code of the voting-dapp front-end.

However, I'm going to explain some key files so you understand how to communicate with your smart contracts. For instance, in the build/contracts/ folder, you'll find several .json files, one per contract, those files are known as ABI which stands for Application Binary Interface, and they hold all of the information about each contract.

This ABI files, are made when you truffle compile, and if you open src/contracts-api/VotingContract.js:

import { toAsciiFromByte32, getWeb3 } from 'utils'
import VotingContractMeta from '../../build/contracts/Voting.json'
import contract from 'truffle-contract'


let instance = null

export default class VotingContract {
  constructor() {
    if (!instance) {
      instance = this
      this.web3 = getWeb3()
      this.contract = contract(VotingContractMeta)
      this.contract.setProvider(this.web3.currentProvider)
    }

    return instance
  }

  async updateCandidatesEventListener(callback) {
    const contractInstance = await this.contract.deployed()
    const updateCandidatesEvent = contractInstance.UpdateCandidates()
    return updateCandidatesEvent.watch(callback)
  }

  async getVotes() {
    const contractInstance = await this.contract.deployed()
    return contractInstance.votes();
  }

  async proposeCandidate() {
    // TODO: This could receive a candidate's name as a parameter
    const getRandomUserURL = 'https://randomuser.me/api/?nat=us'
    const { eth: { accounts: [ account ] } } = this.web3
    const contractInstance = await this.contract.deployed()
    const {
      results: [{
        name: { first, last },
        id: { value: id },
      }]
    } = await fetch(getRandomUserURL)
      .then(response => response.json())

    return contractInstance
      .postulateCandidate(`${first} ${last} ${id}`, { from: account })
  }

  async castVote(candidateName) {
    const { eth: { accounts: [ account ] } } = this.web3
    const contractInstance = await this.contract.deployed()
    return contractInstance.voteForCandidate(candidateName, { from: account })
  }

  async getAllCandidates() {
    const contractInstance = await this.contract.deployed()
    const candidateList = (await contractInstance.listCandidates())
      .map(candidate => toAsciiFromByte32(candidate))

    return Promise.all(candidateList.map(
        (candidate) => contractInstance.getCandidateVotes.call(candidate)
      ))
      .then(allVotes => allVotes.map((votes, index) => ({
        name: candidateList[index],
        votes: Number(votes.toString()),
      })))
  }

  async getCandidateVotes(candidate) {
    const contractInstance = await this.contract.deployed()
    const result = await contractInstance.getCandidateVotes.call(candidate.name)
    return {
      candidate: candidate.name,
      votes: Number(result.toString())
    }
  }
}

You can see in the constructor (from lines 11 to 14) where we're getting a reference to the Web3 provider instance, then we create a contract using the ABI file and set it's provider.

After that, it becomes super easy to operate on the contract, we get an instance reference of the deployed contract using async/await and then just call the function we're interested in, then return the value of said function. truffle-contract already abstracts all of the complexity for you and gives you a nice API to use.

This file acts as an abstraction layer for custom behavior for each function.

This leads me to the other important aspect, the src/modules/candidates/candidates.actions.js file:

import {
  GET_CANDIDATES,
  VOTE,
  NEW_CANDIDATE,
} from './candidates.constants'
import { VotingContract } from 'contracts-api'

export const getCandidates = () => {
  return {
    type: GET_CANDIDATES,
    payload: (new VotingContract()).getAllCandidates(),
  }
}

export const postulateNewCandidate = () => {
  return {
    type: NEW_CANDIDATE,
    payload: (new VotingContract())
      .proposeCandidate({ type: 'random' })
      .then(() => (new VotingContract()).getAllCandidates()),
  }
}

export const vote = (candidateName) => {
  return {
    type: VOTE,
    payload: (new VotingContract())
      .castVote(candidateName)
      .then(() => (new VotingContract()).getAllCandidates()),
  }
}

These are pretty standard redux action creator functions, but they're using redux-promise-middleware, which makes the actions look a lot simpler. You can see how easy it is to call each contract function using our custom abstraction layer.

Also, you can listen to events being published by your contract, there's an event called UpdateCandidates and if you take a peek at src/modules/candidates/candidates.event-listeners.js file:

import { getCandidates } from 'modules/candidates'
import { VotingContract } from 'contracts-api'

export default async (dispatch, getState) => {
  await (new VotingContract())
    .updateCandidatesEventListener((error, candidateList) => {
      dispatch(getCandidates())
    })
}

This is a very simple function which is called from the src/store.js file, in line 36:

applyCandidatesEventListeners(store.dispatch, store.getState);

It gets the dispatch and getState functions passed from the store instance, and that's how it dispatches actions when events are called.

And that's about it, everything else is a pretty standard react application. Now we can go ahead and start our brand new front-end:

$ npm start

_Note: While on development mode, it's better to deactivate MetaMask if you already have it installed, it has some nasty bugs regarding events and synchronization with local RPC servers (like Ganache) _

Voting Demo Dapp

Success! You should have been presented with something like the picture above, you should be able to hit the plus fab button and the app should be creating new "candidates" each time. While you're at it, go ahead and take a look at the Transactions and Blocks tabs in Ganache, you'll see that each time you either vote or create a new candidate, a transaction is made and a block is added.

Deployment to Ropsten network

We will need MetaMask installed, this will keep things nice and easy. We'll use MetaMask to create ourselves an Ethereum account and fill it with free ETH (using a faucet) so we can deploy our smart contracts to the Ropsten network (that's a testing network, so that's why we can get ETH for free).

First, create yourself a secure password:

MetaMask password prompt

Then, copy the 12 word mnemonic you've been given somewhere safe:

MetaMask mnemonic

Copy them somewhere safe, seriously, they're important. Now you can go to Infura.io to get your API key by signing up. Once you do, put those 12 words and the Infura API key in their respective places in your truffle.js file:

//...
var infuraApiKey = 'YOUR GENERATED INFURA API KEY';

var mnemonic = 'YOUR 12 WORDS MNEMONIC';
//...

Then, on your MetaMask UI, select the Ropsten network:

Selecting Ropsten Network

Last thing, go to the MetaMask faucet and request some ether for your account, you'll need it to deploy your contracts:

MetaMask faucet

Wait for the transaction to finish and then that's it! Now, let's deploy to Ropsten:

$ truffle compile
$ truffle migrate --network ropsten

Done. You're now officially on Ropsten, now you only need your MetaMask pointing at Ropsten Network (like you already have) and you can interact with your deployed smart contract using your local react app (at least while your MetaMask is pointing to the Ropsten Network).

If you want to deploy your app as a real distributed application, you can use IPFS.io, which will help you deploy your application in a P2P protocol static hosting. In case you want to deploy your front-end on a typical centralized way, surge.sh is pretty good for that, and super easy to use.

That's it for this tutorial, hope you enjoyed the music and the snack. The source code for this application is at this repo, should you have any questions about implementations or the code in general, feel free to create issues on the Github repo or post them as comments bellow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment