Making a User Interface for DApps

Milecia

As much of a buzz term as blockchain has become, it's still a great technology. To take some of the magic out of it, blockchain is essentially a distributed database. A blockchain is made of blocks that contain data and those blocks are chained together across multiple machines, which make up the distributed network.

In this article, we'll build a distributed app in the Redwood framework that handles video data. We'll make a smart contract to handle our blockchain interactions. Then we'll set up the Redwood distributed app that will work with the blockchain.

Background on DApps

A DApp is very similar to a regular app. The only difference is that the back-end runs on a decentralized network instead of a central server that might host APIs or other services. It uses the Ethereum blockchain to store data and it has smart contracts to handle the business logic.

By using smart contracts, DApps don't have an owner. A smart contract is like a set of rules that every part of the network has to follow. Once the smart contract has been deployed to Ethereum, you can't change it. That means no companies or individuals can change the instructions for that set of rules in any way.

Ethereum

For a little context, Ethereum is a decentralized, open-source blockchain that has smart contract functionality. This is used as the network to host smart contracts. Anybody can join an Ethereum network and they will automatically follow the rules in the smart contract.

Setting up a Redwood app

Now that we have a little background on what we'll be making, let's start by making the Redwood app. To do this, open a terminal and run:

1yarn create redwood-app user-dapp

This will generate new directories and files for the project that covers everything from the front-end to the back-end, including a database. Even when you're working with a DApp, there might be reasons you want to track data in a more common way, like easier data processing.

With the app created, you should see two main directories: api and web. The api folder holds all of the logic for the GraphQL back-end and the code to manage the database transactions. The web folder, which is where we'll be focused, holds all of the front-end logic and user interface.

Some Installations

You'll need to download Ganache to set up a local Ethereum network that you can develop on. Once you have it installed, you can use the "Quickstart" option.

You'll also need to install Truffle with the following command in the web directory.

1yarn add truffle

This is how we'll deploy the smart contract to the local Ethereum network.

There are a few dependencies that need to be installed too. In the web directory run:

1yarn add truffle truffle-contract web3

This gives us everything we need for the smart contract.

Initializing the Truffle project

Now run the following in the web directory:

1yarn truffle init

This will create an initial migration and the smart contract for it in the web directory. Now we'll jump straight into some code since we have everything set up.

Writing the smart contract

Inside the web > contracts directory, add a new file called VideoList.sol. This is where we'll write some Solidity code to define the rules for this contract. It looks very similar to JavaScript, but it's not the same language. Copy and paste the following code into this new file.

1pragma solidity ^0.5.0;
2
3contract VideoList {
4 uint public videoCount = 0;
5
6 struct Video {
7 uint id;
8 string url;
9 }
10
11 mapping(uint => Video) public videos;
12
13 constructor() public {
14 createVideo("https://res.cloudinary.com/milecia/video/upload/c_pad,h_360,w_480,q_70,du_10/elephant_herd.mp4");
15 }
16
17 function createVideo(string memory _content) public {
18 videoCount ++;
19 videos[videoCount] = Video(videoCount, _content);
20 }
21}

Let's walk through what happened here. We start by defining the version of Solidity we want to work with. Then we create the definition for the contract. In this example, the contract is called VideoList.

In the contract, we have a publically exposed videoCount variable that holds how many videos have been added to the blockchain.

Next, there's a struct that defines a Video record. It has an integer id value and a string url value. Then we map all of the videos into a public videos array.

Using the constructor, we add a default video to the blockchain when the smart contract is deployed. Lastly, we have a function that will let us add new videos to the blockchain.

This is the whole smart contract! All that's left to do is create a migration for it and deploy it to the local blockchain.

Migrations and deploys

Before we actually do the migration and deploy, there's a little config file we need to add. In the web directory, add a new file called truffle-config.js. Inside the file, paste the following code.

1module.exports = {
2 networks: {
3 development: {
4 host: "127.0.0.1",
5 port: 7545,
6 network_id: "*" // so it can match any network id
7 }
8 },
9 solc: {
10 optimizer: {
11 enabled: true,
12 runs: 200
13 }
14 }
15}

This is how we connect to the local Ethereum blockchain that's running through Ganache. These are the default values in Ganache and you can see them directly in the app.

Then we'll need to add a new migration file to the migrations folder. Create a new migration file called 2_deploy_contracts.js. We put the number at the beginning of the migration file so the network knows which order they should be run in.

Open this file and add the following code.

1const VideoList = artifacts.require("./VideoList.sol");
2
3module.exports = function(deployer) {
4 deployer.deploy(VideoList);
5};

This is how we deploy the VideoList smart contract we created.

With this in place, we can run the migration for the smart contract. In the web directory, run:

1truffle migrate

You should see that the migration has run successfully. Now let's take a look at the smart contract in the Truffle console. We'll need the address of the smart contract for a later part of this article, so let's grab it while we're here.

In the web directory, run:

1truffle console

This should bring you to an instance of the smart contract we deployed with the migration. Let's run so code in the console to get an instance of the contract with a function.

1videoList = await VideoList.deployed()

We have to interact with the blockchain in an async manner, that's why we're using the await keyword in the Truffle console. Now let's get the address for this contract because we'll need it in the DApp.

1videoList.address

This should return a string similiar to this:

0x82293fe0BE6cCbA6Eb9bd6d5824fC6ACeB6d3957

Since we have the smart contract built and deployed, we can turn our attention to the DApp which uses the data from the blockchain.

Working on the front-end

We'll take advantage of a Redwood command to get started on the front-end. We need a page that lets users interact with the DApp. Leave the web directory and go to the project root in the terminal and run the following command.

1yarn rw g page dapp /

This will generate the page component, a Storybook component for the page, and a test for the page. It'll also add the route for this new page to the Routes.tsx file. Since we added the / in the generate command above, it makes this new page the root page for the app.

Back in the web directory, take a look inside src > pages > DappPage. This has all the files we talked about. Our focus will be on DappPage.tsx. Open this file and delete everything out of it.

Connecting to the Ethereum blockchain with Web3.js

The first thing we need to do is make a new file called config.tsx in the web > src directory. This is where we'll add some config values that let the front-end connect to the blockchain network we have running with Ganache.

Remember that address we got for our smart contract earlier? It's ok if you don't. All you have to do is open a terminal in the web directory and run the following commands to get that address again.

1truffle console
2videoList = await VideoList.deployed()
3videoList.address

In the config.tsx file, add the following line with your own smart contract address.

1export const VIDEO_LIST_ADDRESS = '0x82293fe0BE6cCbA6Eb9bd6d5824fC6ACeB6d3957'

Next, you'll have to get an array of values for the ABI of the smart contract. This describes the smart contract behavior and functionality to the front-end. You'll find this in web > build > contracts > VideoList.json. In the json file, you'll see a key-value pair named "abi". Copy that whole array and add the following code to config.tsx like this.

1export const VIDEO_LIST_ABI: any = [
2 {
3 "constant": true,
4 "inputs": [],
5 "name": "videoCount",
6 "outputs": [
7 {
8 "name": "",
9 "type": "uint256"
10 }
11 ],
12 "payable": false,
13 "stateMutability": "view",
14 "type": "function",
15 "signature": "0xc61b5f4c"
16 },
17 {
18 "constant": true,
19 "inputs": [
20 {
21 "name": "",
22 "type": "uint256"
23 }
24 ],
25 "name": "videos",
26 "outputs": [
27 {
28 "name": "id",
29 "type": "uint256"
30 },
31 {
32 "name": "url",
33 "type": "string"
34 }
35 ],
36 "payable": false,
37 "stateMutability": "view",
38 "type": "function",
39 "signature": "0xe6821bf5"
40 },
41 {
42 "inputs": [],
43 "payable": false,
44 "stateMutability": "nonpayable",
45 "type": "constructor",
46 "signature": "constructor"
47 },
48 {
49 "constant": false,
50 "inputs": [
51 {
52 "name": "_content",
53 "type": "string"
54 }
55 ],
56 "name": "createVideo",
57 "outputs": [],
58 "payable": false,
59 "stateMutability": "nonpayable",
60 "type": "function",
61 "signature": "0x8e878969"
62 }
63]

With this config file in place, let's import a few things in DappPage.tsx.

1import { useState, useEffect } from 'react'
2import Web3 from 'web3'
3import { VIDEO_LIST_ABI, VIDEO_LIST_ADDRESS } from '../../config'

Now let's build the DappPage component. We'll start with just the outline of the component and a few states.

1export const DappPage = () => {
2 const [account, setAccount] = useState<string>('')
3 const [videoList, setVideoList] = useState<any>()
4 const [videos, setVideos] = useState([])
5}

Next, we'll call the function that will connect to the Ethereum network. We're using the useEffect hook here because we only want this to happen when the page initially loads. So right below the states we defined, add this code.

1useEffect(() => {
2 loadData()
3}, [])

Let's jump into writing the loadData method. This is where we'll actually connect to the network and read data from the blockchain.

1const loadData = async () => {
2 const web3 = new Web3('http://localhost:7545')
3
4 const accounts = await web3.eth?.getAccounts()
5
6 setAccount(accounts[0])
7
8 const videoList = new web3.eth.Contract(VIDEO_LIST_ABI, VIDEO_LIST_ADDRESS)
9 setVideoList(videoList)
10
11 const videoCount = await videoList.methods.videoCount().call()
12
13 for (var i = 1; i <= videoCount; i++) {
14 const video = await videoList.methods.videos(i).call()
15 setVideos([...videos, video])
16 }
17}

This is an async function because we want to wait for the blockchain to handle the request. We make a new instance of Web3 that connects to the local Ethereum blockchain we have running. Then we get the accounts associated with this blockchain and grab the first one. We'll need this a little later.

Next, we pass in the config values we wrote earlier to interact with the smart contract. We're getting the videoList contract and fetching the videoCount to see how many videos are available.

Then we loop through all of the videos in the blockchain and save them to the app's state.

Now we can return a view to the page that displays the smart contract id and the videos we've fetched from the blockchain. Right below the loadData method, add the following return statement.

1return (
2 <div>
3 Dapp account id: {account}
4 <ul id="videoList">
5 {videos.map((video, key) => {
6 return (
7 <div key={key}>
8 <label>
9 <video src={video.url}></video>
10 </label>
11 </div>
12 )
13 })}
14 </ul>
15 </div>
16)

If you go to the root of the project in a terminal and run yarn rw dev, you should see something similar to this in the browser.

You've officially connected an app to an Ethereum blockchain using a smart contract you wrote! All that's left is adding a way for users to add new videos to the blockchain.

Adding the videos to the blockchain

For the last bit of functionality, we'll use the Cloudinary upload widget to add videos directly to the blockchain.

We need to add the react-cloudinary-upload-widget package in a terminal in the web directory.

1yarn add react-cloudinary-upload-widget

Then we'll add another import line to DappPage.tsx below the others.

1import { WidgetLoader, Widget } from 'react-cloudinary-upload-widget'

Now we can write the function to add a video to the blockchain. After the loadData function, add this code.

1const createVideo = (content) => {
2 videoList.methods.createVideo(content).send({ from: account, gas: 4712388 })
3 .once('receipt', (receipt) => {
4 console.log(receipt)
5 })
6}

This is how we use the createVideo method we defined in the smart contract. One thing to note is that we need to send a certain amount of gas with this request. The amount of gas you have is tied to the account you're connected with. Each transaction in a smart contract has a gas price, but since we didn't change any default values 4712388 is what the contract expects as a gas payment.

Now we'll define the callback that will give us a response from Cloudinary when the video has successfully uploaded. Right below the createVideo function we just wrote, add this.

1const successCallBack = (results) => {
2 const videoInfo = results.info
3 const url = videoInfo.url
4
5 createVideo(url)
6}

This will be called in the Widget when we get a success response from Cloudinary. It'll take the URL from Cloudinary and add it to the blockchain. That means it's finally time to add that widget to the return statement. Below the line that displays the account id, add the following code.

1<WidgetLoader />
2<Widget
3 sources={['local', 'camera']}
4 cloudName={'test_name'}
5 uploadPreset={'fwe9ewffw'}
6 buttonText={'Open'}
7 style={{
8 color: 'white',
9 border: 'none',
10 width: '120px',
11 backgroundColor: 'green',
12 borderRadius: '4px',
13 height: '25px',
14 }}
15 folder={'test0'}
16 onSuccess={successCallBack}
17/>

This is how we connect to Cloudinary and get URLs for videos we upload. You'll need to log in to your Cloudinary account and get the cloudName and uploadPreset name from the dashboard. Now you should see a new "Open" button in the browser.

If you click that button, you'll see the upload widget.

Go ahead and upload a new video and then reload the page. You should see your new video similar to this.

You have a fully functioning DApp now! Feel free to add more functionality to the smart contract!

Finished code

You can check out some of the code in this Code Sandbox or clone the project from the user-dapp folder of this repo.

Conclusion

DApps are a cool way to interact with blockchains. Some good next steps would be learning more about Solidity and smart contracts or just making more DApps with other frameworks!

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.