Scout App for monitoring

Wallet Web App

We will build a simple client UI that will run in the browser; on the backend, it will only interact with our smart contract.

Before starting, let’s make a clean redeploy of our smart contracts from yesterday:

rm -rf build truffle compile truffle develop (in second terminal) truffle migrate

Create a new directory called client that includes the index.html and main.js files. For the HTML, we’ll start with a simple skeleton file and some Bootstrap classes.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport"
     content="width=device-width, initial-scale=1, user-scalable=yes">
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
 <nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="#">DailyToken Wallet</a>
    </div>
    <ul class="nav navbar-nav">
      <li class="active"><a href="#">Home</a></li>
    </ul>
  </div>
</nav>
<div class="container-fluid">
</div>
</body>
</html>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="./main.js"></script>
</html>

For serving our project you can use any web server, we chose http-server that’s available as npm package:

Install it with:

npm install -g http-server

Then go into our client directory and run:

http-server .

Check balance

We start with the most basic thing: checking balance of an address. First, add an input field for entering the address. Place this in the container div.

<div class="row form-group">
      <div class="col-md-4">
        <div class="form-group">
          <label for="ownerAddress">Your address:</label>
          <input type="text" class="form-control" id="ownerAddress">
        </div>
      </div>
  </div>

Bellow that we’ll add a panel with a button for checking the balance.

<div class="col-md-4">
      <div class="panel panel-default">
        <div class="panel-heading">Check balance</div>
        <div class="panel-body" id="showBalance">Set your address and check your balance</div>
        <div class="panel-footer">
          <input type="button" class="btn btn-primary" id="checkBalanceBtn" value="Check Balance">
        </div>
      </div>
    </div>

The HTML by itself won’t be doing much. Let’s head over to our main.js file and start interacting with our smart contract. First of all we must connect to the network; that’s done with the Web3 library that we need to import in index.html, just above the main.js import.

<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>

Back in main.js, this is how we instantiate the Web3 object.

web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:9545"));

Next we need to copy the ABI of our smart contract. ABI stands for Application Binary Interface which contains the full interface definition of smart contracts. Basically it defines all the input and output types of all functions of the contract. When using Truffle, ABIs are generated in the compilation step and are part of the JSON files in the build folder. Head over there and look into DailyToken.json. The first key is the contract name and the second is the ABI. Just copy and paste this into a variable in main.js like so:

Full value not shown for brevity.

abi = [
    {
      "constant": true,
      "inputs": [],
      "name": "totalSupply",
      "outputs": [
        {
          "name": "",
          "type": "uint256"
        }
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    },
...

We must have the ABI definition in order to initialize our smart contract type and tell it which functions it has. TokenContract = web3.eth.contract(abi);

Next we instantiate our already-deployed smart contract from its blockchain address.

    contractInstance = TokenContract.at('0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f');

This address 0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f is the deployed address of the DailyToken contract on my local network; yours will be different. This can be seen in the output of the truffle migrate command.

That finishes the setup phase; now we can write our checkBalance function.

function checkBalance(address){
  let balance = contractInstance.balanceOf.call(address);
  return balance;
}

As you can see, the actual call to the smart contract function balanceOf is exactly the same as the calls we did in the Truffle console. No surprise there, since the Truffle console is a Javascript console. Now connect the checkBalance function to the click event of the Check Balance button.

$(document).ready(function() {
  $('#checkBalanceBtn').click(function(){
    owner = $('#ownerAddress').val();
    let balance = checkBalance(owner);
    $('#showBalance').html("Your balance is " + balance);
  });
});

We have a (minimal) working web app that interacts with our smart contract and uses no backend at all. Now let’s send some tokens.

Sending tokens

Add a new HTML form with input fields for sender and recipient addresses, the number of tokens we want to transfer, and a button to execute the transfer.

<div class="col-md-4">
      <div class="panel panel-default">
        <div class="panel-heading">Send Tokens</div>
        <div class="panel-body">
          <div class="form-group">
            <label>Sender address</label>
            <input type="text" class="form-control" id="senderAddress">
          </div>
          <div class="form-group">
            <label>Recipient address</label>
            <input type="text" class="form-control" id="recipientAddress">
          </div>
          <div class="form-group">
            <label>Amount</label>
            <input type="text" class="form-control" id="amount">
          </div>
        </div>
        <div class="panel-footer">
          <input type="button" class="btn btn-danger" id="sendTokensBtn" value="Send Tokens">
        </div>
      </div>
      </div>

Your form should be similar to this one.

image alt text

In main.js write the sendTokens function:

function sendTokens(senderAddr, recipientAddr, amount){
      contractInstance.transfer(recipientAddr, amount, {from: senderAddr}, function() {
        let recipientBalance = checkBalance(recipientAddr);
        alert("Recipient " + recipientAddr + " has: " + recipientBalance + " tokens");
      })
    }

It accepts the two addresses and the amount to transfer. Then it makes the call to the transfer function, quite similar to the call we did in the console. For the last parameter, it accepts a callback that gets executed when the transaction is finished.

Then connect this function to the click event of the Send Tokens button.

$('#sendTokensBtn').click(function(){
    let sender = $('#senderAddress').val();
    let recipient = $('#recipientAddress').val();
    let amount = $('#amount').val();
    sendTokens(sender, recipient, amount);
  })

Let’s try sending 19 tokens from the contract owner’s address (0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef), which currently holds all the tokens, to another account ( 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5). After hitting Send Tokens you should receive an alert that says:

Address 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5 has: 19 tokens.

To make sure, let’s check the balances of both addresses in our Check Balance panel. The owner’s address now has 9999981 tokens left, and the recipient address has 19 tokens.

I don’t like having to manually check balances. We’ll add a timer that will check the balance for the given address every second.

setInterval(function(){
    let owner = $('#ownerAddress').val();
    let balance = checkBalance(owner);
    $('#showBalance').html("Your balance is " + balance);
}, 1000)

Also, let’s remove the alert line from the sendTokens function, so now it looks like this:

function sendTokens(senderAddr, recipientAddr, amount){
  contractInstance.transfer(recipientAddr, amount, {from: senderAddr}, function() {
    let recipientBalance = checkBalance(recipientAddr);
  })
}

Now let’s set the owner’s address in the Check Balance panel, and send some more tokens to the other address. As you can see, the balance now updates right away. However, we can do better than this. Let’s set up a proper watcher for transfer events that will update the price only when a transfer happens, instead of running every second.

Listen for events

We need to listen for transfer events that change the balance of our address. This means listening for events where our address is the sender or receiver of tokens. To listen for incoming transfers we need this filter:

filter = contractInstance.Transfer({to: myAddr})

For outgoing transfers we need:

filter = contractInstance.Transfer({from: myAddr})

But to listen for both we need two separate listeners, or one listener that catches all events and does the filtering afterwards. We’ll go with the latter approach. First, comment out or remove the setInterval bit from earlier. Now add a button called Listen for Transfers that creates a listener for the address entered in Your Address input. Add this button in the first panel footer, next to the Check Balance button.

<div class="panel-footer">
                <input type="button" class="btn btn-primary" id="checkBalanceBtn" value="Check Balance">
          <input type="button" class="btn-success btn" id="listenAddress" value="Listen for Transfers">
        </div>

As a first draft, let’s create a function that will log the event result.

function createEventFilter(myAddr){
  filter = contractInstance.Transfer({});
  console.log("Listening for transfers to and from " + myAddr);
  filter.watch(function(error, result){
    if(!error){
      console.log("Transfer happened!!!");
      console.log(result)
    }else{
      console.log("Got event error");
      console.log(error);
    }
  });
}

First we get a handle for the events from our specific contract instance by passing an empty object to listen for all events. Then we call the filter.watch function that receives a callback function as parameter to handle the events. This callback uses the error-first ordering of arguments, where the error is the first parameter and the result is the second. The first parameter will be undefined if there are no errors. For now, just log the event data. Let’s try it out.

Then let’s connect the createEventFilter function to our button with id listenAddress:

$("#listenAddress").click(function(){
    let sender =  $("#ownerAddress").val();
    createEventFilter(sender);
  });

Then we can test this. As expected, we get the first log: Transfer happened!!! Followed by the event data.

{logIndex: 0, transactionIndex: 0, transactionHash: "0x77cac553a764794dd69b540a82929b1181a72e43c063294903183f4385239145", blockHash: "0x8559092fd3bde1480b9ca08803c5ef0945e36d55a746965a564c57bee7e7f4c1", blockNumber: 15, …}
address:"0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f"
args:
from:"0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5"
to:"0x2191ef87e392377ec08e7c08eb105ef5448eced5"
tokens:BigNumber {s: 1, e: 0, c: Array(1)}
__proto__:Object
blockHash:"0x8559092fd3bde1480b9ca08803c5ef0945e36d55a746965a564c57bee7e7f4c1"
blockNumber:15
event:"Transfer"
logIndex:0
transactionHash:"0x77cac553a764794dd69b540a82929b1181a72e43c063294903183f4385239145"
transactionIndex:0
type:"mined"
}

We got a lot more data than we need. Things like blockNumber, blockHash, transactionHash can be useful in other contexts for determining the order of events. For now, let’s focus on the task at hand. The values we are interested in are located in the args key. Let’s go back and modify our listener function.

function createEventFilter(myAddr){
  filter = contractInstance.Transfer({});
  console.log("Listening for transfers to and from " + myAddr);
  filter.watch(function(error, result){
    if(!error){
      let eventData = result.args;
      if(eventData.from == myAddr || eventData.to == myAddr){
        console.log("Transfer detected, update prices");
        let balance = checkBalance(myAddr);
        $('#showBalance').html("Your balance is " + balance);
      }
    }else{
      console.log("Got event error");
      console.log(error);
    }
  });
}

Here we get the event data from result.args and then check to see if our address matches the sender or recipient addresses. If true, then check the balance again and update the HTML of the showBalance element with the new balance. The error handling part can stay the same.

For testing, get a new address that doesn’t have tokens transferred to it yet. Just as a reminder, you can get all accounts with web3.eth.accounts in the Truffle console. Open the developer console in your browser; now set the new address in the Your Address input box and click Listen for Transfers. You should see:

Listening for transfers to and from 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e

printed out in your browser console.

Now send tokens to and from this address; in both cases, the balance will automatically update. Also make a transfer that doesn’t include our address;you should see no change in balance and no logs in the console.

List past transactions

Currently we are only using transfer events as signals to refresh the balance, but we aren’t making any use of their data. Let’s create a balance sheet containing all outgoing and incoming transactions for our account. The event listening logic we have so far does not include past transfers involving our account; it only detects events that happen after the listener has been executed. We need a function that can collect all transfers for an account, from the deployment of the smart contract, even before the registration of our listener. First let’s sort the HTML part. Add this HTML as the last element in the container div.

<div class="row">
    <div class="col-md-6">
      <div class="panel panel-default">
          <div class="panel-heading">Balance sheet</div>
          <div class="panel-body">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h5>Incoming transactions</h5>
              </div>
              <div class="panel-body">
                <table class="table table-striped" id="inTbl">
                <thead>
                  <tr>
                    <th>Source</th>
                    <th>Amount</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>
                      <td>Set your address</td>
                  </tr>
                </tbody>
                </table>
              </div>
            </div>
            <div class="panel panel-default">
              <div class="panel-heading">
                Outgoing transfers
              </div>
              <div class="panel-body">
                <table class="table table-striped" id="outTbl">
                <thead>
                  <tr>
                    <th>Target</th>
                    <th>Amount</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>
                      <td>Set your address</td>
                  </tr>
                </tbody>
                </table>
              </div>
              </div>
          </div>
          <div class="panel-footer">
            <input type="button" class="btn-success btn" id="listenAddress" value="Listen for Transfers">
          </div>
        </div>
    </div>
  </div>

This should look like:

image alt text

The balance sheet is split into two tables: Incoming and Outgoing transfers.

Also we moved the Listen for Transfers button down here. Let’s head over to main.js.

We need to write a function that will get all transfers for our account, even the ones before our watcher or web app were created.

var inTs = {};
var outTs = {};

function getPastTransfers(myAddr){
  inTs = {};
  outTs = {};
  let loadedAll = _.after(2, renderTables)
  contractInstance.Transfer({from: myAddr}, { fromBlock: 0, toBlock: 'latest' }).get(function(error, events){
    if (error)
      console.log('Error in getting outgoing transfers: ' + error);
    else{
      outTs = [];
      events.forEach(function(event){
        outTs[event.transactionHash] = event.args;
      })
      loadedAll();
    }
  });

First set two global variables to hold the transactions, then write the getPastTransfers function.

contractInstance.Transfer({from: myAddr}, { fromBlock: 0, toBlock: 'latest' }).get(function(error, events){

Like before, we get the transfer handle from our deployed contract, filter by our address as the from (sender) parameter and also pass in a second argument that will get the events from the first block to last. On top of this call, we chain the get function that fetches all events from the blockchain, filtered by our conditions.

Finally, in the callback of this get function we have an array of all events matching our filter.

Now we save these events into our global objects inTs and outTs using their unique transactionHash as the key. This is necessary because once we register our watcher, it will regenerate some of the events that have happened, so there could be duplicates in our table. To avoid this, we use the transactionHash as a unique key for storing our event data.

outTs[event.transactionHash] = event.args;

We need to repeat the same thing for our incoming transactions. Here is the complete getPastTransfers function:

function getPastTransfers(myAddr){
  inTs = {};
  outTs = {};
  let loadedAll = _.after(2, renderTables)
  contractInstance.Transfer({from: myAddr}, { fromBlock: 0, toBlock: 'latest' }).get(function(error, events){
    if (error)
      console.log('Error in getting outgoing transfers: ' + error);
    else{
      outTs = [];
      events.forEach(function(event){
        outTs[event.transactionHash] = event.args;
      })
      loadedAll();
    }
  });
  contractInstance.Transfer({to: myAddr}, { fromBlock: 0, toBlock: 'latest' }).get(function(error, events){
    if (error)
      console.log('Error in getting incoming transfers: ' + error);
    else{
      inTs = [];
      events.forEach(function(event){
        inTs[event.transactionHash] = event.args;
      })
      loadedAll();
    }
  });
}

Both of these operations — fetching outgoing and incoming transfer events — are asynchronous; we need to wait for both of them before rendering our tables. That is why we make use of lodash and its _.after function. This line creates a function called loadedAll that, when run twice, executes the renderTables function.

let loadedAll = _.after(2, renderTables)

The renderTables function parses our stored event data and populates the tables.

function renderTables(){
  let inTbl = $('#inTbl').find('tbody');
  inTbl.html('');
  Object.values(inTs).forEach(function(event){
    let row = createRow(event, 'from');
    inTbl.append(row);
  });
  let outTbl = $('#outTbl').find('tbody');
  outTbl.html('');
  Object.values(outTs).forEach(function(event){
    let row = createRow(event, 'to');
    outTbl.append(row);
  });
}

function createRow(event, field){
    return [
      '<tr>',
        '<td>'+ event[field] +'</td>',
        '<td>'+ event['tokens'] +'</td>',
      '</tr>'
    ].join('');
}

createRow is a helper function that generates a new HTML row using event data. It creates a list of elements and then merges them, which is simply for better code readability.

Also, we need to update the createEventFilter function to append rows to our tables (when new relevant events are detected) and keep updating the balance.

function createEventFilter(myAddr){
  filter = contractInstance.Transfer({});
  console.log("Listening for transfers to and from " + myAddr);
  filter.watch(function(error, result){
    if(!error){
      let eventData = result.args;
      if(eventData.from == myAddr){
        if(!outTs[result.transactionHash]){
          updateBalance(myAddr);
          let outTbl = $('#outTbl').find("tbody");
          outTbl.append(createRow(eventData, 'to'));
          outTs[result.transactionHash] = eventData;
        }
      }
      if(eventData.to == myAddr){
        if(!inTs[result.transactionHash]){
          updateBalance(myAddr);
          let inTbl = $('#inTbl').find("tbody");
          inTbl.append(createRow(eventData, 'from'));
          inTs[result.transactionHash] = eventData;
        }
      }
    }else{
      console.log("Got event error");
      console.log(error);
    }
  });
}

We are using a function we don’t have called updateBalance. We’ll extract the logic for updating the balance of the main address into this function.

function updateBalance(address){
        let balance = checkBalance(address);
        $('#showBalance').html("Your balance is " + balance);
    }

Then this function will also be used in the onClick handler for the Check Balance button:
$('#checkBalanceBtn').click(function(){
        owner = $('#ownerAddress').val();
        updateBalance(owner);
  });

To tie everything up, we need to update the onClick event for our Listen for Transfers button that had an ID “listenAddress”. Here is that event listener in the $(document).ready section.

$("#listenAddress").click(function(){
    let sender =  $("#ownerAddress").val();
    getPastTransfers(sender);
    createEventFilter(sender);
  });

And don’t forget to add the lodash import in your index.html file. Here is the full JS import section at the end of index.html.

</body>
<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js"></script>
<script src="./main.js"></script>
</html>

Summary

Today we got plenty of coding practice as we covered lots of ground. We built a frontend web app that can check balances, make transfers, listen for events, and show all account transactions — using only the blockchain as a backend service. This helps us get a better picture of how blockchain technology fits into the ecosystem of the modern web.

Resources: Web3.js library Smart Contract ABI

Scout App for monitoring

Darko Kolev

Student of life, people, software development and business. Currently Lead Software Engineer at BitcoinAverage

  1. Comments for Building a Wallet Web App

You must login to comment

You May Also Like