In this lab we're building a frontend for our smart contract. We will use a framework called Truffle together with React to create a simple website from which we can call functions in our contract.
Truffle is a development environment and testing framework that helps us developing the frontend for the CryptoFlight. It's often described as a Swiss army knife for Ethereum dapp development. We are going to use Truffle together with its React bindings to build and deploy a simple frontend.
React is a declarative, component-based JavaScript library that we will use to build the frontend. It was developed by Facebook and open sourced in 2013 and quickly gained popularity within the community. We are also making use of a CSS framework called Semantic UI, and more specifically its React-implementation of the components. If you feel more comfortable using Angular or Vue for frontend development, feel free to use any of them instead, the concepts will be very similar to those explained here.
Install dependencies and set up the environment
To install Truffle you first need Node and its package manager NPM. Depending on what OS you're running the installation process will look a little bit different. If you're on Windows I recommend spinning up a virtual machine with Ubuntu or use the official Docker-image that you can find over at Docker Hub.
Next we need to install some global dependencies for React, start by running
npminstall-gtruffle
Depending on your system setup you might need to run the command as sudo. Next, navigate to the folder where you want to create your project and create a folder for it. Then we'll use truffle unbox to create a new template project with React.
If you look into the folder you'll find 4 folders, a licence file and a config file. The folder client contains a create-react-app setup of React that is ready to go. In contracts we store our solidity-contracts. There's already two contract in the folder, one for performing the migrations and an example contract that stores a number. In migrations you find two helper modules that we will use to deploy the contracts. The test-folder contains some sample code for testing a contract.
Before we go any further we just gonna make sure that our local development server is working fine and that we can view the frontend. Navigate to the client-folder and run npm start (as in the screenshot above). Then go to localhost:3000 in your favorite browser (Chrome) and you should see something similar to this:
The development server brings us some neat things, for example hot reloading. This means that the website will reload as soon as you hit save in your code editor. Try for example to change the animation time from 20 seconds to 2 seconds and hit save. You should then see an almost instant reload and the logo spinning 10 times faster:
Configuration
Before we start to build the actual application we'll need to do a couple of additions to the config file to be able to deploy our test contract. Copy and paste the following snippet to truffle-config.js:
truffle-config.js
constpath=require('path');constHDWalletProvider=require('truffle-hdwallet-provider');constPROVIDER='https://ropsten.infura.io/v3/abcdef123456';constMNEMONIC='one two three four five six seven eight nine ten eleven twelve';module.exports= { networks: { dev: { host:'127.0.0.1', port:8545, network_id:'*' }, ropsten: {provider:function() {returnnewHDWalletProvider(MNEMONIC,PROVIDER); }, network_id:3, gas:400000 } }, contracts_build_directory:path.join(__dirname,'client/src/contracts'), solc: { optimizer: { enabled:true, runs:200 } }};
As you see we're using two additional dependencies, path and HDWalletProvider. The first one, path, is part of your node-installation but the second one we need to install manually. When we include dependencies in a Node-project the interpreter will start looking for the dependency in the current folder and then work its way up through the file-system. So to include HDWalletProvider we can either initialize a project in the project root folder, or (recommended) install it globally. Do this by running:
npminstall-gtruffle-hdwallet-provider
And remember to add this to your Dockerfile if you plan to deploy your project using Docker.
In the third line we're defining our Mnemonic that we exported from MetaMask in lab 1. If you forgot them you can easily get them back from MetaMask by going to settings and click "Reveal Seed Words":
Of course you shouldn't keep your seed words in the codebase. I suggest storing them in an environment variable both locally and on the live server. If you're using Docker you can define these in your docker-compose.yml, just remember to never ever push this to any version control system.
We also need to provide an API key for the Infura Ethereum API. You can obtain a free API key by going to https://infura.io/ and sign up for a new account.
Next up, we're exporting an object with the networks, a path to the build directory and some configuration for the Solidity compiler. The reason we're setting up two networks is that we both want to deploy to the Ropsten testnet as well as our private testnet. Deploying to the private testnet is almost instant and you'll most likely want to deploy your contract several times we'll developing and troubleshooting before deploying it to the actual Ropsten network. This is even more important if you plan to deploy your app to the mainnet later.
And one last tip for MetaMask, when you deploy and test your contract multiple times it's highly likely that MetaMask will tell you that "the tx doesn't have the correct nonce". If so, simply click the "Reset Account" button in the MetaMask settings view and things should run smoothly again.
Add your smart contract and compile it using Truffle
In the previous lab we developed the smart contract in Solidity, now we will import it to our Truffle project. Create a new file in the contracts-folder and name it CryptoFlight.sol. Then copy all the code over from Remix to this file and hit save.
To make sure everything is working as expected, we'll compile the new contract and deploy it to the private testnet. Import the contract to the top of 2_deploy_contracts.js and then call then it as an argument to the deploy-function. Your file should now look like this:
2_deploy_contracts.js
var CryptoFlight =artifacts.require('./CryptoFlight.sol');module.exports=function(deployer) {deployer.deploy(CryptoFlight);};
This should be enough to be able to compile and deploy CryptoFlight to the testnet. Make sure everything works by running:
trufflecompiletrufflemigrate--networkdev
If everything works as expected you should see an output similar to:
That's it for deploying the contracts to the testnet. If everything works fine you can also take the time to deploy it to the Ropsten testnet. All you need to do is to change the --network flag to be ropsten instead of dev:
truffle migrate --network ropsten
After a short time your app should be live and accessible on the Ropsten testnet, well done!
Building the frontend
In this section we will build a very simple frontend for communicating the the smart contract. To do this we'll make use React together with a CSS framework called Semantic UI. Navigate to the client folder and use the Node Package Manager to install Semantic UI:
npminstall--savesemantic-ui-reactsemantic-ui-css
Import the Semantic UI CSS package in index.js to make use of its CSS-rules:
index.js
import'semantic-ui-css/semantic.min.css';
You should be able to start the development server by running npm start inside of the client directory. After a couple of seconds the server should be up and running and you can navigate to localhost:3000 in your browser (the same as the one you used to install MetaMask) to see what it looks like. You should see a boilerplate page that is provided by Truffle when we create the project.
The frontend is a very simple one-page application that gets rendered client-side. For simplicity we will just keep everything in one file for now. Instead of using the existing code from Truffle we will start by deleting all of it and rewrite the app from the ground up. I will explain line by line what's happening. So, start by selecting everything inside App.js and hit delete. Then follow through as we build the application.
We will have four components in our applications, App, Flights, CryptoFlightToken and AirlineFlights. App will be the top-level component and the other three will be child components to this one.
The first 6 lines are all about importing some libraries as well as the css and contract-declarations into the app. Note that the imports from npm packages are just using the name of the packet while the external files we import are using relative file paths. We will keep adding imports here later as we build out the application.
Next we will declare the component itself and set an initial state-object:
App.js
classAppextendsReact.Component { state = { web3:null, account:null };
Define the class App which extends React.Component, this creates a class-based component. We use a class-based component since we want access to a state-object as well as the lifecycle methods of the component (or you can use hooks if you feel more comfortable with them). Next we are defining our initial state as a simple JavaScript object. This is the state our component will have when it first mounts in the DOM.
The componentDidMount() lifecycle method gets called right after the render method finishes. We mark the function async to tell the JavaScript engine that it will contain asynchronous code. Any method marked with the await-keyword will be considered as blockingand stop further execution until the result is returned.
We are using a library called web3 that is a widely used Ethereum API for JavaScript. You might remember it from the first lab where we used it to convert a number from ether to wei. Most of the functions here are asynchronous, meaning that when we call them they will return a Promise which can either resolve to a value or reject with an error object.
The render method is what actually renders stuff to the DOM. Create a new method called render and type in the following:
Most of this is just pure markup using some components from the semantic ui library. If you want a header image (the one defined on line 12) just add this to the public-folder. Note that the <>...</> is used when we want to return several components in the same root level of the DOM from a function in React (more about that here).
So where's the actual action going on for adding a flight and making bids? If you look at line 61 you see that we added a Flight-component which we haven't yet created so it's time to get going with that, stay tuned for the next section!
Sidenote on the config-file
Before we create the Flight-component we'll first make a config file containing the address to our CryptoFlightFactory once it's deployed to the blockchain. It's good practice to keep this separate from the actual source code so we'll make a file called config.js and place it in the client-folder. We'll come back to this file later when we do the deployment but for now it's enough to just export one constant from it. Add the following exports-statement to config.js:
config.js
module.exports= { cryptoFlightFactoryAddress:''};
As you might guessed, this is where we'll place the address to the contract in the blockchain once we deploy the app.
Adding a Flight-component
Create a new folder inside client/src and name it components (mkdir client/src/components), then create a new file named Flights.js inside that folder. This is where we will create flight offers and bid for tickets. We'll also create a separate component for displaying offers deployed by an airline where they can finalize or remove offers. Create another file in the same folder names AirlineFlights.js where we'll put the logic related to this.
Woaw that's a mouthful, you might say. But actually it should be pretty straightforward. First we import all required dependencies (including AirlineFlight which we're gonna come back to soon). We then create new class, Flights, which inherits from React.Component. In this class we set the initial state of the whole component.
The componentDidMount() lifecycle method will be called right before the component first renders to the screen. This is the perfect place to connect to the factory contract so that we can get the lists of flights and show it to the user. On line 40 you see that we try to access an environment-variable called NODE_ENV. This makes it easy for us to determine if the app runs on the server or in our local development environment. If we run locally we are using the address from cryptoFlightFactoryNetwork to the CryptoFlightFactory contract, otherwise we'll pull this one off the config-file that we created in the previous section.
We then want to fetch all the flights from the CryptoFlightFactory. To do this we are defining a helper-function called fetchFlights. The reason we break out this functionality is because we will need to refetch the flights whenever the user makes a change to the contracts, for example adding or removing a contract. To do this we rely on having cryptoFlightFactory-object attached to the state-object. The setState-function takes in a callback-function as the second argument, which gets invoked when the setState finishes its execution. We'll take advantage of this and pass in fetchFlight as the callback function.
In fetchFlights we are making use of the async/await pattern so we mark this function to be async. The first line in the function gets the addresses to all deployed contracts from the CryptoFlight factory contract. This is simply an array of addresses which belongs to different CryptoFlight contracts. We want to display not only the address to the user but also the departure, destination and minimum bid so we need to get these one by one for each contract. It's not possible to return an array of structs in the current version of Solidity so we need to use this workaround for now. As always, every call to the blockchain using web3 returns a Promise which will either resolve or reject when we get the reply. We are making use of a little trick here where we map over the array of addresses and transform it from a simple string to instead be a Promise which, when it resolves will give us the creator, minimum bid, departure, destination and tell us whether or not the flight offer is finalized. Once it resolves it will return an object with all of these attributes inside it. We then use the Promise.all() function which takes in an array of Promises and returns a single Promise that resolves first when all of the Promises passed in resolves (or rejects). We make a blocking call to this function (using the await keyword) and once it resolves we update the state once again.
If that didn't feel crystal clear the first time your read it, just try to go through it slowly line by line and try to get familiar with the idea. Note that doing it this way we send all the requests in parallel. This saves us some time but we have no idea of which order the Promises are resolving to. If you need the return value from one Promise before you call the next one you can instead use reduce to chain them together. But for this purpose it's both easier and more efficient to run them all in parallel.
Next up we have functions for adding and removing a flight, adding travellers to flights as well as finalizing flights. These all follow the same idea so I won't go through them in detail. The important thing to note is that we always update the loading flag for the state we are altering before sending a transaction and then reset this flag as soon as we get a response. This way it's very easy for us to give the user feedback about what's happening. Since the operations on the blockchain takes around 20-30 seconds to complete, it's important to keep the user informed about what's happening.
We then declare a couple of render helper-functions. It's best practice to put all of these just above the render-method. renderFlightOffers() will return a list of cards containing every active offer. renderAirlineFlights() returns the flights for a particular airline. These are only visible for the user that created the offers. They can choose to finalize or remove an offer here. Since we need local state in each one of these we are making use of a separate class-based component, namely AirlineFlight so now is a good time to actually implement this one. Open up AirlineFlight.js and add the following code:
This component should be fairly straight forward. Each card just shows the departure and destination of the flight and give the creator the option to either finalize it, passing in the number of seats available, or to remove it. To keep things simple and clear we pass in these as props and use the functions declared in Flights.js to perform the actual operations.
Now back to Flights.js again, the next thing we need to do is to render the finalized flights so that a user can see whether or not they got a seat on that flight. When the user opens up a card we send a request to the blockchain to obtain the list of travellers that got a seat.
That was all of the helper-functions for rendering and we can now start to write the main render function for Flights. This is now pretty short since all we need to do is some layout using the components from Semantic UI and call the helper-functions. Lastly we export the component as default so that we can make use of it inside App.js.
There are some small details left out, for example the error handling which we are doing with a function passed as a props from the parent component. If you had some basic web programming course before, all of this should be familiar so I mostly focused on the parts involving the blockchain.
Time to go live!
Well done, we now have a working smart contract and a frontend to communicate with it. Our next task is to deploy the smart contract to the Ropsten network and our frontend to a real live server. Worth to note is that our frontend is just to give the users of the contract an easier way to interact with it. There is nothing stopping people from interacting directly with the contract. This is one of the main benefit of building a completely decentralized application, there is no single point of failure.
Deploying the smart contract is as easy as running one command using Truffle:
trufflemigrate--networkropsten
That's it! If you have enough ether on your account the contract will now be live within a short period of time.
Deploying the React-application is just a little bit more complex. We will deploy the app manually here but if you build a real application I highly recommend setting up a continuous integration pipeline to make sure that everything is okay and that the tests pass before going live.
First you need a server to deploy to. The easiest way is to sign up for a VPS at some provider like AWS, Azure, DigitalOcean or Linode. All of these have free tiers for students so pick anyone. Pick Ubuntu 18.04 as your pre-installed OS and choose at least 1GB of RAM. Everything else can be set to default.
After you set up the server and SSH'd into it we need to install a webserver (NGinx), set up the firewall (UFW) and upload our frontend. Start by updating the package manager apt and install nginx:
sudoaptupdatesudoaptinstallnginx
And make sure that NGinx is running:
systemctlstatusnginx
Then configure UncomplicatedFirewall (ufw) to allow to port 80 and 443:
sudoufwallow'Nginx HTTP'
In the default configuration of NGinx it expects the files to be located in /var/www/html, if you have time I highly recommmend setting up your own configuration of NGinx and installing an SSL-cert using LetsEncrypt. There are some great explanation on how to set up LetsEncrypt on the official website and the scope of it lies outside the scope of this tutorial so I won't go through it here. Just remember to set up a CRON-job for renewing the certificate every three months or so.
Now we are ready to build the frontend and upload the frontend to the server. The easiest way to do this is to use the scp command and since you probably want to deploy several times throughout development we're going to write a short shell-script for that.
Create a new file in you client directory and name it deploy.sh then add the following lines to it:
Change the SERVER-variable to the actual ip of your server. Now you also need to make sure that the file is executable. You do this by running chmod u+x deploy.sh. Now you can easily deploy the frontend for CryptoFlight with just one command.
One last thing to note, if you want to pass in the address to the contract you need to do so in config.js and set the environment variable NODE_END to equal "production". For example, if you're running Ubuntu you do this in /etc/environment.
Wrap up
That's it for the frontend! We installed Node, Truffle and React and created a very simple frontend for interacting with the CryptoFlight contract. We also set up a simple web server on a Ubuntu-system and used NGinx to serve our static files. In the next lab we will keep developing both the blockchain-side of the application as well as the frontend to make use of our own token.