Front end Development - cogeorg/teaching GitHub Wiki

Initializing Web3

We want to connect our frontend to the blockchain to receive data directly from the contracts and to interact with them. To do that, we will use Web3.js. Web3.js is a JavaScript library that servers as a connector between your frontend application and your Ethereum node. To establish that link it is required to bind Web3 to a provider. If you open the frontend application with the MetaMask extension enabled, an instance of Web3 will be injected into the window object of your webpage. In all other situations you will have to define the address of the HTTP provider hosting your node and show an error message to the user.

Let's adapt app.js to create an instance of Web3 or to reuse an existing one. Open it and insert the following code in the initWeb3: function() { ... } block

// initialize web3
if (window.web3) {
      App.web3Provider = window.web3.currentProvider
}
// If no injected web3 instance is detected, fall back to Ganache
else {
    App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545')
}
web3 = new Web3(App.web3Provider)
return App.initContract()

Connecting to your Contract

First of all, we need a running backend, meaning that we have to start Ganache and deploy our contracts. A quick reminder:

$ truffle migrate --network ganache --reset --compile-all

This command will not only deploy the contracts to the specified node but will also save a json file containing the compiled version of the source code for each of the contracts into the build/contracts/ folder.

The contract we actually want to connect to is the one called ZombieOwnership, because it inherits from all the other contracts. Therefore, modify the initContract: function() { ... } block in you app.js file as follows:

$.getJSON('ZombieOwnership.json', function(zombieOwnershipArtifact){
    // get the contract artifact file and use it to instantiate a truffle contract abstraction
    App.contracts.ZombieOwnership = TruffleContract(zombieOwnershipArtifact);
    // set the provider for our contract
    App.contracts.ZombieOwnership.setProvider(App.web3Provider);
    // retrieve zombies from the contract

    //update account info
    App.displayAccountInfo();

    // show zombies owned by current user
    return App.reloadZombies();
});

The functions displayAccountInfo() and reloadZombies() do not yet exist and we will implement them soon. For now, just add an empty function

displayAccountInfo: function () {
            
},

reloadZombies: function () {
            
},

Display Account Information

It is time to prepare the index.html file to display the user information, her address and her current Ether balance. Therefore, insert the following block underneath the jumbotron-div and inside the container-div

<div class="col-md-12" id="zombie-list">
    <div class="row">
        <div class="col-lg-12">
            <p id="account" class="welcome pull-right"></p>
            <p id="accountBalance" class="welcome pull-left"></p>
        </div>
    </div>
</div>

This is a great time to change the jumbotron as well if you want to.

Let's fill this with content. Thus, we need to implement the displayAccountInfo() function already predefined in app.js . But first, we need another object variable for App, namely account. On the top, specify

account: 0x0,

Now, please insert the following snippet inside the displayAccountInfo: function () { ... } block:

// get current account information
web3.eth.getCoinbase(function (err, account) {
    // if there is no error
    if (err === null) {
        //set the App object's account variable
        App.account = account;
        // insert the account address in the p-tag with id='account'
        $("#account").text(account);
        // retrieve the balance corresponding to that account
        web3.eth.getBalance(account, function (err, balance) {
            // if there is no error
            if (err === null) {
                // insert the balance in the p-tag with id='accountBalance'
                $("#accountBalance").text(web3.fromWei(balance, "ether") + " ETH");
            }
        });
    }
});

You should be able to see the account information on the webpage now. If you have not done so already, run

$ npm run dev

Generate a new Zombie

A new player wants to create her first zombie. Thus, we need a button for her to click on. Here comes the HTML. Modify the div zombie-list as follows

<div class="col-md-12" id="zombie-list">
    <div class="row">
    <div class="col-lg-12">
        <p id="message" class="welcome pull-left"></p>
        <p id="account" class="welcome pull-right"></p>
        <p id="accountBalance" class="welcome pull-left"></p>
    </div>
    </div>

    <div class="row">
    <button class="btn btn-info btn-lg btn-create" data-toggle="modal" data-target="#createZombie">Generate a zombie</button>
    </div>
</div>

This button will trigger a bootstrap modal where the player is asked to enter the zombie's name. Please define the following modal at the top of the body-tag, before the container-div.

 <!-- Modal form to create a zombie -->
<div class="modal fade" id="createZombie" role="dialog">
    <div class="modal-dialog">

        <!-- Modal content-->
        <div class="modal-content">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal">&times;</button>
            <h4 class="modal-title">Create a zombie</h4>
        </div>
        <div class="modal-body">

            <div class="row">
            <div class="col-lg-12">
                <form>
                <div class="form-group">
                    <label for="zombie_name">Zombie name</label>
                    <input type="text" class="form-control" id="zombie_name" placeholder="Enter the name of your zombie">
                </div>
                </form>
            </div>
            </div>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-primary btn-success" data-dismiss="modal" onclick="App.createRandomZombie(); return false;">Submit</button>
            <button type="button" class="btn" data-dismiss="modal">Close</button>
        </div>
        </div>

    </div>
</div>

Once this is all in place, we can define the functionality in app.js. Include the following function

createRandomZombie: function () {
    // get information from the modal
    var _zombieName = $('#zombie_name').val();

    // if the name was not provided
    if (_zombieName.trim() == '') {
            // we cannot create a zombie
            return false;
    }

    // get the instance of the ZombieOwnership contract
    App.contracts.ZombieOwnership.deployed().then(function (instance) {
            // call the createRandomZombie function, 
            // passing the zombie name and the transaction parameters
            instance.createRandomZombie(_zombieName, {
                from: App.account,
                gas: 500000
            });
    // log the error if there is one
    }).then(function () {

    }).catch(function (error) {
            console.log(error);
    });
},

You should be able to generate a first zombie now. MetaMask will ask you to confirm the transaction. However, we have no possibility yet to show the zombies that belong to one wallet.

Display Zombies belonging to one Wallet

Let's start with the HTML again. Include a placeholder for the list of zombies

<div class="col-md-12" id="zombie-list">
    <div class="row">
    <div class="col-lg-12">
        <p id="message" class="welcome pull-left"></p>
        <p id="account" class="welcome pull-right"></p>
        <p id="accountBalance" class="welcome pull-left"></p>
    </div>
    </div>

    <div class="row">
    <button class="btn btn-info btn-lg btn-create" data-toggle="modal" data-target="#createZombie">Generate a zombie</button>
    </div>

    <div id="zombieRow" class="row">
    <!-- ZOMBIES LOAD HERE -->
    </div>
</div>

We also define a template for all zombies that we will fill using JavaScript. Include this underneath the container-div

<div id="zombieTemplate" style="display: none;">
    <div class="row-lg-12">
        <div class="panel panel-default panel-article">
        <div class="panel-heading">
            <h3 class="panel-title"></h3>
        </div>
        <div class="panel-body">
            <strong>DNA</strong>:
            <span class="zombie-dna"></span>
            <br/>
            <strong>Level</strong>:
            <span class="zombie-level"></span>
            <br/>
            <strong>Ready to attack</strong>:
            <span class="zombie-readyTime"></span>
            <br/>
            <strong>Battles won</strong>:
            <span class="zombie-winCount"></span>
            <br/>
            <strong>Battles lost</strong>:
            <span class="zombie-lossCount"></span>
            <br/>
        </div>
        <div class="panel-footer">
            <button type="button" class="btn btn-primary btn-success btn-levelup" onclick="App.levelUp(this); return false;">Level Up</button>
        </div>
        </div>
    </div>
</div>

It is time to implement reloadZombies() now. But first, we need a loading variable that checks whether we are retrieving the zombies already. Since it always takes some time to interact with smart contracts, this helps prevent timeouts.

App = {
      web3Provider: null,
      contracts: {},
      account: 0x0,
      loading: false,

      // ...

The function then looks like this

reloadZombies: function () {
    // avoid reentry
    if (App.loading) {
            return;
    }
    App.loading = true;

    // refresh account information because the balance may have changed
    App.displayAccountInfo();

    // define placeholder for contract instance
    // this is done because instance is needed multiple times
    var zombieOwnershipInstance;

    App.contracts.ZombieOwnership.deployed().then(function (instance) {
            zombieOwnershipInstance = instance;
            // retrieve the zombies belonging to the current user
            return zombieOwnershipInstance.getZombiesByOwner(App.account);
    }).then(function (zombieIds) {
            // Retrieve and clear the zombie placeholder
            var zombieRow = $('#zombieRow');
            zombieRow.empty();

            // fill template for each zombie
            for (var i = 0; i < zombieIds.length; i++) {
                var zombieId = zombieIds[i];
                zombieOwnershipInstance.zombies(zombieId.toNumber()).then(function (zombie) {
                        App.displayZombie(
                            zombieId.toNumber(),
                            zombie[0],
                            zombie[1],
                            zombie[2],
                            zombie[3],
                            zombie[4],
                            zombie[5]
                        );
                });
            }
            // hide and show the generate button
            App.displayGenerateButton(zombieIds);
            // app is done loading
            App.loading = false;
    // catch any errors that may occur
    }).catch(function (err) {
            console.log(err.message);
            App.loading = false;
    });
},

There are two functions in the reloadZombies() function that we have not defined yet, displayZombie() and displayGenerateButton(). displayZombie() fills the HTML template with the zombie information and appends it to the correct section.

displayZombie: function (id, name, dna, level, readyTime, winCount, lossCount) {
    // Retrieve the zombie placeholder
    var zombieRow = $('#zombieRow');

    // define the price for leveling up
    // should not be hard-coded in the final version
    var etherPrice = web3.toWei(0.001, "ether");

    // Retrieve and fill the zombie template
    var zombieTemplate = $('#zombieTemplate');
    zombieTemplate.find('.panel-title').text(name);
    zombieTemplate.find('.zombie-dna').text(dna);
    zombieTemplate.find('.zombie-level').text(level);
    zombieTemplate.find('.zombie-readyTime').text(App.convertTime(readyTime));
    zombieTemplate.find('.zombie-winCount').text(winCount);
    zombieTemplate.find('.zombie-lossCount').text(lossCount);
    zombieTemplate.find('.btn-levelup').attr('data-id', id);
    zombieTemplate.find('.btn-levelup').attr('data-value', etherPrice);

    // add this new zombie to the placeholder
    zombieRow.append(zombieTemplate.html());
},

displayGenerateButton() checks whether there is already a zombie in the current wallet and if so, it hides the "Generate a zombie" button.

displayGenerateButton: function (zombieIds) {
    if (zombieIds.length > 0) {
            $(".btn-create").hide();
    } else {
            $(".btn-create").show();
    }
},

You may have noticed that there is another function called in displayZombie(). convertTime() converts the timestamp stored in the zombie struct into a human readable time

convertTime: function (timestamp) {
    var d = new Date();
    var date = new Date(timestamp * 1000 + d.getTimezoneOffset() * 60000);
    return date;
},

Now you should be able to see the list of your zombies once you refresh the webpage, and the button should be gone. Each zombie has a "Level Up" button but it is not functioning yet. Let's fix it.

Level Up

The HTML button calls a function levelUp(data) that does not exist yet. Here is the implementation:

levelUp: function (data) {
    // retrieve the zombie data
    var _zombieId = data.getAttribute("data-id");
    var _price = parseFloat(data.getAttribute("data-value"));

    // call the levelUp function in the zombie contract
    App.contracts.ZombieOwnership.deployed().then(function (instance) {
        return instance.levelUp(_zombieId, {
            from: App.account,
            value: _price,
            gas: 500000
        });
    // catch any occuring errors
    }).then(function () {

    }).catch(function (err) {
            console.error(err);
    });
},

Go ahead and try it out. After a few seconds and a page refresh, you should be able to see the increased zombie level.

Refreshing and listening to Events

It is very inconvenient to have to refresh the page every time we interact with the smart contracts or change the account we are using. To remedy the latter, one can make use of an interval that checks the current account every second and if it has changed, reloads the page.

To implement this, include the following snippet in the unnamed function in the bottom of app.js:

// placeholder for current account 
var _account;
// set the interval
setInterval(function () {
    // check for new account information and display it
    App.displayAccountInfo();
    // check if current account is still the same, if not
    if (_account != App.account) {
            // load the new zombie list
            App.reloadZombies();
            // update the current account
            _account = App.account;
    }
}, 100);

The view should change every time you switch the account using MetaMask now.

We also implemented an event in solidity that fires every time a new zombie is created. JavaScript can listen to these events and act accordingly. We want the page to refresh once a new zombie has been created such that the zombie information are visible. To do that, add the following function to app.js

listenToEvents: function () {
    App.contracts.ZombieOwnership.deployed().then(function (instance) {
        // watch the event
        instance.NewZombie({}, {}).watch(function (error, event) {
            // log error if one occurs
            if (error) {
                    console.error(error);
            }
            // reload the zombie list if event is triggered
            App.reloadZombies();
        });
    });
}

The two empty objects {} inside the event call are the default values for the filter and the range. If we wanted to, we could only listen to certain events, e.g. those that include a certain value (filter) or those starting from a specific block (range). In our case, it would make sense to only listen to events that concern the current account. However, this value is not an argument in the NewZombie event in solidity so we would have to update it. Feel free to do that on your own.

This function now needs to be called inside the initContract() function so add the following just above the return statement

// Listen to smart contract events
App.listenToEvents();

If you switch to an account that does not have a first zombie yet and create one, wait for a few seconds until the transaction is processed. The new zombie will automatically appear.

We should also create an event for the leveling up. Again, feel free to do that on your own.

⚠️ **GitHub.com Fallback** ⚠️