Create Web Frontend using Brownie react-mix

In this article, we continue exploring the functionality of Brownie, a smart contract development and testing framework for Solidity and Vyper. We created a simple smart contract in the previous tutorials and deployed it to the Ropsten testnet. Please check the following articles if you haven’t done so.

Our smart contract is now on the testnet, but we only used it on the Brownie console. It would be helpful if we had a front-end web application to interact with it even if we don’t have Brownie on our computer. Brownie has a template system called Brownie Mixes, which we can use as a starting point for specific types of smart contract projects. There is a mix called react-mix, which β€œcomes with everything you need to start using React with a Brownie project”, so it sounds like exactly what we need. Let’s see how it works.

How to set up Brownie react-mix with existing smart contract

As Brownie Mixes are templates, it is probably best to use them when starting a new project. But it is also possible to use it even if we already have a smart contract. We just need to modify the directory structure and some files. 

The current project files

Before installing anything, let’s review our current project. We used the SimpleStorage smart contract from the Solidity documentation, which is stored in the contracts directory. We also created a simple unit test (tests/test_storage.py). We used the script scripts/deploy.py to deploy the smart contract, which stores artefact files in the build directory.

Since we deployed it to the Ropsten testnet (chain ID = 3), the artifact file was stored in the 3 subdirectory, and the map.json file contains the corresponding entry. We also have the file .env (containing the environment variable WEB3_INFURA_PROJECT_ID), brownie-config.yaml (containing one line: dotenv: .envΒ ), which are used to deploy the smart contract to the Ropsten testnet.Β 

The entire directory structure looks like the following.

[~/brownie_test]$ tree .
.
β”œβ”€β”€ .env
β”œβ”€β”€ .gitattributes
β”œβ”€β”€ .gitignore
β”œβ”€β”€ brownie-config.yaml
β”œβ”€β”€ build
β”‚   β”œβ”€β”€ contracts
β”‚   β”‚   └── SimpleStorage.json
β”‚   β”œβ”€β”€ deployments
β”‚   β”‚   β”œβ”€β”€ 3
β”‚   β”‚   β”‚   └── 0xafB83356eeeAA6E18B9a76126DE8edFD61BE5385.json
β”‚   β”‚   └── map.json
β”‚   β”œβ”€β”€ interfaces
β”‚   └── tests.json
β”œβ”€β”€ contracts
β”‚   └── storage.sol
β”œβ”€β”€ interfaces
β”œβ”€β”€ reports
β”œβ”€β”€ scripts
β”‚   └── deploy.py
└── tests
    └── test_storage.py

Install react-mix

We can install react-mix by using the brownie bake command, as shown below.

[~/brownie_test]$ brownie bake react-mix
Brownie v1.17.1 - Python development framework for Ethereum

Downloading from https://github.com/brownie-mix/react-mix/archive/master.zip...
405kiB [00:00, 1.94MiB/s]
SUCCESS: Brownie mix 'react-mix' has been initiated at /Users/mikio/brownie_test/react

It will create a new directory called react in the current directory, containing all the directories we need to start a new project in Brownie. 

We can then install the necessary dependencies.

[~/brownie_test]$ cd ./react/client
[~/brownie_test/react/client]$ yarn install

Walking Through App.js

After react-mix is installed, let’s see how it is implemented. The code is based on the latest version at the time of writing (December 2021).

The main part of the application is in react/client/src/App.js. The componentDidMount() lifecycle method takes care of the initial setup, such as connecting to the Ethereum network, getting the account information, and loading smart contracts.Β 

Line 24 creates a web3 instance by calling getWeb3.js.

react/client/src/App.js:

// Get network provider and web3 instance.
const web3 = await getWeb3()

It uses the window.ethereum object from getEthereum.js, if it’s available, to create the web3 instance.

react/client/src/getWeb3.js:

const ethereum = await getEthereum()
let web3

if (ethereum) {
    web3 = new Web3(ethereum)

Browser extension wallets such as MetaMask inject the window.ethereum object to the web page and provide account information and connectivity to the Ethereum network. The code above uses this functionality.

Going back to App.js, the code requests MetaMask to provide the account information in line 29.

react/client/src/App.js:

// Try and enable accounts (connect metamask)
try {
     const ethereum = await getEthereum()
     ethereum.enable()

MetaMask documentation suggests using ethereum.request({ method: 'eth_requestAccounts' }) instead of ethereum.enable(), so we should probably update it.

Then, it obtains the account information in line 37 and the chain ID in line 40 from MetaMask.

react/client/src/App.js:

// Use web3 to get the user's accounts
const accounts = await web3.eth.getAccounts()

// Get the current chain id
const chainid = parseInt(await web3.eth.getChainId())

It loads the information about smart contracts in line 46 by calling the method loadInitialContracts(), defined from lines 50 to 82.

react/client/src/App.js:

this.setState({
    web3,
    accounts,
    chainid
}, await this.loadInitialContracts)

This method uses another method loadContract(), defined from line 84 to 107, to actually load the smart contract artifacts. It searches the contract address in the file client/src/artifacts/deployments/map.json (line 91).

react/client/src/App.js:

// Get the address of the most recent deployment from the deployment map
let address
try {
    address = map[chain][contractName][0]
} catch (e) {
...

The file map.json is created by Brownie when the smart contract is deployed. Currently, the file exists in the default directory build/deployments, so we will need to change the location.

Then, it loads the smart contract artifacts for the address in line 100. Again the JSON file is currently located in build/deployments, so we will need to change the location of this file as well.Β 

react/client/src/App.js:

// Load the artifact with the specified address
let contractArtifact
try {
    contractArtifact = await import(`./artifacts/deployments/${chain}/${address}.json`)
} catch (e) {
...

It creates a smart contract object using the address and the ABI in the artifact in line 106.

react/client/src/App.js:

return new web3.eth.Contract(contractArtifact.abi, address)

This object is stored in the react state, among others (web3, accounts, chain ID), so we can use it to interact with the smart contract. For example, the following part (line 125 – 139) shows how to call the set() function (line 133) and the get() function (line 136).Β 

react/client/src/App.js:

changeSolidity = async (e) => {
    const {accounts, solidityStorage, solidityInput} = this.state
    e.preventDefault()
    const value = parseInt(solidityInput)
    if (isNaN(value)) {
        alert("invalid value")
        return
    }
    await solidityStorage.methods.set(value).send({from: accounts[0]})
        .on('receipt', async () => {
            this.setState({
                solidityValue: await solidityStorage.methods.get().call()
            })
        })
}

The sample code is written for the smart contracts implemented in the react/contracts directory. Although the smart contracts are essentially the same as ours, we will need to update the code to use our smart contract.

Post-installation modification

We will need to make the following changes so that the React app can use our smart contract.

  • Move the client directory
  • Move the smart contract artifacts
  • Update brownie-config.yaml
  • Update client/src/App.js

Move the client directory

The template creates all the necessary directories so that we can start a new smart contract project in Brownie. But, as we already have our smart contract, we only need the client application in the react/client directory. So, let’s copy the react/client directory to the project root directory.

[~/brownie_test]$ cp -R react/client client

This client directory is essentially the one created by create-react-app with some web3 specific files, such as getEthereum.js and getWeb3.js as well as the directory artifacts which is to store smart contract artifacts, as we saw in the previous section.

After the copy finishes, we can delete the react directory. The project directory structure looks like below (excluding the react directory).

[~/brownie_test]$ tree .
.
β”œβ”€β”€ brownie-config.yaml
β”œβ”€β”€ build
β”‚   β”œβ”€β”€ contracts
β”‚   β”‚   └── SimpleStorage.json
β”‚   β”œβ”€β”€ deployments
β”‚   β”‚   β”œβ”€β”€ 3
β”‚   β”‚   β”‚   └── 0xafB83356eeeAA6E18B9a76126DE8edFD61BE5385.json
β”‚   β”‚   └── map.json
β”‚   β”œβ”€β”€ interfaces
β”‚   └── tests.json
β”œβ”€β”€ client
β”‚   β”œβ”€β”€ README.md
β”‚   β”œβ”€β”€ package-lock.json
β”‚   β”œβ”€β”€ package.json
β”‚   β”œβ”€β”€ node_modules
β”‚   β”‚   β”œβ”€β”€ ...
β”‚   β”‚   └── robots.txt
β”‚   β”œβ”€β”€ public
β”‚   β”‚   β”œβ”€β”€ favicon.ico
β”‚   β”‚   β”œβ”€β”€ ...
β”‚   β”‚   └── robots.txt
β”‚   β”œβ”€β”€ src
β”‚   β”‚   β”œβ”€β”€ App.css
β”‚   β”‚   β”œβ”€β”€ App.js
β”‚   β”‚   β”œβ”€β”€ App.test.js
β”‚   β”‚   β”œβ”€β”€ artifacts
β”‚   β”‚   β”‚   β”œβ”€β”€ contracts
β”‚   β”‚   β”‚   β”œβ”€β”€ deployments
β”‚   β”‚   β”‚   └── interfaces
β”‚   β”‚   β”œβ”€β”€ getEthereum.js
β”‚   β”‚   β”œβ”€β”€ getWeb3.js
β”‚   β”‚   β”œβ”€β”€ index.css
β”‚   β”‚   β”œβ”€β”€ index.js
β”‚   β”‚   β”œβ”€β”€ logo.svg
β”‚   β”‚   β”œβ”€β”€ serviceWorker.js
β”‚   β”‚   └── setupTests.js
β”‚   └── yarn.lock
β”œβ”€β”€ contracts
β”‚   └── storage.sol
β”œβ”€β”€ interfaces
β”œβ”€β”€ reports
β”œβ”€β”€ scripts
β”‚   └── deploy.py
└── tests
    └── test_storage.py

Move the smart contract artefacts

Our current smart contract artifacts are in the build directory. But as we saw in the previous section, the React app needs to access the artifacts, so let’s move the artifacts from the build directory to the client/src/artifacts directory.

[~/brownie_test]$ mv build/* client/src/artifacts/

Update brownie-config.yaml

We also need to let Brownie know that we are now using a new directory for the artifacts; otherwise, Brownie will continue using the build directory. We can do so by adding the build entry to the project_structure section in the file brownie-config.yaml as shown below.

We also need to set dev_deployment_artifacts to true, which will create and remove the artifacts in the development environment.

brownie-config.yaml:

dotenv: .env 

project_structure:
    build: client/src/artifacts

dev_deployment_artifacts: true

Update App.js

Lastly, we need to update the React app to use our existing smart contract. The file App.js is the main application file, so the file’s actual content will naturally be different depending on the smart contract. But, in this article, we will use the template code as much as possible for demonstration purposes.Β 

In this article, we will change the following points:

  • Update the references to the template smart contracts (vyperStorage, solidityStorage) to our smart contract (simpleStorage)
  • Remove the chain ID checks (the sample code is for the Kovan testnet (chain ID = 42), but we are using Ropsten testnet.)
  • Clean up the messages shown on the page
  • Show the transaction Hash after executing the set() function so that we can verify the transaction.

The entire file is as follows. 

client/src/App.js:

import React, {Component} from "react"
import './App.css'
import {getWeb3} from "./getWeb3"
import map from "./artifacts/deployments/map.json"
import {getEthereum} from "./getEthereum"

class App extends Component {

    state = {
        web3: null,
        accounts: null,
        chainid: null,
        simpleStorage: null,
        storageValue: 0,
        storageInput: 0,
        transactionHash: null
    }

    componentDidMount = async () => {

        // Get network provider and web3 instance.
        const web3 = await getWeb3()

        // Try and enable accounts (connect metamask)
        try {
            const ethereum = await getEthereum()
            // ethereum.enable()
            ethereum.request({ method: 'eth_requestAccounts' });
        } catch (e) {
            console.log(`Could not enable accounts. 
            Interaction with contracts not available.
            Use a modern browser with a Web3 plugin to fix this issue.`)
            console.log(e)
        }

        // Use web3 to get the users accounts
        const accounts = await web3.eth.getAccounts()

        // Get the current chain id
        const chainid = parseInt(await web3.eth.getChainId())

        this.setState({
            web3,
            accounts,
            chainid
        }, await this.loadInitialContracts)

    }

    loadInitialContracts = async () => {
        var _chainID = 0;
        if (this.state.chainid === 3){
            _chainID = 3;
        }
        if (this.state.chainid === 1337){
            _chainID = "dev"
        }
        const simpleStorage = await this.loadContract(_chainID, "SimpleStorage")

        if (!simpleStorage) {
            return
        }

        const storageValue = await simpleStorage.methods.get().call()

        this.setState({
            simpleStorage,
            storageValue,
        })
    }

    loadContract = async (chain, contractName) => {
        // Load a deployed contract instance into a web3 contract object
        const {web3} = this.state

        // Get the address of the most recent deployment from the deployment map
        let address
        try {
            address = map[chain][contractName][0]
        } catch (e) {
            console.log(`Could not find any deployed contract "${contractName}" on the chain "${chain}".`)
            return undefined
        }

        // Load the artifact with the specified address
        let contractArtifact
        try {
            contractArtifact = await import(`./artifacts/deployments/${chain}/${address}.json`)
        } catch (e) {
            console.log(`Failed to load contract artifact "./artifacts/deployments/${chain}/${address}.json"`)
            return undefined
        }

        return new web3.eth.Contract(contractArtifact.abi, address)
    }

    changeStorage = async (e) => {
        const {accounts, simpleStorage, storageInput} = this.state
        e.preventDefault()
        const value = parseInt(storageInput)
        if (isNaN(value)) {
            alert("invalid value")
            return
        }
        await simpleStorage.methods.set(value).send({from: accounts[0]})
            .on('transactionHash', async (transactionHash) => {
                this.setState({ transactionHash })
            })
            .on('receipt', async () => {
                this.setState({
                    storageValue: await simpleStorage.methods.get().call()
                })
            })
    }

    render() {
        const {
            web3, 
            accounts,
            simpleStorage,
            storageValue,
            storageInput,
            transactionHash
        } = this.state

        if (!web3) {
            return <div>Loading Web3, accounts, and contracts...</div>
        }

        if (!simpleStorage) {
            return <div>Could not find a deployed contract. Check console for details.</div>
        }

        const isAccountsUnlocked = accounts ? accounts.length > 0 : false

        return (<div className="App">
           {
                !isAccountsUnlocked ?
                    <p><strong>Connect with Metamask and refresh the page to
                        be able to edit the storage fields.</strong>
                    </p>
                    : null
            }
            <h1>Simple Storage</h1>
            <div>The current stored value is {storageValue}.</div>
            <br/>
            <form onSubmit={(e) => this.changeStorage(e)}>
                <div>
                    <label>Change the value to </label>
                    <input
                        name="storageInput"
                        type="text"
                        value={storageInput}
                        onChange={(e) => this.setState({storageInput: e.target.value})}
                    />.
                    <p>
                        <button type="submit" disabled={!isAccountsUnlocked}>Submit</button>
                    </p>
                </div>
            </form>
            <br/>
            {transactionHash ?
                <div>
                    <p>Last transaction Hash: {transactionHash}</p>
                </div>
            : null
            }
        </div>)
    }
}

export default App

Import account into MetaMask

Since we use MetaMask to interact with the Ethereum network in the React app, we need an account on MetaMask. For demonstration purposes, we will import our deployment_account account to MetaMask.

We can find the account by running the brownie accounts list command. If you don’t have an account, you can create one by following the previous article.Β 

[~/brownie_test]$ brownie accounts list
Brownie v1.17.1 - Python development framework for Ethereum

Found 1 account:
 └─deployment_account: 0x84aa678F1088eC3D6cb74204bB239615846C3526

If you have not installed MetaMask, you can find the tutorial on Finxter below. 

To import the deployment_account account into MetaMask, we first need to export it from Brownie as a JSON keystore file by running brownie accounts export command.Β 

[~/brownie_test]$ brownie accounts export deployment_account ~/brownie_test/deployment_account.json
Brownie v1.17.1 - Python development framework for Ethereum

SUCCESS: Account with id 'deployment_account' has been exported to keystore '/Users/mikio/brownie_test/deployment_account.json'

Log on to MetaMask, click on the account icon in the top-right corner and select Import Account.

Select β€œJSON File” from the drop-down menu as Select Type and choose the file exported above. Type the password (created when the account was generated) and click β€œImport”. 

After a few minutes, the account should become visible on MetaMask. Optionally we can update the account name to deployment_account by clicking on the three dots on the right-hand side of the account name and selecting Account details. Then click on the pencil icon next to the account name.

Use Development Network

We can test the application using the local blockchain (Ganache). Start the Brownie console with the --network development option, which automatically runs the ganache-cli command.

[~/brownie_test]$ brownie console --network development
Brownie v1.17.1 - Python development framework for Ethereum

BrownieTestProject is the active project.

Launching 'ganache-cli --accounts 10 --hardfork istanbul --gasLimit 12000000 --mnemonic brownie --port 8545 --defaultBalanceEther 100000000000000000000'...
Brownie environment is ready.

Then, deploy the smart contract.

>>> run('deploy')

Running 'scripts/deploy.py::main'...
Enter password for "deployment_account": 
Transaction sent: 0x9a45d022b665c1c7e9a9b5df937d8f5ced4da2d6245f67c34474a6b32ff2a85a
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  SimpleStorage.constructor confirmed   Block: 1   Gas used: 90539 (0.75%)
  SimpleStorage deployed at: 0x68612eDF8f534eB752DD1Ea1aa931C7808CF75D1

Note that the artifacts are stored in the client/src/artifacts/deployments/dev directory, and a new entry is added to the file client/src/artifacts/deployments/map.json.

client/src/artifacts/deployments/map.json:

{
  "3": {
    "SimpleStorage": [
      "0xafB83356eeeAA6E18B9a76126DE8edFD61BE5385"
    ]
  },
  "dev": {
    "SimpleStorage": [
      "0x68612eDF8f534eB752DD1Ea1aa931C7808CF75D1"
    ]
  }
}

We also need to transfer some Ether to the deployment account.

>>> deployment_account = accounts.load('deployment_account')
Enter password for "deployment_account": 
>>> deployment_account.balance()
0
>>> accounts[0].transfer(deployment_account, '1 ether')
Transaction sent: 0x148c052e4f0fd172cab4b1c779d663edce80e31198833bdaa3ddd6ffcdbe73ff
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 0
  Transaction confirmed   Block: 2   Gas used: 21000 (0.18%)

<Transaction '0x148c052e4f0fd172cab4b1c779d663edce80e31198833bdaa3ddd6ffcdbe73ff'>
>>> deployment_account.balance()
1000000000000000000

On MetaMask, the account balance should also show 1 Ether. Make sure to select the network localhost:8545.Β 

Now, open a different terminal and start the React app, which should automatically open the page using the default web browser on localhost:3000.

[~/brownie_test]$ cd client
[~/brownie_test/client]$ yarn start

Compiled successfully!

You can now view client in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.1.3:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

We should see the main screen on the browser showing that the current store value is 0.

We can type an arbitrary value (e.g. 10) in the text field and click the Submit button. A MetaMask confirmation window pops up, showing the transaction fee.Β 

After clicking the Confirm button, the store value should become 10, and the transaction hash should appear at the bottom.

We can also confirm the current storage value from the Brownie console. Load the SimpleStorage smart contract using the at method. We can find the address in the deploy output above or in the file client/src/artifacts/deployments/map.json.Β 

As expected, the get() function returns the value 10.

>>> simple_storage = SimpleStorage.at('0x68612eDF8f534eB752DD1Ea1aa931C7808CF75D1')
>>> simple_storage.get()
10

We can stop the React app by typing Ctrl-C on the second terminal and the Brownie console by typing quit() on the first terminal. Terminating the Brownie console (i.e. the local Ganache blockchain) removes the development artifacts in the client/src/artifacts/deployment directory.

Β Use Ropsten testnet

We can use the Ropsten testnet in the same way. To interact with the smart contract, the account (deployment_account in this case) needs some test Ether on the Ropsten testnet to pay transaction fees.

In addition, make sure that the valid Infura project ID is set to WEB3_INFURA_PROJECT_ID environment variable in the .env file, which we did in the previous article.

On the terminal, let’s start the React app.Β 

[~/brownie_test/client]$ yarn start

Compiled successfully!

You can now view client in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.1.3:3000

Note that the development build is not optimized.
To create a production build, use yarn build.

Open MetaMask and select the network to β€œRopsten Test Network”.

Reload the application, and now the main page should appear. It shows that the current value is 5.

Let’s change it to a different value. Type an arbitrary value (e.g. 10) in the text field and click the Submit button. The MetaMask confirmation window pops up.

After clicking on the Confirm button, the transaction hash will appear at the bottom. We can search the transaction on Ropsten Etherscan, and it will show the details about the transaction. For example, it shows that the input data was 10 at the bottom for this example.

Once the transaction has been confirmed, the React app shows that the currently stored value is 10.

We can check the value on the brownie console as well. On the second terminal, start the console using the --network ropsten option.

[~/brownie_test]$ brownie console --network ropsten    
Brownie v1.17.1 - Python development framework for Ethereum

BrownieTestProject is the active project.
Brownie environment is ready.
>>> network.show_active()
'ropsten'
>>> network.is_connected()
True

We can find our smart contract address in the file client/src/artifacts/deployments/map.json.Β 

client/src/artifacts/deployments/map.json:

{
  "3": {
    "SimpleStorage": [
      "0xafB83356eeeAA6E18B9a76126DE8edFD61BE5385"
    ]
  }
}

Load the smart contract by specifying the address and checking the current storage value.

>>> simple_storage = SimpleStorage.at('0xafB83356eeeAA6E18B9a76126DE8edFD61BE5385')
>>> simple_storage.get()
10

We can stop the React app by typing Ctrl-C on the first terminal and the Brownie console by typing quit() on the second terminal. Since the artefacts are stored in the client/src/artifacts directory, we can always restart the React app and Brownie console to access the smart contract on the testnet.

Summary

In this article, we explored the Brownie react-mix, a template for creating a React app as a front-end application in Brownie. It comes with sample smart contracts and React app code. Since we already had a smart contract, we modified the directory structure and some files to use our smart contract in the React app. Then we ran the app in the local development environment and the Ropsten testnet.Β 

The application implemented in this article is very primitive, but it shows the basic functionality to use the Ethereum network via MetaMask. It is a regular React application, so we can use it as a starting point and enhance it to suit our needs. 

As Brownie is Python-based and React is JavaScript, they might not be a natural fit. But the functionality to share the artifacts between Brownie and React would be helpful when developing smart contracts and the front-end application at the same time.Β 

You can find more about Brownie React Mix on Github.