Building a GSN powered (d)app from scratch

In this tutorial, we will integrate several libraries from the SDK, as well as OpenZeppelin Contracts, to build a GSN-powered (d)app. The GSN, or Gas Station Network, is a decentralized network of relayers that allow you to subsidize your users' transactions. This way, they do not need gas to use your (d)app. You can read more about the GSN here.

We will use the create-react-app package to bootstrap a React application and use the @openzeppelin/network package to easily set up a web3 object with GSN support. Also, since the GSN is not available on your local ganache, we will use the @openzeppelin/gsn-helpers package to emulate it locally. We will use the @openzeppelin/cli to manage our contracts and extend from @openzeppelin/contracts-ethereum-package to add GSN capabilities to them.

It might feel like there are many moving pieces here, but each component has a well-defined role in building this application. That said, if you are new to the OpenZeppelin platform, it may help to look into the OpenZeppelin Contracts GSN guide and how to start your first SDK project before you continue reading.

We will create a super simple contract that just counts transactions sent to it from users but will tie it into the GSN so the users will not have to pay for the gas from those transactions. Let’s get started!

Setting up the environment

We will begin by creating a new npm project and installing all dependencies.

mkdir gsn-dapp && cd gsn-dapp
npm init -y
npm install @openzeppelin/network
npm install --save-dev @openzeppelin/gsn-helpers @openzeppelin/contracts-ethereum-package @openzeppelin/upgrades

Make sure you have the OpenZeppelin CLI installed, as well as Ganache for running a local network.

npm install --global @openzeppelin/cli ganache-cli

Use the CLI to set up a new project and follow the prompts so we can write our first contract.

openzeppelin init

Creating our contract

We will write our vanilla Counter contract in the newly created contracts folder.

pragma solidity ^0.5.0;

contract Counter {
  uint256 public value;

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

This is simple enough. Now, let’s modify it to add GSN support. This requires extending from GSNRecipient contract and implementing the acceptRelayedCall method. This method must return whether we accept or reject to pay for a user transaction. For the sake of simplicity, we will be paying for all transactions sent to this contract.

For most (d)apps, it is probably not a good idea to have such a generous policy since any malicious user can easily drain your contract from funds. Check out our guide on GSN payment strategies for different approaches to this problem.
pragma solidity ^0.5.0;

import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";

contract Counter is GSNRecipient {
  uint256 public value;

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

  function acceptRelayedCall(
    address relay,
    address from,
    bytes calldata encodedFunction,
    uint256 transactionFee,
    uint256 gasPrice,
    uint256 gasLimit,
    uint256 nonce,
    bytes calldata approvalData,
    uint256 maxPossibleCharge
  ) external view returns (uint256, bytes memory) {
    return _approveRelayedCall();
  }

  function _preRelayedCall(bytes memory context) internal returns (bytes32) {
  }

  function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, bytes32) internal {
  }
}

Start ganache on a separate terminal by running ganache-cli. Then, create an instance of our new contract using the OpenZeppelin CLI with oz create, and follow the prompts, including saying yes to call a function to initialize the instance. Be sure to copy the address of your instance which is returned at the end of the process.

It is important that you remember to call the initialize() function when creating the contract, as this will be sure to get your contract ready to be used in the GSN.
$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate Counter
? Pick a network development
All contracts are up to date
? Call a function to initialize the instance after creating it? Yes
? Select which function * initialize()
✓ Instance created at 0x7F73086E24ce5834E62075dEAB2b8F10865FFF9B

Great! Now, if we deployed this contract to mainnet or the rinkeby testnet, we would almost be ready to start sending gasless transactions to it since the GSN is set up on both of those networks. However, since we are on a local ganache, we need to set it up ourselves.

Deploying a local GSN for development

The GSN is composed of a central RelayHub contract that coordinates all relayed transactions, as well as multiple decentralized relayers. The relayers are processes that receive requests to relay a transaction via an HTTP interface and send them to the network via the RelayHub.

Having ganache already running, you can start a new relayer in a new terminal using the following command from @openzeppelin/gsn-helpers:

$ npx oz-gsn run-relayer
Deploying singleton RelayHub instance
RelayHub deployed at 0xd216153c06e857cd7f72665e0af1d7d82172f494
Starting relayer
 -Url http://localhost:8090
...
RelayHttpServer starting. version: 0.4.0
...
Relay funded. Balance: 4999305160000000000
Under the hood, this command takes care of several steps to have a local relayer up and running. First, it will download a relayer binary for your platform and start it. It will then deploy the RelayHub contract to your local ganache, registering the relayer on the hub, and funding it so it can relay transactions. You can run these steps individually by using other oz-gsn commands or even directly from your code.

The last step will be to fund our Counter contract. GSN relayers require recipient contracts to have funds since they will then charge the cost of the relayed transaction (plus a fee!) to it. We will again use the oz-gsn set of commands to do this. Make sure to replace the recipient address with the address of your Counter contract instance.

$ npx oz-gsn fund-recipient --recipient 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

Cool! Now that we have our GSN-powered contract and a local GSN to try it out, let’s build a small (d)app.

Creating the dapp

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

npx create-react-app client

First, create a symlink so we can access our compiled contract .json files. From inside the client/src directory, run:

ln -ns ../../build

This will allow our front end to reach our contract artifacts.

Then, replace client/src/App.js file with the following code. This will use @openzeppelin/network to create a new provider connected to the local network. It will use a key generated on the spot to sign all transactions on behalf of the user and will use the GSN to relay them to the network. This allows your users to start interacting with your (d)app right away, even if they do not have MetaMask installed, an Ethereum account, or any ETH at all.

import React, { useState, useEffect, useCallback } from "react";
import { useWeb3Network } from "@openzeppelin/network/react";

const PROVIDER_URL = "http://127.0.0.1:8545";

function App() {
  // get GSN web3
  const context = useWeb3Network(PROVIDER_URL, {
    gsn: { dev: true }
  });

  const { accounts, lib } = context;

  // load Counter json artifact
  const counterJSON = require("./build/contracts/Counter.json");

  // load Counter Instance
  const [counterInstance, setCounterInstance] = useState(undefined);

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

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

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

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

  const increase = async () => {
    await counterInstance.methods.increase().send({ from: accounts[0] });
    getCount();
  };

  return (
    <div>
      <h3> Counter counterInstance </h3>
      {lib && !counterInstance && (
        <React.Fragment>
          <div>Contract Instance or network not loaded.</div>
        </React.Fragment>
      )}
      {lib && counterInstance && (
        <React.Fragment>
          <div>
            <div>Counter Value:</div>
            <div>{count}</div>
          </div>
          <div>Counter Actions</div>
            <button onClick={() => increase()} size="small">
              Increase Counter by 1
            </button>
        </React.Fragment>
      )}
    </div>
  );
}

export default App;
You can pass a dev: true flag to the gsn options when setting up the provider. This will use the GSNDevProvider instead of the regular GSN provider. This is a provider set up specifically for testing or development, and it does not require a relayer to be running to work. This can make development easier, but it will feel less like the actual GSN experience. If you want to use an actual relayer, you can run npx oz-gsn run-relayer locally (see the OpenZeppelin GSN helpers for more info).

Great! We can now fire up our application running npm start from within the client folder. Remember to keep both your ganache and relayer up and running. You should be able to send transactions to your Counter contract without having to use MetaMask or have any ETH at all!

Moving to a testnet

It is not too impressive sending a local transaction in your ganache network, where you already have a bunch of fully-funded accounts. To witness the GSN at its full potential, let’s move our application to the Rinkeby testnet. If you later want to go onto mainnet, the instructions are the same.

Let’s start by deploying our Counter contract to Rinkeby. You will need an account with some Rinkeby Ether for this, which you will have to register in your network.js file. Take a look at deploying to a public network guide for more information.

$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate: Counter
? Pick a network: rinkeby
✓ Added contract Counter
✓ Contract Counter deployed
? Call a function to initialize the instance after creating it?: Yes
? Select which function * initialize()
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

The next step will be to instruct our (d)app to connect to a Rinkeby node instead of to the local network. Change the PROVIDER_URL in your App.js to do this using, for instance, an Infura Rinkeby endpoint. At this point, you will also want to pass in a config object as we will be using a real GSN provider rather than our developer environment, and our config options give us more control over things such as the gas price we are willing to pay. For production (d)apps, you will want to configure this to your requirements.

import { useWeb3Network, useEphemeralKey } from "@openzeppelin/network/react";

// inside App.js#App()
const context = useWeb3Network('https://rinkeby.infura.io/v3/' + INFURA_API_TOKEN, {
  gsn: { signKey: useEphemeralKey() }
});

Redeploy using the cli command oz create, selecting Rinkeby as the network, and copy the address returned at the end (You will need it later to fund your contract!).

We are almost there! If you try to use your (d)app now, you will notice that you are not able to send any transactions. This is because your Counter contract has not been funded on this network yet. Instead of using the oz-gsn fund-recipient command we used earlier, we will now use the online gsn-tool by pasting in the address of your instance. To do this, the web interface requires that you use MetaMask on the Rinkeby Network, which will allow you to deposit funds into your contract.

OpenZeppelin GSN Dapp Tool

That’s it! We can now start sending transactions to our Counter contract on the Rinkeby network from our browser without even having MetaMask installed.

Wrapping up

In this example, we have built a GSN-powered (d)app from scratch combining several OpenZeppelin libraries. First, we extended from the OpenZeppelin Contracts to have our contract act as GSN recipients. Then, we used the OpenZeppelin CLI to compile and deploy our contract on a local network. After that, we set up a local GSN (relayer included!) using the oz-gsn run-relayer command from @openzeppelin/gsn-helpers and funded our recipient with oz-gsn fund-recipient. Once we had our contract set up in our local network, we relied on create-react-app to set up a new client-side app and used @openzeppelin/network to easily get a web3 GSN provider to start interacting with our contract.

If you want to fast-forward to start building your GSN-powered (d)app right away, make sure to check out our GSN Starter Kit, which provides you with a ready-to-use project template that combines all of what we have seen on this guide!