RSK Workshop: Quiz App to dApp

Table of contents

Pre-requisites

Before we begin, you will need the following things set up on your system:

  1. The basics
  1. Install wallet browser 1.1 Nifty Chrome extension 1.2 Metamask instructions

  2. Get some R-BTC from the RSK faucet

  3. Truffle.

To install truffle, enter the following commands in your terminal;

$ npm install -g truffle
  1. Install an add-on to enable CORS.
  2. Swarm

Installing Swarm

The easiest way to install Swarm is via its pre-compiled releases. There are also instructions for compiling the source yourself instead.

Visit swarm.ethereum.org/downloads and select the appropriate package to install for your system. This page should automatically select and highlight the right one for you (in bold).

Example commands on Linux.

curl https://ethswarm.blob.core.windows.net/builds/swarm-linux-amd64-0.5.7-5ccfd995.tar.gz > swarm-linux-amd64-0.5.7-5ccfd995.tar.gz
tar -zxvf swarm-linux-amd64-0.5.7-5ccfd995.tar.gz
mkdir -p ${HOME}/swarm/bin
mv swarm-linux-amd64-0.5.7-5ccfd995/swarm ${HOME}/swarm/bin
echo 'export PATH=$PATH:${HOME}/swarm/bin' >> ~/.zshrc

If you have bashrc terminal you must replace ~/.zshrc for ~/.bashrc.

Mac OSX or Windows (with a POSIX-compliant shell such as git bash) should be pretty similar.

Close this shell and open up a new one, as we’ll need the updated PATH environment variable.

Let’s check that we have got a working binary.

swarm version
Swarm
Version: 0.5.8-unstable
Git Commit: 6faff7fcb6f25c706e75d8d3c8945c4231663b93
Go Version: go1.14.3
OS: linux

Next, let’s start swarm.

swarm
INFO [05-19|15:03:36.058] Maximum peer count                       ETH=50 LES=0 total=50
INFO [05-19|15:03:36.059] You don't have an account yet. Creating one...
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Unlocking swarm account 0xD1bCFFf13f996247d8A84a37bC7b32436B40c62F [1/3]
Passphrase:

Note that Swarm is a service which uses peer to peer networking. Your computer is one node of many connected to this same network, and talking to this same protocol. Therefore, the very first time that you start up Swarm on your computer, you will be prompted to create an account, which will be used to uniquely identify this particular node - that is what the password is for.

INFO [05-19|15:03:53.009] Starting peer-to-peer node               instance=swarm/v0.5.8-6faff7fc/linux-amd64/go1.14.3
INFO [05-19|15:03:53.065] New local node record                    seq=1 id=0f1272cb73bcf1ba ip=127.0.0.1 udp=30399 tcp=30399
INFO [05-19|15:03:53.065] Updated bzz local addr                   oaddr=5c31b4c2924e4689554b80893c663833de5852b32f090969860739dbdb1a69c0 uaddr=enode://d18081c0f7bf09c021d519e0d8351473def7a408820bffabc62bf2e878fd2ff84df3b46407ab347d632dfbec4f13cd7635ea2ee4c8fdace17c442ae032615d48@127.0.0.1:30399
INFO [05-19|15:03:53.065] Starting bzz service
INFO [05-19|15:03:53.065] Starting hive                            baseaddr=5c31b4c2
INFO [05-19|15:03:53.066] Detected an existing store. trying to load peers
INFO [05-19|15:03:53.066] hive 5c31b4c2: no persisted peers found
INFO [05-19|15:03:53.066] Swarm network started                    bzzaddr=5c31b4c2924e4689554b80893c663833de5852b32f090969860739dbdb1a69c0
INFO [05-19|15:03:53.066] bzzeth starting...
INFO [05-19|15:03:53.066] Starting outbox
INFO [05-19|15:03:53.066] Started P2P networking                   self=enode://d18081c0f7bf09c021d519e0d8351473def7a408820bffabc62bf2e878fd2ff84df3b46407ab347d632dfbec4f13cd7635ea2ee4c8fdace17c442ae032615d48@127.0.0.1:30399
INFO [05-19|15:03:53.066] Started Pss
INFO [05-19|15:03:53.066] Loaded EC keys                           pubkey=04fbdbfa2ee4034122e076512e390f8348cf1d2dd3a249f8f49ff5178e917cd18dccfc42cea2a2e906f25d7cb88b61b205a73b0bb2b07d43de0ca6c2708a0dc058 secp256=02fbdbfa2ee4034122e076512e390f8348cf1d2dd3a249f8f49ff5178e917cd18d
INFO [05-19|15:03:53.066] starting bzz-retrieve
INFO [05-19|15:03:53.066] Starting Swarm HTTP proxy                port=8500
INFO [05-19|15:03:53.068] Mapped network port                      proto=tcp extport=30399 intport=30399 interface=NAT-PMP(192.168.50.1)
INFO [05-19|15:03:53.069] IPC endpoint opened                      url=/home/bguiz/.ethereum/bzzd.ipc
INFO [05-19|15:03:53.070] Mapped network port                      proto=udp extport=30399 intport=30399 interface=NAT-PMP(192.168.50.1)
INFO [05-19|15:03:53.248] New local node record                    seq=2 id=0f1272cb73bcf1ba ip=172.23.144.94 udp=30399 tcp=30399
ERROR[05-19|15:04:06.517] batch has timed out                      peer=3de6224e3c9c430f:656e6f64653a2f2f ruid=3716585580

Now visit http://localhost:8500 and you will see a web user interface for downloading and uploading files. Have a play around with this if you like, otherwise jump back into your terminal.

You should see output similar to this related to serving up the front end.

INFO [05-19|15:08:08.421] created ruid for request                 ruid=ffcc6158   method=GET url=/
INFO [05-19|15:08:08.421] respondHTML                              ruid=ffcc6158   code=200
INFO [05-19|15:08:08.422] request served                           ruid=ffcc6158   code=200 time=570.234µs
INFO [05-19|15:08:08.453] created ruid for request                 ruid=d89959fa   method=GET url=/favicon.ico
INFO [05-19|15:08:08.453] request served                           ruid=d89959fa   code=200 time=41.936µs

Objectives

  • Use your JS portfolio to create a web3 and blockchain portfolio by reusing old apps and transforming to dApps.
  • Learn smart contracts and decentralize storage.
  • Create a fun dApp.

Run the app

In order to run the simple JS app, follow the next instructions:

Clone repo

Clone this repo, using the commands below;

$ git clone https://github.com/rsksmart/quiz-dapp
$ cd quiz-dapp

Install dependencies

$ npm install

Run the project

$ npm run start

It should open a new tab in your browser.

Note the questions are stored in the questions.json file.

Sequence diagram

Sequence diagrams for app and dApp

App to dApp

It’s time to convert your simple plain JS app to an dApp!

Run swarm

Run swarm in a new terminal and don’t close it.

$ swarm

Open a new terminal and upload your questions to swarm decentralized storage. When completed, swarm returns a hash.

$ cd js
$ swarm --defaultpath questions.json up questions.json

# 54347e7150fdfa881f56d9845976b6d541930e60a16d6f5cd6877a6c3df31827

Copy the hash returned and get the file info:

$ curl -s http://localhost:8500/bzz-raw:/54347e7150fdfa881f56d9845976b6d541930e60a16d6f5cd6877a6c3df31827 | jq

{
  "entries": [
    {
      "hash": "809b82283b3d0c3459fde4340ecb6a7a297b1d25e6cab6084d21257ba29e82c2",
      "contentType": "application/json",
      "mode": 436,
      "size": 2014,
      "mod_time": "2020-06-23T03:40:25-05:00"
    }
  ]
}

You can verify the file content by sending a request to this new hash

curl -s http://localhost:8500/bzz-raw:/809b82283b3d0c3459fde4340ecb6a7a297b1d25e6cab6084d21257ba29e82c2 | jq

{
  "questions": [
    {
      "id": 1,
      "question": " ...? ",
      "nextId": 2,
      "previousId": null,

      ...

Get the questions from swarm

Important: You need to get CORS disabled!

Add the new data source in app.js:

Before:

mounted: () {
    axios.get('js/questions.json')
    ...
}

After:

mounted: () {
    axios.get('http://localhost:8500/bzz-raw:/809b82283b3d0c3459fde4340ecb6a7a297b1d25e6cab6084d21257ba29e82c2')
}

Your data source is not static anymore. Your quiz application is now loading its data from a decentralised file store. Congratulations, your app is on its way to becoming a DApp!

Initialize truffle

Change your directory and initialize truffle using the commands below;

$ cd ..
$ truffle init

It will create the contract folder, migrations, and truffle-config.js file.

Note: if you don’t have truffle installed, you can install it using the command below:

$ npm install -g truffle

Specify the solidity compiler version

In the truffle-config.js file, in the compilers section, we need to change the solidity compiler version

compilers: {
    solc: {
      version: "^0.4.14",    // Just change this line
    }
  }

Create a contract

In the contracts folder, create a new file named Quiz.sol.

Copy and paste the following code in your editor:

pragma solidity ^0.4.14;

contract Quiz {
    address public owner;
    uint8[4] rightAnswers = [2, 3, 1, 4];
    address[] public players;
    uint8[] public playerHits;
    string public questions;

    event AnswerEvent(uint8 hitsCounter);

    constructor() public {
        owner = msg.sender;
    }

    function getPlayerHits() public view returns (uint8[] memory) {
        return playerHits;
    }

    function getPlayers() public view returns (address[] memory) {
        return players;
    }

    function answerQuestions (uint8[] playerAnswers) public returns (uint8) {
        uint8 hits;

        for (uint i = 0; i < playerAnswers.length; i++) {
            if (playerAnswers[i] == rightAnswers[i]) {
                hits++;
            }
        }
        players.push(msg.sender);
        playerHits.push(hits);

        emit AnswerEvent(hits);

        return hits;
    }
}

Now, in the terminal run the compile command

truffle compile

Get the gas price

Get and save the gas price from testnet:

$ curl https://public-node.testnet.rsk.co/2.0.1/ -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":1}' > .gas-price-testnet.json

$ ls -al

This command will create a new file named .gas-price-testnet.json with the gas price from testnet and can be verified with the command ls -al

Config truffle

Install the Wallet Provider and dotenv dependency:

npm install @truffle/hdwallet-provider dotenv --save

Create a new file named .env in the root directory of your project and write your mnemonic on it.

$ touch .env
A_MNEMONIC='your twelve words mnemonic ...'

Remember: Don’t share or deploy your mnemonic never!

You should add the .env file to your .gitignore file.

echo ".env" >> .gitignore

Open your truffle-config.js file and add the following lines at the beginning of the file:

const fs = require('fs');
const HDWalletProvider = require('@truffle/hdwallet-provider');
require('dotenv').config();

const A_MNEMONIC = process.env.A_MNEMONIC;

const gasPriceTestnetRaw = fs.readFileSync(".gas-price-testnet.json").toString().trim();
const gasPriceTestnet = parseInt(JSON.parse(gasPriceTestnetRaw).result, 16);
if (typeof gasPriceTestnet !== 'number' || isNaN(gasPriceTestnet)) {
  throw new Error('unable to retrieve network gas price from .gas-price-testnet.json');
}

console.log(gasPriceTestnet);

Add the testnet rsk network

In the same truffle-config.js in the networks section, add the testnet configuration:

testnet: {
      provider: () => new HDWalletProvider(A_MNEMONIC, 'https://public-node.testnet.rsk.co/2.0.1/'),
      network_id: 31,
      gasPrice: Math.floor(gasPriceTestnet * 1.1),
      networkCheckTimeout: 1e9
    },

You can test your connection by running the following commands in your terminal

$ truffle console --network testnet
65000000
truffle(testnet)>

Run migrations

Add a new file named 2_deploy_contracts.js.

$ touch migrations/2_deploy_contracts.js

Then open 2_deploy_contracts.js and write the following code:

var Quiz = artifacts.require('Quiz');

module.exports = function (deployer) {
  deployer.deploy(Quiz)
}

Run the migrations:

If you are in the truffle console then just write migrate:

truffle(testnet)> migrate

Otherwise

$ truffle migrate --network testnet

Result of migrations

You should see something similar to the following output in your terminal.

$ truffle(testnet)> migrate

Compiling your contracts...
===========================
✔ Fetching solc version list from solc-bin. Attempt #1
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/Quiz.sol
✔ Fetching solc version list from solc-bin. Attempt #1
> Artifacts written to /home/dulce/dev/rsk/appjs2dapp-workshop-rsk-dulce/build/contracts
> Compiled successfully using:
   - solc: 0.4.26+commit.4563c3fc.Emscripten.clang

Starting migrations...
======================
> Network name:    'testnet'
> Network id:      31
> Block gas limit: 6800000 (0x67c280)

1_initial_migration.js
======================
   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x544c68d0a8b13d6a2471a3baa8c5358c052475749c183a79ac04d34913b8b7a5
   > Blocks: 0            Seconds: 21
   > contract address:    0x9BD85b119C9F0b491ef3D1AbD8d99C4C360896f3
   > block number:        957437
   > block timestamp:     1592959920
   > account:             0x8FCC0638F6F20cE2C468c7a7a2eA84b1cf6Cb1eE
   > balance:             2.750087998627249784
   > gas used:            196887 (0x30117)
   > gas price:           0.0715 gwei
   > value sent:          0 ETH
   > total cost:          0.0000140774205 ETH

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:     0.0000140774205 ETH

2_deploy_contracts.js
=====================
   Deploying 'Quiz'
   ----------------
   > transaction hash:    0xb8b6917afe56551b0ba2b0e937b574c21ddbf7cdc4662101100e21d6db847922
   > Blocks: 1            Seconds: 39
   > contract address:    0xF9F201aE6e34d8B4CC60f998413a161eF5FE65AF
   > block number:        957440
   > block timestamp:     1592960035
   > account:             0x8FCC0638F6F20cE2C468c7a7a2eA84b1cf6Cb1eE
   > balance:             2.750029175577249784
   > gas used:            780714 (0xbe9aa)
   > gas price:           0.0715 gwei
   > value sent:          0 ETH
   > total cost:          0.000055821051 ETH

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:      0.000055821051 ETH

Summary
=======
> Total deployments:   2
> Final cost:          0.0000698984715 ETH

Exit from truffle console with Ctrl + C or typing .exit

Modify the front end

Adding web3.js

In index.html, before the rest of the scripts, we are going to add the following: Generally all the scripts are included in the open and closed tag</body>

<script src="js/truffle-contract.js"></script>

Then, include the web3 CDN just before the js/app.js script tag

<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script src="js/app.js"></script>

The order of the scripts will be:

  1. truffle-contract.js <- truffle contracts library
  2. vue.js <- Vue CDN library
  3. bulma-steps.min.js <- Bulma steps (used to render the questions in a step by step form).
  4. axios.min.js <- The axios library. (used to get the questions data from DS).
  5. web3.min.js <- Web3 library
  6. app.js <- our app script
<script src="js/truffle-contract.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/bulma-steps@2.2.1/dist/js/bulma-steps.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
  <script src="js/app.js"></script>

Replace mounted method content

In the app.js file, clear the mounted method and add this new lines:

mounted () {
    this.initWeb3()
      .then(() => {
        console.log('App initialized')
      })
},

Replace the method section

Replace the method section

methods: {
    // get the questions from swarm !!!
    async initWeb3() {
      await axios.get('http://localhost:8500/bzz-raw:/07f1112168025f4c309b652fb364b9ea126728e7b0959338b0bf9f5187d474d0')
        .then(response => {
          this.questions = response.data.questions
        })

      if (window.ethereum) {
        this.web3Provider = window.ethereum
        try {
          // request account access
          await window.ethereum.enable()
        } catch (error) {
          console.error("user denied account access")
        }
      } else if (window.web3) {
        this.web3Provider = window.web3.currentProvider
      } else {
        this.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545')
      }
      // Inicialized web3 !!!
      web3 = new Web3(this.web3Provider)

      return this.initContract()
    },
    //Load the compiled contract information -> Quiz.json
    initContract: function (params) {
      return axios.get('../build/contracts/Quiz.json')
        .then(response => {
          const QuizArtifact = response.data
          this.contracts.Quiz = TruffleContract(QuizArtifact)
          // Set the provider for our contract
          this.contracts.Quiz.setProvider(this.web3Provider)
        })
    },
    selectOption: function (evt) {
      if (evt) {
        document.querySelectorAll('div.answer').forEach(element => {
          element.classList.remove('answer') // disable css hover effect
        })
        const element = evt.target
        const answer = element.dataset.i + 1
        const index =  parseInt(element.dataset.index) + 1

        const question = this.getQuestion(index)
        this.answers.push(answer)

        this.counter++

        if (this.counter === this.questions.length) {
          // show modal and return
          document.querySelector('div.modal').classList.add('is-active')
        }

        setTimeout(function () {
          // trigger click event on <a> next button
          document.querySelector('a[data-nav="next"]').click()
          document.querySelectorAll('div.option-box').forEach(element => {
            element.classList.add('answer') // enable css hover effect
          })
        }, 500) // Wait one second after answer each question.
      }
    },
    sendQuestions: function () {
      const contract = this.contracts.Quiz
      web3.eth.getAccounts((error, accounts) => {
        if (error) {
          console.error(error)
        }

        const account = accounts[0]

        this.contracts.Quiz.deployed()
          .then(instance => {
            const quizInstance = instance

            return quizInstance.answerQuestions(this.answers, { from: account })
          })
          .then(result => {
            document.querySelector('div.modal').classList.remove('is-active')
          })
          .catch(err => {
            console.error(err)
          })


      })
    },
    //method that return the question by index
    getQuestion: function (index) {
      for (let i = 0; i < this.questions.length; i++) {
        if (index === this.questions[i].id) {
          return this.questions[i]
        }
      }
      return false
    }
  }

Last step!

Now reload your app in a browser, and interact with it.

Congratulations, your app is now interacting with a smart contract (and decentralised storage). Your app is decentralised!