Building an OpenZeppelin dapp with Solidity Hot Loader

[UPDATE] :warning: This tutorial uses OpenZeppelin Network.js which is deprecated. We are no longer actively developing new features nor addressing issues.

Introduction

The Solidity Hot Loader brings the web developer experience to Solidity to help you develop your smart contracts and dapps faster.

In this tutorial, we will use Solidity Hot Loader, to build a simple dapp.
We will create a Counter dapp, which displays the value of a Counter that can be increased. We will update the code in the smart contract to show the power of the Solidity Hot Loader.

The Solidity Hot Loader watches your smart contract .sol files you’re using. Once there’s a change, it compiles them, updates the local blockchain using the OpenZeppelin SDK, and refreshes your app in the browser while preserving your blockchain state. Compare this against the existing Solidity development cycle.

The Solidity Hot Loader development cycle:

  1. Make a change to your smart contract .sol file and save it
  2. Wait a bit
  3. Done

The existing (slow) Solidity development cycle:

  1. Make a change to your smart contract .sol file and save it
  2. Compile your contract
  3. Deploy your contract
  4. Restore the state
  5. Restart dapp to get new contract

Video

If you enjoy video :video_camera: , watch @ylv-io’s Devcon presentation: Devcon Delightful Developer Experience With Solidity Hot Loader .

Setting up the environment

We will use the create-react-app package to bootstrap a React application.

First we need a local blockchain to develop on.
We use ganache-cli , so go ahead and install (if you haven’t already) and then run a local blockchain using ganache-cli

$ npm install -g ganache-cli
$ ganache-cli --deterministic

Next we need a project directory to develop our project in. In a separate terminal create the project directory and create package.json with npm init.

mkdir myproject && cd myproject
npm init -y

We use the OpenZeppelin CLI for compiling and deploying our contract, so go ahead and install OpenZeppelin CLI

npm install @openzeppelin/cli

Next, initialize the project with OpenZeppelin. This will create directories and config for our project.

$ npx openzeppelin init
? Welcome to the OpenZeppelin SDK! Choose a name for your project myproject
? Initial project version 1.0.0
Project initialized. Write a new contract in the contracts folder and run 'openzeppelin create' to deploy it.

Counter contract

Our Counter smart contract consists of an integer value and a function to increase it by one.

In the contracts directory save Counter.sol with the following contents:

pragma solidity ^0.6.0;

contract Counter {
    uint256 public value;

    function increase() public {
        value = value + 1;
    }
}

Next we deploy the Counter contract to our local blockchain.

Run npx oz deploy and use the interactive commands to deploy the Counter contract to the development network.
Choose kind: upgradeable.
There is no function that we need to call to initialize.

$ npx oz deploy
✓ Compiled contracts with solc 0.6.12 (commit.27d51765)
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy Counter
✓ Added contract Counter
✓ Contract Counter deployed
All implementations have been deployed
? Call a function to initialize the instance after creating it? No
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
To upgrade this instance run 'oz upgrade'
0xCfEB869F69431e42cdB54A4F4f105C19C080A601

Creating the dapp

We will create our dapp using the create-react-app package, which bootstraps a simple client-side application using React.

npx create-react-app client

Setup Solidity Hot Loader

Now that the directories for our dapp have been created, we can install Solidity Hot Loader.
In the client directory install Solidity Hot Loader

npm install @openzeppelin/solidity-loader

The Solidity Hot Loader uses json-loader so we should go ahead and install that too.

npm install json-loader

Next up, we configure Solidity Hot Loader and set it to enabled.
To stop the Solidity Hot Loader automagically updating our contracts, set disabled to true.

In the client directory create a config directory. In the config directory create a webpack.js file with the following code, notice that we are starting with the Solidity Hot Loader enabled (disabled: false):

// client/config/webpack.js
const solidityLoaderOptions = {
  network: 'development',
  // you can stop loader from automatic compile/push/upgrade
  // action by setting disabled flag to true, but it will still
  // serve .json files from file system
  disabled: false,
};

module.exports = {
  solidityLoader: {
    test: /\.sol$/,
    use: [
      { loader: 'json-loader' },
      {
        loader: '@openzeppelin/solidity-loader',
        options: solidityLoaderOptions,
      },
    ],
  },
  solidityLoaderOptions,
};

We now add in the Solidity Hot Loader to webpack.

In the client directory create a config-overrides.js file with the following code:

// client/config-overrides.js
const { solidityLoader } = require('./config/webpack');

module.exports = function override(config, env) {
  //do stuff with the webpack config...

  // allow importing from outside of app/src folder, ModuleScopePlugin prevents this.
  const scope = config.resolve.plugins.findIndex(o => o.constructor.name === 'ModuleScopePlugin');
  if (scope > -1) {
    config.resolve.plugins.splice(scope, 1);
  }

  // add solidity loader support
  // have to insert before last loader, because CRA user 'file-loader' as default one
  config.module.rules.splice(config.module.rules - 2, 0, solidityLoader);

  return config;
};

We use react-app-rewired to override create-react-app webpack configs to add in the Solidity Hot Loader without ejecting.

In the client directory, install react-app-rewired

$ npm i react-app-rewired

To use react-app-rewired, we now need to change the package.json scripts.

In package.json in the client directory, update the scripts to use react-app-rewired so that they looks as follows:

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },

Add the dapp code

We will use OpenZeppelin Network.js to access the Web3 provider. OpenZeppelin Network.js is an easy to use and reliable library that provides one line access to the web3 API. It can be used in both React and in (vanilla) JavaScript. It also supports the Gas Station Network.

First we change to the client directory

cd client

Then, install OpenZeppelin Network.js
:warning: This project is deprecated. We are no longer actively developing new features nor addressing issues.

npm install @openzeppelin/network 

Our dapp will display a Counter that we can increase the value of, consisting of displaying the value and a button to increase the counter.

In the code below, first we import useWeb3Injected from the React implementation of OpenZeppelin Network.js (@openzeppelin/network/react). We then get a web3Context using useWeb3Injected.
The dapp loads the Counter json artifact.
The display of the Counter is handled in a component.
To start, on the client/src/App.js file, replace the placeholder code in App.js in our react project with the following code:

// client/src/App.js
import React, { useState } from 'react';
import './App.css';

import { useWeb3Network } from '@openzeppelin/network/react';
import Counter from './components/Counter.js';

function App() {
  const web3Context = useWeb3Network('http://127.0.0.1:8545');
  
  // load Counter json artifact
  let counterJSON = undefined;
  try {
    // see https://github.com/OpenZeppelin/solidity-loader
    counterJSON = require('../../contracts/Counter.sol');
  } catch (e) {
    console.log(e);
  }

  // load Counter instance
  const [counterInstance, setCounterInstance] = useState(undefined);
  let deployedNetwork = undefined;
  if (!counterInstance && web3Context && counterJSON && counterJSON.networks && web3Context.networkId) {
    deployedNetwork = counterJSON.networks[web3Context.networkId.toString()];
    if (deployedNetwork) {
      setCounterInstance(new web3Context.lib.eth.Contract(counterJSON.abi, deployedNetwork.address));
    }
  }

  return (
    <div className="App">
      <div>
        <h1>OpenZeppelin Solidity Hot Loader</h1>
        <div>
          <Counter {...web3Context} JSON={counterJSON} instance={counterInstance} deployedNetwork={deployedNetwork} />
        </div>
      </div>
    </div>
  );
}

export default App;

Add a Counter component to the dapp

The Counter component displays the current value of the counter and has a button to increase the value of the counter.

In the client/src directory create a components directory. In the components directory create a Counter.js file with the following code:

// client/src/components/Counter.js
import React, { useState, useEffect, useCallback } from 'react';

export default function Counter(props) {
  const { instance, accounts, lib } = props;
  const { _address } = instance || {};

  const [count, setCount] = useState(0);

  const getCount = useCallback(async () => {
    if (instance) {
      // Get the value from the contract to prove it worked.
      const response = await instance.methods.value().call();
      // Update state with the result.
      setCount(response);
    }
  }, [instance]);

  useEffect(() => {
    getCount();
  }, [getCount, instance]);

  const [sending, setSending] = useState(false);

  const increase = async number => {
    try {
      if (!sending) {
        setSending(true);

        await instance.methods.increase().send({ from: accounts[0] });

        getCount();

        setSending(false);
      }
    } catch (e) {
      setSending(false);
      console.log(e);
    }
  };

  return (
    <div>
      <h3>Counter Instance</h3>
      {lib && instance && (
        <React.Fragment>
          <div>
            <div>Instance address: {_address}</div>
          </div>
          <div>
            <div>Counter Value: {count}</div>
          </div>
            <React.Fragment>
              <div>
                <h4>Counter Actions</h4>
              </div>
              <div>
                <button onClick={() => increase()} size="small">
                  {sending ? <span>Sending ...</span> : <span> Increase Counter</span>}
                </button>
              </div>
            </React.Fragment>
        </React.Fragment>
      )}
    </div>
  );
}

Run the dapp

We can now fire up our application running npm start from within the client directory.

$ npm start

Our dapp that we created will display in the browser https://localhost:3000

With our dapp loaded in the browser, we can increase the value of the counter.

Solidity Hot Loader in action

We can now make changes to our contract and this will be reflected in the dapp using the Solidity Hot Loader.

In Counter.sol change the value that the counter is increased by from 1 to 3.

  function increase() public {
    value = value + 3;
  }

Save the change in Counter.sol and Solidity Hot Loader will compile the contract, upgrade it and refresh the dapp.

Press the Increase Counter button and once the transaction in confirmed the counter will have increased by 3.


Let’s take a moment and go over the Solidity Hot Loader development cycle again:

  1. Make a change to your smart contract .sol file and save it
  2. Wait a bit
  3. Done

We didn’t have to manually do any of the following:

  • Compile your contract
  • Deploy your contract
  • Restore the state
  • Restart dapp to get new contract

This is a big improvement to the developer experience, and allows us to develop faster using the Solidity Hot Loader.


We can keep developing our smart contract and Solidity Hot Loader will update our dapp.

Please note, that Solidity Hot Loader under the covers uses OpenZeppelin Upgrades, so is limited to contracts that can be upgradeable, e.g. no constructors: https://docs.openzeppelin.com/upgrades/2.8/writing-upgradeable

4 Likes

A post was split to a new topic: Solidity Hot Loader with Vue

A post was split to a new topic: The create command is deprecated: Building an OpenZeppelin dapp with Solidity Hot Loader