Gasless Transactions on Ethereum

Ahmed Castro - Jan 6 '23 - - Dev Community

Account Abstraction, Rollups, and privacy on blockchain are possible thanks to the ability to execute transactions on behalf of someone else securely. In this video, we are going to create gasless transactions that will be processed by a Relayer using a Verifying Smart Contract. All of this is done securely thanks to cryptography. I hope this video helps you understand where blockchain is headed.

Before we start

For this tutorial you will need NodeJs that I recommend downloading it from Linux via NVM, and also you will need Metamask or another compatible wallet with Goerli funds that you can get from a faucet. You will also need an Infura API Key.

The Verifying Smart Contract

First, we will launch the verifying contract on Goerli Testnet.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
using ECDSA for bytes32;

contract VerifierHelloWorld  {
    string public hello = "Hello world!";
    address public helloSetter;

    function _verify(bytes32 helloHash, address _helloSetter, bytes memory signature) internal pure returns (bool)
    {
        return helloHash
            .toEthSignedMessageHash()
            .recover(signature) == _helloSetter;
    }

    function relaySetHello(string memory _hello, address _helloSetter, bytes memory signature) public
    {
        require(_verify(bytes32(abi.encodePacked(_hello)), _helloSetter, signature), "Invalid signature");
        hello = _hello;
        helloSetter = _helloSetter;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Frontend

Then we build the frontend consisting of the HTML and JS file. The frontend is the interface that allows us to sign transactions and send them to the Relayer.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
</head>
<body>
  <div>
    <h3>Hello Verifier</h3>
    <input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none"></input>
    <p id="account_address" style="display: none"></p>
    <p id="web3_message"></p>
    <p id="contract_state"></p>
    <input type="text" id="_hello"></input>
    <button type="button" id="sign" onclick="_signMessage()" value="Hello!">Sign</button>
    <p id="hashed_message"></p>
    <p id="signature"></p>
    <h4>Hello Verifier</h4>
    <span>Hello</span><br>
    <input type="text" id="_helloRelay"></input><br>
    <span>Hello Setter</span><br>
    <input type="text" id="_helloSetterRelay"></input><br>
    <span>Signature</span><br>
    <input type="text" id="_signatureRelay"></input><br>
    <button type="button" id="relay" onclick="_relaySetHello()">Relay</button>
  </div>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
  <script type="text/javascript" src="blockchain_stuff.js"></script>
</body>

  <script>
    function _signMessage()
    {
      _hello = document.getElementById("_hello").value
      signMessage(_hello)
    }

    function _relaySetHello()
    {
      _helloRelay = document.getElementById("_helloRelay").value
      _helloSetterRelay = document.getElementById("_helloSetterRelay").value
      _signatureRelay = document.getElementById("_signatureRelay").value
      relaySetHello(_helloRelay, _helloSetterRelay, _signatureRelay)
    }
  </script>
</html>
Enter fullscreen mode Exit fullscreen mode

In JavaScript, remember to set the MY_CONTRACT_ADDRESS variable with the contract you just launched

blockchain_stuff.js

const NETWORK_ID = 5

const MY_CONTRACT_ADDRESS = "0xE59da879e33b71C145b7c526a7B8C5b93195C51D"
const MY_CONTRACT_ABI_PATH = "./json_abi/Contract.json"
var my_contract

var accounts
var web3

function metamaskReloadCallback() {
  window.ethereum.on('accountsChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Se cambió el account, refrescando...";
    window.location.reload()
  })
  window.ethereum.on('networkChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Se el network, refrescando...";
    window.location.reload()
  })
}

const getWeb3 = async () => {
  return new Promise((resolve, reject) => {
    if(document.readyState=="complete")
    {
      if (window.ethereum) {
        const web3 = new Web3(window.ethereum)
        window.location.reload()
        resolve(web3)
      } else {
        reject("must install MetaMask")
        document.getElementById("web3_message").textContent="Error: Porfavor conéctate a Metamask";
      }
    }else
    {
      window.addEventListener("load", async () => {
        if (window.ethereum) {
          const web3 = new Web3(window.ethereum)
          resolve(web3)
        } else {
          reject("must install MetaMask")
          document.getElementById("web3_message").textContent="Error: Please install Metamask";
        }
      });
    }
  });
};

const getContract = async (web3, address, abi_path) => {
  const response = await fetch(abi_path);
  const data = await response.json();

  const netId = await web3.eth.net.getId();
  contract = new web3.eth.Contract(
    data,
    address
    );
  return contract
}

async function loadDapp() {
  metamaskReloadCallback()
  document.getElementById("web3_message").textContent="Please connect to Metamask"
  var awaitWeb3 = async function () {
    web3 = await getWeb3()
    web3.eth.net.getId((err, netId) => {
      if (netId == NETWORK_ID) {
        var awaitContract = async function () {
          my_contract = await getContract(web3, MY_CONTRACT_ADDRESS, MY_CONTRACT_ABI_PATH)
          document.getElementById("web3_message").textContent="You are connected to Metamask"
          onContractInitCallback()
          web3.eth.getAccounts(function(err, _accounts){
            accounts = _accounts
            if (err != null)
            {
              console.error("An error occurred: "+err)
            } else if (accounts.length > 0)
            {
              onWalletConnectedCallback()
              document.getElementById("account_address").style.display = "block"
            } else
            {
              document.getElementById("connect_button").style.display = "block"
            }
          });
        };
        awaitContract();
      } else {
        document.getElementById("web3_message").textContent="Please connect to Goerli";
      }
    });
  };
  awaitWeb3();
}

async function connectWallet() {
  await window.ethereum.request({ method: "eth_requestAccounts" })
  accounts = await web3.eth.getAccounts()
  onWalletConnectedCallback()
}

loadDapp()

const onContractInitCallback = async () => {
  var hello = await my_contract.methods.hello().call()
  var helloSetter = await my_contract.methods.helloSetter().call()

  var contract_state = "Hello: " + hello
    + ", helloSetter: " + helloSetter

  document.getElementById("contract_state").textContent = contract_state;
}

const onWalletConnectedCallback = async () => {
}

// Sign and Relay functions

async function signMessage(message)
{
  const hashedMessage = Web3.utils.asciiToHex(message).padEnd(66,'0');

  const signature = await ethereum.request({
    method: "personal_sign",
    params: [hashedMessage, accounts[0]],
  });

  //document.getElementById("hashed_message").textContent="Hashed Message: " + hashedMessage.padEnd(66,'0');
  document.getElementById("signature").textContent="Signature: " + signature;

  // split signature
  /*
  const r = signature.slice(0, 66);
  const s = "0x" + signature.slice(66, 130);
  const v = parseInt(signature.slice(130, 132), 16);
  console.log({ r, s, v });
  */
}

async function relaySetHello(helloRelay, helloSetterRelay, signatureRelay)
{
  var url = "http://localhost:8080/relay?"
  url += "hello=" + helloRelay
  url += "&helloSetter=" + helloSetterRelay
  url += "&signature=" + signatureRelay

  const myRequest = new Request(url, {
    method: 'GET',
    headers: new Headers(),
    mode: 'cors',
    cache: 'default',
  });

  fetch(myRequest);

  alert("Message sent!")
}
Enter fullscreen mode Exit fullscreen mode

The Relayer Backend

Now an example of a backend that is responsible for transmitting transactions to blockchain.

Remember to set the CONTRACT_ADDRESS variable with the contract you just launched. And BACKEND_WALLET_ADDRESS with the wallet that will pay the funds.

backend.js

import createAlchemyWeb3 from "@alch/alchemy-web3"
import dotenv from "dotenv"
import fs from "fs"
import cors from "cors"
import express from "express"

const app = express()
dotenv.config();

const CONTRACT_ADDRESS = "0xE59da879e33b71C145b7c526a7B8C5b93195C51D"
const BACKEND_WALLET_ADDRESS = "0x18747BE67c5886881075136eb678cEADaf808028"
const JSON_CONTRACT_PATH = "./json_abi/Contract.json"
const PORT = 8080
var web3 = null
var contract = null

const loadContract = async (data) => {
  data = JSON.parse(data);

  const netId = await web3.eth.net.getId();
  contract = new web3.eth.Contract(
    data,
    CONTRACT_ADDRESS
  );
}

async function initAPI() {
  const { GOERLI_RPC_URL, PRIVATE_KEY } = process.env;
  web3 = createAlchemyWeb3.createAlchemyWeb3(GOERLI_RPC_URL);

  fs.readFile(JSON_CONTRACT_PATH, 'utf8', function (err,data) {
    if (err) {
      return console.log(err);
    }
    loadContract(data, web3)
  });

  app.listen(PORT, () => {
    console.log(`Listening to port ${PORT}`)
  })
  app.use(cors({
    origin: '*'
  }));
}

async function relaySetHello(hello, helloSetter, signature)
{
  const nonce = await web3.eth.getTransactionCount(BACKEND_WALLET_ADDRESS, 'latest'); // nonce starts counting from 0
  const transaction = {
   'from': BACKEND_WALLET_ADDRESS,
   'to': CONTRACT_ADDRESS,
   'value': 0,
   'gas': 300000,
   'nonce': nonce,
   'data': contract.methods.relaySetHello(
     hello,
     helloSetter,
     signature)
     .encodeABI()
  };
  const { GOERLI_RPC_URL, PRIVATE_KEY } = process.env;
  const signedTx = await web3.eth.accounts.signTransaction(transaction, PRIVATE_KEY);

  web3.eth.sendSignedTransaction(signedTx.rawTransaction, function(error, hash) {
    if (!error) {
      console.log("🎉 The hash of your transaction is: ", hash, "\n");
    } else {
      console.log("❗Something went wrong while submitting your transaction:", error)
    }
  });
}

//http://localhost:8080/relay?helloSetter=0x18747BE67c5886881075136eb678cEADaf808028&hello=hola&signature=0x6903cb647fb3d47b91e8aecc8adc686466557d5edf96814e2b21c745f455a8502e895e696c59f8d65fd9bb57e4f202d45bb6a40c07bc8fd283d666f31264ce411b
app.get('/relay', (req, res) => {
  var hello = req.query["hello"]
  var helloSetter = req.query["helloSetter"]
  var signature = req.query["signature"]
  var message = helloSetter + " setted hello to " + " " + hello
  relaySetHello(hello, helloSetter, signature)
  res.setHeader('Content-Type', 'application/json');
  res.send({
    "message": message
  })
})
initAPI()
Enter fullscreen mode Exit fullscreen mode

We will also need to add the json_abi/Contract.json file which contains the Json ABI of the contract we just launched.
json_abi/Contract.json

[
    {
        "inputs": [],
        "name": "hello",
        "outputs": [
            {
                "internalType": "string",
                "name": "",
                "type": "string"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "helloSetter",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "string",
                "name": "_hello",
                "type": "string"
            },
            {
                "internalType": "address",
                "name": "_helloSetter",
                "type": "address"
            },
            {
                "internalType": "bytes",
                "name": "signature",
                "type": "bytes"
            }
        ],
        "name": "relaySetHello",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]
Enter fullscreen mode Exit fullscreen mode

And also remember to add a .env with your RPC url and your private key.

.env

GOERLI_RPC_URL=YOURURLHERE
PRIVATE_KEY=YOURKEYHERE
Enter fullscreen mode Exit fullscreen mode

package.json

{
  "name": "relayer-demo",
  "version": "1.0.0",
  "description": "",
  "main": "backend.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node backend.js"
  },
  "keywords": [],
  "author": "Filosofía Código",
  "license": "MIT",
  "dependencies": {
    "@alch/alchemy-web3": "^1.4.7",
    "dotenv": "^16.0.3",
    "node-fetch": "^3.3.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we install the dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Alternatively, we install the dependencies manually: npm install @alch/alchemy-web3 dotenv node-fetch.

Test the DApp

To start the frontend.

npm install -g lite-server
lite-server
Enter fullscreen mode Exit fullscreen mode

To start the relayer backend

node backend.js
Enter fullscreen mode Exit fullscreen mode

Now you can sign and relay transactions.

Thanks for watching this video!

Follows us on dev.to and in Youtube for everything related to Blockchain development.

. . . . . . . . . . . . . . . . . . . . . . . . .