Handling Orders using Web3 and Ethereum

Milecia

One of the big draws to blockchain is that you can execute large financial transactions in minutes instead of days or weeks. One day, you could be able to sell your house in 15 minutes and all of the ownership information will be stored securely on the blockchain.

That's why we're going to build a small ordering Dapp to show how this concept works. We'll create a Dapp that lets users create orders on the blockchain, including payments. All of this will be neatly wrapped in a Redwood app and use Cloudinary for our image hosting.

Setting up the Tools

There are a few things we need to install before we touch the code. First, you'll need to install Ganache. This is a free app that lets us develop smart contracts and Dapps against a local Ethereum network. That way we don't have to worry about paying for transactions on the blockchain.

Next, we'll actually create the Redwood app we'll use for our Dapp. In a terminal, run the following command:

1$ yarn create redwood-app order-dapp

This will generate the front-end code in the web directory and back-end code in the api directory we'll need to make this app. There are just a couple more things we need to install. In your terminal, go to the web directory and install these packages.

1$ yarn add truffle truffle-contract web3

These three packages are going to help us interact with the local Ethereum network with our smart contract and also the front-end. Now we have everything installed and set up so we can dive into writing a smart contract.

Writing a Smart Contract with Solidity

We're going to use Truffle to set up our smart contract. So in your terminal go to the web directory and run the following command:

1$ truffle init

This will create a new contracts folder inside the web directory. If you take a look in the contracts folder, you'll see an initial smart contract called Migrations.sol.

It sets the owner of the smart contract to the address that deploys it and gives us access to some restricted functionality for the smart contract.

Now we'll be making our own smart contract with the functionality to handle the orders users want to make. That way you'll see some of the differences in blockchain development instead of back-end development.

We need to make a config file to handle the connections to the local EVM. In the web directory add or update the truffle-config.js file. In this file, add the following code:

1module.exports = {
2 networks: {
3 development: {
4 host: "127.0.0.1",
5 port: 7545,
6 network_id: "*",
7 },
8 },
9 compilers: {
10 solc: {
11 optimizer: {
12 enabled: true,
13 runs: 200
14 },
15 }
16 },
17};

The networks object defines the connection to the local EVM network. You can get all of this info from the server settings in Ganache, but these are the default values. We have enabled the optimizer for the compiler to make our smart contract run smoother.

That's all we need to connect to the network!

Working with a smart contract on the Ethereum network

In the contracts folder, add a new file called OrderMaker.sol. This is where we'll define the function to add orders to the blockchain and make orders accessible to our Dapp.

Open this file and add the following line:

1pragma solidity ^0.5.0;

Every smart contract starts with the version we're using for the compiler. Then we'll define the contract itself.

1contract OrderMaker {
2
3}

There are some similarities between Solidity and JavaScript and a little C++. You'll see them as we fill in this contract. Inside the contract, let's add a struct that defines the order.

1struct Order {
2 string itemName;
3 uint256 price;
4 uint256 quantity;
5}

This is very similar to an interface in TypeScript. It's a type that defines the data we expect in an order. Next, we'll initialize a global array that holds all of the orders for the smart contract.

1Order[] private orders;

This is a private variable which means it can't be accessed outside of the contract. Then we'll add a constructor that creates an initial order on the blockchain.

1constructor() public {
2 _createOrder('Jalapeno', 9, 3);
3}

A constructor in a smart contract only gets executed one time, when the contract is deployed to the network. There are a few things that we'll define before we get to the _createOrder, but it's coming up.

For now, let's define a couple of mappings for the orders that can be accessed outside of the contract like in our Dapp.

1mapping(address => Order) public ordersByUser;
2mapping(uint256 => Order) public ordersById;

A mapping in Solidity is like an object in JavaScript. An example of the ordersById mapping would look like this in JavaScript.

1let ordersById = {
2 1: {
3 itemName: 'Jalapeno',
4 price: 9,
5 quantity: 4
6 },
7}

After these mappings, we need to make a helper function to get the price of the items in the orders users make. This will introduce an interesting thing with Solidity.

1function _lookupPrice(string memory _itemName)
2 private
3 pure
4 returns (uint256 price)
5{
6 if (
7 keccak256(abi.encodePacked(_itemName)) ==
8 keccak256(abi.encodePacked('Jalapeno'))
9 ) {
10 return 9;
11 } else if (
12 keccak256(abi.encodePacked(_itemName)) ==
13 keccak256(abi.encodePacked('Feta'))
14 ) {
15 return 12;
16 } else if (
17 keccak256(abi.encodePacked(_itemName)) ==
18 keccak256(abi.encodePacked('Water'))
19 ) {
20 return 18;
21 } else if (
22 keccak256(abi.encodePacked(_itemName)) ==
23 keccak256(abi.encodePacked('Lemon'))
24 ) {
25 return 15;
26 } else {
27 return 5;
28 }
29}

If you take a look, we're comparing strings to decide what price to return. In Solidity, there's no direct way to handle string comparison. That's why we have all of the keccak256(abi.encodePacked(_itemName)) == keccak256(abi.encodePacked('Jalapeno')) statements. It compares the hashes of the strings and is how we handle string comparison in Solidity smart contracts.

Now we can define that function we called in the constructor to add an initial order.

1function _createOrder(
2 string memory _itemName,
3 uint256 _quantity,
4 uint256 _price
5) internal {
6 orders.push(Order(_itemName, _price, _quantity));
7
8 uint256 id = orders.length;
9
10 ordersByUser[msg.sender] = orders[id - 1];
11
12 ordersById[id] = orders[id - 1];
13}

This is an internal function which means only this contract and contracts that inherit this one can call this _createOrder function. We pass in the three inputs needed to make a new order. Then we add the new order to the private orders array, get an id, and add the order to our public mappings.

The last thing to add is the function that lets users create orders from our Dapp.

1function createOrder(string memory _itemName, uint256 _quantity) public payable {
2 require(msg.value == 0.001 ether);
3 uint256 itemPrice = _lookupPrice(_itemName);
4 _createOrder(_itemName, _quantity, itemPrice);
5}

One important thing to note is that this function has the payable modifier. That means a user is able to send a payment along with their request. This is part of what makes blockchain technology so interesting. It's the only way to securely send money with online transactions.

Can you imagine trying to send money along with an API request? That's one of the problems blockchain solves.

Deploying the Smart Contract to the Local EVM

We have the truffle-config.js file in web which sets up the connection to the local Ethereum network. If you're running Ganache like we set up earlier, these are the network defaults. This is also where you would define connections to test networks or the real Ethereum network.

With this config file updated and the smart contracts written, we can deploy these contracts to the local EVM. In your terminal in the web directory, run this command:

1$ truffle migrate

You should see some output in your terminal that tells you about the smart contracts you just deployed. Make sure that you get the contract address for the OrderMaker contract. We'll need that to connect the Dapp to this smart contract.

You've deployed a smart contract to the EVM! All that's left is creating the front-end code and giving users access to the functionality of the smart contract.

Building the Front-end

In your terminal, go to the root of the project and run the following command:

1$ yarn redwood generate page order

This will create a new directory called OrderPage in web > src > pages. In this new folder, you'll see several files: a test file, a Storybook story, and the component file. Open the OrderPage.tsx file. This is where we'll make the user interface.

Before we do that, there's one more config file we need to set up to access the smart contract on our local Ethereum network.

Adding the Config File

In the web > src directory, add a new file called config.tsx. We'll have two variables in this file. The first will be the address for the smart contract we deployed. At the top of your file, add the following code but be sure to use your contract address:

1export const ORDER_MAKER_ADDRESS = '0x369B41C6951B712e0Cb9c3e13E2737999Ef202c2'

The next variable we need to add is the ABI (application binary interface) for the smart contract. When you deployed the smart contract a couple of steps back, the web > build > contracts directory was added. In this directory, you'll find OrderMaker.json.

Open that file and copy the abi value and add it to the config.tsx file with our address. The code will look like this:

1export const ORDER_MAKER_ABI: any = [
2 {
3 "constant": true,
4 "inputs": [
5 {
6 "name": "",
7 "type": "uint256"
8 }
9 ],
10 "name": "ordersById",
11 "outputs": [
12 {
13 "name": "itemName",
14 "type": "string"
15 },
16 {
17 "name": "price",
18 "type": "uint256"
19 },
20 {
21 "name": "quantity",
22 "type": "uint256"
23 }
24 ],
25 "payable": false,
26 "stateMutability": "view",
27 "type": "function",
28 "signature": "0x2c4cb11a"
29 },
30 {
31 "constant": true,
32 "inputs": [
33 {
34 "name": "",
35 "type": "address"
36 }
37 ],
38 "name": "ordersByUser",
39 "outputs": [
40 {
41 "name": "itemName",
42 "type": "string"
43 },
44 {
45 "name": "price",
46 "type": "uint256"
47 },
48 {
49 "name": "quantity",
50 "type": "uint256"
51 }
52 ],
53 "payable": false,
54 "stateMutability": "view",
55 "type": "function",
56 "signature": "0x933f7ae8"
57 },
58 {
59 "inputs": [],
60 "payable": false,
61 "stateMutability": "nonpayable",
62 "type": "constructor",
63 "signature": "constructor"
64 },
65 {
66 "constant": false,
67 "inputs": [
68 {
69 "name": "_itemName",
70 "type": "string"
71 },
72 {
73 "name": "_quantity",
74 "type": "uint256"
75 }
76 ],
77 "name": "createOrder",
78 "outputs": [],
79 "payable": true,
80 "stateMutability": "payable",
81 "type": "function",
82 "signature": "0x97de706f"
83 }
84]

Think of the contract address as the API we would connect to and the ABI is like the definition for the functions and parameters we're able to interact with. Now we can work on the OrderPage component.

Making the Order Page

We generated our page earlier so you can open the OrderPage.tsx file and delete all of the existing code. We'll start fresh by adding the imports we'll need to make this Dapp work. At the top of your file, add the following code:

1import { useEffect, useState } from 'react'
2import Web3 from 'web3'
3
4import { ORDER_MAKER_ABI, ORDER_MAKER_ADDRESS } from '../../config'

Then we can add the shell for the OrderPage component. Add this code to your file:

1const OrderPage = () => {
2
3}
4
5export default OrderPage

Now we can start filling in this component to finish the logic and UI for users to work with.

Adding the Blockchain Calls

We'll add the states that we need to handle rendering. Inside the component, add this code:

1const [account, setAccount] = useState<string>('')
2const [order, setOrder] = useState(null)
3const [orderMaker, setOrderMaker] = useState(null)
4
5const web3 = new Web3('http://localhost:7545')

The main thing to note is that we're creating a new instance of Web3 that connects to the local EVM. This is similar to how you might define a connection to an API.

Then we need to set the states for our component based on the data we get from our blockchain connection. To do that, add this code below the EVM connection.

1useEffect(() => {
2 loadData()
3}, [])
4
5const loadData = async () => {
6 const accounts = await web3.eth?.getAccounts()
7 setAccount(accounts[0])
8
9 const orderMaker = new web3.eth.Contract(
10 ORDER_MAKER_ABI,
11 ORDER_MAKER_ADDRESS
12 )
13 setOrderMaker(orderMaker)
14
15 const order = await orderMaker.methods.ordersById(1).call()
16 setOrder(order)
17}

We're calling this loadData function in the useEffect hook one time when the component is loaded to get the values we need from the EVM.

Then we define the loadData function. This starts by getting all of the account addresses that are on the network and we're taking the first one to work with. In a production Dapp, you'd have a more sophisticated way to work with addresses.

Next, we create an object that lets us access the methods and parameters available from the OrderMaker smart contract we deployed. We do that by using the contract address and ABI from our configs to connect to this specific smart contract on the network. This is like calling a specific endpoint on an API.

After that, we get the first order from the smart contract. This was created in the constructor function so we'd have something to read from the blockchain initially. We call the ordersById mapping with an id value.

One thing to note is that we use the call method for read requests from the blockchain. This will be an important difference when you see what it looks like to create a new order. We'll add the function to handle this below the loadData function.

1const createOrder = async (event) => {
2 event.preventDefault()
3
4 const { itemName, quantity } = event.target.elements
5
6 await orderMaker.methods.createOrder(itemName.value, quantity.value).send({
7 from: account,
8 gas: 4712388,
9 value: web3.utils.toWei("0.001")
10 })
11}

This createOrder function will take the values from a form submission and store the user's order on the blockchain. The main thing to note is how we make this call to the smart contract.

We're passing the values from the form as the parameters. Then we call the send method to execute this transaction on the blockchain. The user's account address is passed into the send method so we know who executed the request.

Since this is a write request, we need to send some gas with the request. That's why we're working with a local EVM so that we do have to spend real Ether.

We also need to send a payment with the request because we are calling the payable function from the smart contract. We're using toWei because this smallest Ether unit and is the standard for working with payments.

All that's left is adding the UI. You are welcome to make something far nicer than what we're about to create, but all of the core functionality will be in place.

Making the Interface

We'll finish this component by adding the return statement with the elements we want to show the user. Add this code below the createOrder function.

1return (
2 <>
3 <h1>Order from EVM</h1>
4 {order &&
5 <>
6 <div>Item: {order.itemName}</div>
7 <div>Price: {order.price}</div>
8 <div>Quantity: {order.quantity}</div>
9 </>
10 }
11 <form onSubmit={createOrder}>
12 <div>
13 <label htmlFor='itemName'>Item Name:</label>
14 <input name='itemName' type='text' />
15 </div>
16 <div>
17 <label htmlFor='quantity'>Amount:</label>
18 <input name='quantity' type='number' />
19 </div>
20 <button type='submit'>Submit</button>
21 </form>
22 </>
23)

If we have an order set in the component, it'll render the info directly from the blockchain. We also have a form that will allow us to make new orders on the blockchain by calling the function from our smart contract.

Here's what that page should look like when you run your app. In a terminal, go to the root of your project and run the following command:

1$ yarn redwood dev

This will start the dev server and you should see something like this in your browser.

Now you have a fully functional Dapp that stores new info on the blockchain and reads existing info into the browser for your users to see!

Finished Code

You can find the complete code in the order-dapp folder of this repo.

You can also check out the front-end code in this Code Sandbox.

Conclusion

Blockchain technology is just starting to gain traction so there are a lot of differences in jargon and programming workflows that will take time to adjust to. Now is a great time to start learning so that you can be one of the early developers!

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.