Connecting to Public Test Networks with Truffle

Connecting to Public Test Networks with Truffle

In this guide, we will use Truffle to deploy the Box contract to a testnet, while learning:

  • What test networks are available
  • How to set up your Truffle project for working on a testnet
  • How to deploy and interact with your testnet contract instances

After you have written your contracts, and tried them out locally and tested them thoroughly, it’s time to move to a persistent public testing environment, where you and your beta users can start interacting with your application.

We will use public testing networks (aka testnets) for this, which are networks that operate similar to the main Ethereum network, but where Ether has no value and is free to acquire - making them ideal for testing your contracts at no cost.

Remember that deploying to a public test network is a necessary step when developing an Ethereum project. They provide a safe environment for testing that closely mimics the main network - you don’t want to take out your project for a test drive in a network where mistakes will cost you money!

This guide for Truffle is based on Connecting to Public Test Networks from OpenZeppelin Learn guides which uses the OpenZeppelin CLI.

Available Testnets

There are four test networks available for you to choose, each with their own characteristics:

Testnet Description ID
Ropsten The only proof-of-work testnet. It has unpredictable block times and frequent chain reorganizations. At the same time, it is the chain that most closely resembles mainnet. 3
Rinkeby A proof-of-authority network. This means that new blocks are added by a set of pre-defined trusted nodes, instead of by whichever miner provides a proof-of-work. It only works with Geth clients, and has 15-second block times. 4
Kovan Another proof-of-authority network, but this one runs only with Parity clients, and has 4-second block times. 42
Goerli Also a proof-of-authority network, but compatible with both Geth and Parity clients, with 15-second block times. 6

NOTE: Each network is identified by a numeric ID. Local networks usually have a large random value, while id=1 is reserved for the main Ethereum network.

It is up to you whether you want to test in Ropsten’s unpredictable environment to assess how robust your application is, or a more reliable one to simplify the testing experience.

Connecting a Project to a Public Network

To connect our Truffle project to a public testnet, we will need to:

  • Get hold of a testnet node
  • Create a new account
  • Update our networks configuration in our Truffle configuration file
  • Fund our testing account

Accessing a Testnet Node

While you can spin up your own Geth or OpenEthereum node connected to a testnet, the easiest way to access a testnet is via a public node service such as Infura. Infura provides access to public nodes for all testnets and the main network, via both free and paid plans.

NOTE: We say a node is public when it can be accessed by the general public, and manages no accounts. This means that it can reply to queries and relay signed transactions, but cannot sign transactions on its own.

Head over to Infura or a public node provider of your choice, sign up, and jot down your assigned project ID - we will use it later to connect to the network.

Creating a New Account

To send transactions in a testnet, you will need a new Ethereum account. There are many ways to do this: here we will use the mnemonics package, which will output a fresh mnemonic (a set of 12 words) we will use to derive our accounts:

$ npx mnemonics
planet auto sign choice ...

:warning: WARNING: Make sure to keep your mnemonic secure. Even if it is just for testing purposes, there are still malicious users out there who will wreak havoc on your testnet deployment for fun!

Setting up our project

NOTE: This part assumes you have already set up a new Truffle project.
If you haven’t, you can run the following:

$ mkdir myproject && cd myproject
$ npm init -y
$ npm i truffle
$ npx truffle init

We will use our beloved Box contract from the OpenZeppelin Learn guides. Our Box contract will let people store a value that can be later retrieved.

Using your favorite editor create Box.sol in the contracts directory with the following contents:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
// from: https://docs.openzeppelin.com/learn/developing-smart-contracts#setting-up-a-solidity-project
pragma solidity ^0.6.0;


contract Box {
    uint256 private value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}

To deploy our contract we need a migrations script. (see Truffle documention on running migrations)

Using your favorite editor create 2_deploy.js in the migrations directory with the following contents:

// migrations/2_deploy.js
// SPDX-License-Identifier: MIT
const Box = artifacts.require("Box");

module.exports = function(deployer) {
  deployer.deploy(Box);
};

Configuring the Network

Since we are using public nodes, we will need to sign all our transactions locally. We will use @truffle/hdwallet-provider to do this, setting it up with our mnemonic. We will also tell the provider how to connect to the test network by using the Infura endpoint.

Let’s start by installing the provider:

$ npm install --save-dev @truffle/hdwallet-provider

Then, we will update our truffle-config.js file with a new connection to the testnet. Here we will use Ropsten, but you can use whichever you want:

const { projectId, mnemonic } = require('./secrets.json');
const HDWalletProvider = require('@truffle/hdwallet-provider');

module.exports = {

  networks: {
    development: {
      host: "127.0.0.1",     // Localhost (default: none)
      port: 8545,            // Standard Ethereum port (default: none)
      network_id: "*",       // Any network (default: none)
    },
    // Useful for deploying to a public network.
    // NB: It's important to wrap the provider as a function.
    ropsten: {
      provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/${projectId}`),
      network_id: 3,       // Ropsten's id
      gas: 5500000,        // Ropsten has a lower block limit than mainnet
      confirmations: 2,    // # of confs to wait between deployments. (default: 0)
      timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
      skipDryRun: true     // Skip dry run before migrations? (default: false for public nets )
    },
   },
 };

NOTE: See the HDWalletProvider documentation for information on configuration options.

Note in the first line that we are loading the project id and mnemonic from a secrets.json file, which should look like the following, but using your own values. Make sure to .gitignore it!

{
    "mnemonic": "planet auto sign choice ...",
    "projectId": "305c137050..."
}

TIP: Instead of a secrets.json file, you can use whatever secret-management solution you like for your project. A popular and simple option is to use dotenv for injecting secrets as environment variables.

We can now test out that this configuration is working by listing the accounts we have available for the Ropsten network. Remember that yours will be different, as they depend on the mnemonic you used.

$ npx truffle console --network ropsten
truffle(ropsten)> await web3.eth.getAccounts()
[
  '0xb022FFdC8F9156BcaA9818cb7B9658364758B5De',
  '0xa9372cde6f32f7EC77FB4962d6C292611dF30117',
  '0xB568d2240E1d3D08c52488d2ceb4e2525aD0F38c',
...
]

We can also test the connection to the Infura node, by querying our account balance, remembering to use the first address from getAccounts as yours will be different.

$ npx truffle console --network ropsten
truffle(ropsten)> await web3.eth.getBalance('0xb022FFdC8F9156BcaA9818cb7B9658364758B5De')
'0'

Empty! This points to our next task: getting testnet funds so that we can send transactions.

Funding the Testnet Account

Most public testnets have a faucet: a site that will provide you with a small amount of test Ether for free. If you are on Ropsten, head on to the Ropsten Faucet to get funds. Alternatively, you can also use MetaMask’s faucet to ask for funds directly to your MetaMask accounts.

Armed with a funded account, let’s deploy our contracts to the testnet!

Working on a Testnet

With a project configured to work on a public testnet, we can now finally deploy our Box contract. The command here, other than specifying the network, is the same as if you were on your local development network, though it will take a few seconds to run as new blocks are mined.

$ npx truffle migrate --network ropsten

Compiling your contracts...
===========================
> Compiling ./contracts/Box.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /home/abcoathup/projects/forum/myproject/build/contracts
> Compiled successfully using:
   - solc: 0.6.7+commit.b8d736ae.Emscripten.clang



Starting migrations...
======================
> Network name:    'ropsten'
> Network id:      3
> Block gas limit: 8000029 (0x7a121d)


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0xb3ae2fe5eb982b747ec3bca0530692245cdc02d0e4bbab0fd5d30c3ec9213097
   > Blocks: 2            Seconds: 17
   > contract address:    0x27fbe63444eC4B5F97217DAC66f1b0CdcF9c4859
   > block number:        7983802
   > block timestamp:     1590647318
   > account:             0xb022FFdC8F9156BcaA9818cb7B9658364758B5De
   > balance:             0.99342436
   > gas used:            164391 (0x28227)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00328782 ETH

   Pausing for 2 confirmations...
   ------------------------------
   > confirmation number: 1 (block: 7983803)
   > confirmation number: 3 (block: 7983805)

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00328782 ETH


2_deploy.js
===========

   Deploying 'Box'
   ---------------
   > transaction hash:    0xed23f94b7fec11934d504e3caf04279e6223a43aba0133ce2bbc32d2d50fb702
   > Blocks: 2            Seconds: 9
   > contract address:    0x98C6f9aeAB681568988cd8CCD5B86AdaE380672F
   > block number:        7983812
   > block timestamp:     1590647396
   > account:             0xb022FFdC8F9156BcaA9818cb7B9658364758B5De
   > balance:             0.99041172
   > gas used:            108291 (0x1a703)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00216582 ETH

   Pausing for 2 confirmations...
   ------------------------------
   > confirmation number: 1 (block: 7983813)
   > confirmation number: 2 (block: 7983814)

   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00216582 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0.00545364 ETH

That’s it! Your Box contract instance will be forever stored in the testnet, and publicly accessible to anyone.

You can see your contract on a block explorer such as Etherscan. Remember to access the explorer on the testnet where you deployed your contract, such as ropsten.etherscan.io for Ropsten.

TIP: You can check out the contract we deployed in the example above, along with all transactions sent to it, here.

You can also interact with your instance as you regularly would, either using truffle console, or programmatically using web3.

$ npx truffle console --network ropsten
truffle(ropsten)> box = await Box.deployed()
truffle(ropsten)> await box.store('42')
{
  tx: '0x3d125322e7d0738d554424085197e9d94b8280849cdac6c3010ea426427928ec',
  receipt: {
    blockHash: '0xf35a7f53e502f524adda3bca18878982e20e775072038e74e3502f078c684714',
    blockNumber: 7983886,
    contractAddress: null,
    cumulativeGasUsed: 42513,
    from: '0xb022ffdc8f9156bcaa9818cb7b9658364758b5de',
    gasUsed: 42513,
    logs: [ [Object] ],
    logsBloom: '0x40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000400000000000000000000000000000',
    status: true,
    to: '0x98c6f9aeab681568988cd8ccd5b86adae380672f',
    transactionHash: '0x3d125322e7d0738d554424085197e9d94b8280849cdac6c3010ea426427928ec',
    transactionIndex: 0,
    rawLogs: [ [Object] ]
  },
  logs: [
    {
      address: '0x98C6f9aeAB681568988cd8CCD5B86AdaE380672F',
      blockHash: '0xf35a7f53e502f524adda3bca18878982e20e775072038e74e3502f078c684714',
      blockNumber: 7983886,
      logIndex: 0,
      removed: false,
      transactionHash: '0x3d125322e7d0738d554424085197e9d94b8280849cdac6c3010ea426427928ec',
      transactionIndex: 0,
      id: 'log_e38e9ac8',
      event: 'ValueChanged',
      args: [Result]
    }
  ]
}
truffle(ropsten)> (await box.retrieve()).toString()
'42'

Keep in mind that every transaction will cost some gas, so you will eventually need to top up your account with more funds.

Next Steps

After thoroughly testing your application on a public testnet, you are ready for the last step on the development journey: deploying your application in production.