Hands-on - web3 - web3.js scaffolding

Previously …

Internals

Truffle builds

Take a look within the build folder in the root of the truffle project:

$ ls ./build/contracts/
Cars.json	Migrations.json

Open up build/contracts/Cars.json in a text editor. You will notice that this looks very similar to the outputs of solc, as you see a few things in it that should loom familiar by now: abi, bytecode, and even ast (abstract syntax tree). You might also notice that there are few new things, which solc certainly does not produce.

In particular, we are interested in deployedBytecode and networks. These are not outputs generated by compilation, but instead are outputs generated when the compiled smart contracts are deployed onto the blockchain.

Deployed bytecode is different from compiled bytecode, because upon deployment, the compiled bytecode is executed, and the result of that is what is stored on the blockchain. Networks is a record of which Ethereum networks the smart contract has been deployed on.

Where's my contract?

You can not possibly operate/ interact with a smart contract unless you know the address at which it is deployed to. That is the reason that truffle stores networks within the builds folder. Take a look at networks.${networkId}.address, that is the address at which this contract is stored at.

Restarting

When you are running a local blockchain, such as Ganache, there are a couple of caveats that you need to be aware of which relate to you restarting the blockchain, and both of them have to do with caches going awry.

Firstly, the truffle cache.

This is the builds folder, which we looked at earlier, indicating to truffle that the smart contract has already been deployed. When you restart Ganache, you get a fresh new blockchain, with fresh accounts, no transactions, and no smart contracts - you are starting from scratch all over again.

However, truffle does not know that this has happened, so it balks when there is no smart contract to talk to at the address it was expecting it to have been deployed to. In order to remedy this, the solution is simple: truffle migrate --reset. The --reset flag indicates to truffle to start from scratch once more, and deploy the smart contracts all over again. This is something that you would never want or need to do normally, since a blockchain can never get "restarted". With Ganache though, this is precisely what happened - possible only because it is a simulated blockchain.

Secondly, the metamask cache.

When this happens, you will see the following error in the developer console of the browser:

MetaMask - RPC Error: Internal JSON-RPC error.
{code: -32603, message: "Internal JSON-RPC error."}

… or another one that looks like this:

Uncaught (in promise) Error: Returned error: Error: Error: [ethjs-rpc] rpc error with payload {"id":1370775312058,"jsonrpc":"2.0","params":["0x..."],"method":"eth_sendRawTransaction"} Error: the tx doesn't have the correct nonce. account has nonce of: 4 tx has nonce of: 6

This one is a bit trickier, because it is not possible for you to inspect - it is in the internals of the metamask browser plugin. Metamask also caches what accounts it knows of on the networks that it is connected to, including the transactions that each of them has made - it would be extremely slow otherwise!

When Ganache is restarted, therefore, metamask's cache should also be cleared. Unfortunately, this does not quite happen automatically, and we need to "force" it to happen, using one of the following methods:

  1. Switch from the localhost network (Ganache) to another (e.g. Ropsten), and then back again to the localhost network
  2. Close your browser, and open it again
  3. Log out of metamask, and use the same BIP39 mnemonic phrase, copied from the top of Ganache, to connect using the same set of accounts again.
  4. Uninstall MetaMask, install it again, and set up using the same mnemonic phrase copied from Ganache

Usually the first option just works, but there have been times where the final (and most intrusive) option was the only one that actually worked!

Troublesome?

This might all seem extremely troublesome - and it certainly is! The best thing that you can do here is to just keep Ganache open while you are developing. As soon as you close it and open it again, you're going to need to do the above steps.

We're are building for, and deploying to, a technology that has and immutable ledger at its core after all - so this is to be expected!

Scaffold

Now let us turn this regular web page into a the bare minimum DApp!

We begin by editing app/index.js.

Imports

Import web3.js from the dependencies:

import Web3 from 'web3';
import { toWei } from 'web3-utils';

Obtain a copy of the contract's truffle build and deploy output:

import carsArtefact from "../../build/contracts/Cars.json";

Create an object to represent the client side of this DApp - which has a number of keys which have yet to be initialised.

const CarsApp = {
  web3: undefined,
  accounts: undefined,
  contract: undefined,
};

Wait for the web page to load - time taken by the browser to download files, parse them, render, et cetera - and then detect whether we have an injected Web3 provider. This provider would be injected by metamask

window.addEventListener('load', function() {
  if (window.ethereum) {
    init();
  } else {
    // basically, politely telling the user to install a newer version of
    // metamask, or else fly 🪁
    console.error('No compatible web3 provider injected');
  }
});

What is window.ethereum, and where did it come from?

We didn't set this ourselves anywhere in our code so far, so where did it come from? When you run a page in your browser, the scripts on that page are not the only ones which are run - any browser extensions that you have installed also can run their scripts.

We have MetaMask installed, and that has injected its web3 provider as window.ethereum, with the intent that the DApp code will use it to interact with the network.

Initialisation

Create an initialisation function:

async function init() {
  try {
    window.CarsApp = CarsApp; // DEBUG
    await window.ethereum.enable(); // get permission to access accounts
    CarsApp.web3 = new Web3(window.ethereum);

    // determine network to connect to
    // TODO

    // initialise the contract
    // TODO


    // set the initial accounts
    // TODO

    console.log('CarsApp initialised');
  } catch (err) {
    console.error('Failed to init contract');
    console.error(err);
  }

  // set up listeners for app interactions.
  // TODO

  // trigger various things that need to happen upon app being opened.
  // TODO
}

Connect to a network:

    // determine network to connect to
    let networkId = await CarsApp.web3.eth.net.getId();
    console.log('networkId', networkId);

    let deployedNetwork = carsArtefact.networks[networkId];
    if (!deployedNetwork) {
      console.warn('web3 provider is connected to a network ID that does not matched the deployed network ID');
      console.warn('Pls make sure that you are connected to the right network, defaulting to deployed network ID');
      networkId = Object.keys(carsArtefact.networks)[0];
      deployedNetwork = carsArtefact.networks[networkId];
    }
    console.log('deployedNetwork', deployedNetwork);

Initialise the contract:

    // initialise the contract
    CarsApp.contract = new CarsApp.web3.eth.Contract(
      carsArtefact.abi,
      deployedNetwork.address,
    );

Set the initial accounts:

    // set the initial accounts
    updateAccounts(await CarsApp.web3.eth.getAccounts());

Now create a new function, outside of the implementation of init.

async function updateAccounts(accounts) {
  CarsApp.accounts = accounts;
  console.log('updateAccounts', accounts[0]);
}

Ready to interact

At this point we should have initialised our application within the CarsApp object, and that has all the web3.js stuff that we need to build a DApp with. At this point we need to add front end HTML and Javascript to interact with the deployed smart contract, via what is already in the CarsApp object.

Let's at this point recap what we have:

  • CarsApp.accounts - This will be an array, with one element, and its value will will be your Ethereum account address.
  • CarsApp.web3 - This is an instance of web3.js that has been set up/ configured to make use of MetaMask.
  • CarsApp.contract - This is an a Javascript wrapper around the Cars smart contract. You can now call and send transactions to methods on this particular contract.

Congratulations

🎉🎉🎉 You have got your front-end application connected to an Ethereum network (Ganache) via a web3 provider (MetaMask); and also initialised a client-side copy of your smart contract which you can interact with using Javascript.

Next, we will add features which allow the user to query the state of the smart contract.

Next …