DEV Community

Ahmed Castro for Filosofía Código EN

Posted on • Updated on

Permit Smart Contracts with EIP 712

Account Abstraction, Rollups, and privacy on blockchain are possible thanks to the ability to execute transactions on behalf of someone else securely. In this blog post, we are going to create a "Permit" setup where transactions will be executed 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.18;

contract Greeter {
    string public greetingText = "Hello World!";
    address public greetingSender;

    struct EIP712Domain {
        string  name;
        string  version;
        uint256 chainId;
        address verifyingContract;
    }

    struct Greeting {
        string text;
        uint deadline;
    }

    bytes32 DOMAIN_SEPARATOR;

    constructor () {
        DOMAIN_SEPARATOR = hash(EIP712Domain({
            name: "Ether Mail",
            version: '1',
            chainId: block.chainid,
            verifyingContract: address(this)
        }));
    }

    function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256(bytes(eip712Domain.name)),
            keccak256(bytes(eip712Domain.version)),
            eip712Domain.chainId,
            eip712Domain.verifyingContract
        ));
    }

    function hash(Greeting memory greeting) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            keccak256("Greeting(string text,uint deadline)"),
            keccak256(bytes(greeting.text)),
            greeting.deadline
        ));
    }

    function verify(Greeting memory greeting, address sender, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            hash(greeting)
        ));
        return ecrecover(digest, v, r, s) == sender;
    }

    function greet(Greeting memory greeting, address sender, uint8 v, bytes32 r, bytes32 s) public {
        require(verify(greeting, sender, v, r, s), "Invalid signature");
        require(block.timestamp <= greeting.deadline, "Deadline reached");
        greetingText = greeting.text;
        greetingSender = sender;
    }
}
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>
    <input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none"></input>
    <p id="account_address" style="display: none"></p>
    <h1>Greeter Verifier</h1>
    <p id="web3_message"></p>
    <p id="contract_state"></p>
    <h3>Sign Greeting</h3>
    <span>Greeting Text</span><br>
    <input type="text" id="_greetingText"></input><br>
    <span>Greeting Deadline</span><br>
    <input type="text" id="_greetingDeadline"></input><br>
    <button type="button" id="sign" onclick="_signMessage()">Sign</button>
    <p id="hashed_message"></p>
    <p id="signature"></p>
    <h3>Verifiy Greeting</h4>
    <span>Greeting Text</span><br>
    <input type="text" id="_greetingTextRelay"></input><br>
    <span>Greeting Deadline</span><br>
    <input type="text" id="_greetingDeadlineRelay"></input><br>
    <span>Greeting Setter</span><br>
    <input type="text" id="_greetingSenderRelay"></input><br>
    <span>Signature</span><br>
    <input type="text" id="_signatureRelay"></input><br>
    <button type="button" id="relay" onclick="_relayGreeting()">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()
    {
      _greetingText = document.getElementById("_greetingText").value
      _greetingDeadline = document.getElementById("_greetingDeadline").value
      signMessage(_greetingText, _greetingDeadline)
    }

    function _relayGreeting()
    {
      _greetingTextRelay = document.getElementById("_greetingTextRelay").value
      _greetingDeadlineRelay = document.getElementById("_greetingDeadlineRelay").value
      _greetingSenderRelay = document.getElementById("_greetingSenderRelay").value
      _signatureRelay = document.getElementById("_signatureRelay").value
      relayGreeting(_greetingTextRelay, _greetingDeadlineRelay, _greetingSenderRelay, _signatureRelay)
    }
  </script>
</html>
Enter fullscreen mode Exit fullscreen mode

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

blockchain_stuff.js

const NETWORK_ID = 5

const GREETER_CONTRACT_ADDRESS = "0x374257dC5707AEDCC1D4F7D0d1b476a57Fc11194"
const GREETER_CONTRACT_ABI_PATH = "./json_abi/Greeter.json"
var greeterContract

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 () {
          greeterContract = await getContract(web3, GREETER_CONTRACT_ADDRESS, GREETER_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 greetingText = await greeterContract.methods.greetingText().call()
  var greetingSender = await greeterContract.methods.greetingSender().call()

  var contract_state = "Greeting Text: " + greetingText
    + ", Greeting Setter: " + greetingSender

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

const onWalletConnectedCallback = async () => {
}

// Sign and Relay functions

async function signMessage(message, deadline)
{
  const msgParams = JSON.stringify({
    types: {
        EIP712Domain: [
            { name: 'name', type: 'string' },
            { name: 'version', type: 'string' },
            { name: 'chainId', type: 'uint256' },
            { name: 'verifyingContract', type: 'address' },
        ],
        Greeting: [
            { name: 'text', type: 'string' },
            { name: 'deadline', type: 'uint' }
        ],
    },
    primaryType: 'Greeting',
    domain: {
        name: 'Ether Mail',
        version: '1',
        chainId: NETWORK_ID,
        verifyingContract: GREETER_CONTRACT_ADDRESS,
    },
    message: {
        text: message,
        deadline: deadline,
    },
  });
  console.log(msgParams)

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

  document.getElementById("signature").textContent="Signature: " + signature;
}

async function relayGreeting(greetingText, greetingDeadline, greetingSender, signature)
{
  const r = signature.slice(0, 66);
  const s = "0x" + signature.slice(66, 130);
  const v = parseInt(signature.slice(130, 132), 16);
  console.log({v,r,s})

  var url = "http://localhost:8080/relay?"
  url += "greetingText=" + greetingText
  url += "&greetingDeadline=" + greetingDeadline
  url += "&greetingSender=" + greetingSender
  url += "&v=" + v
  url += "&r=" + r
  url += "&s=" + s

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

  fetch(relayRequest);

  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 GREETER_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 GREETER_CONTRACT_ADDRESS = "0x374257dC5707AEDCC1D4F7D0d1b476a57Fc11194"
const BACKEND_WALLET_ADDRESS = "0xb6F5414bAb8d5ad8F33E37591C02f7284E974FcB"
const GREETER_CONTRACT_ABI_PATH = "./json_abi/Greeter.json"
const PORT = 8080
var web3 = null
var greeterContract = null

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

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

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

  fs.readFile(GREETER_CONTRACT_ABI_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 relayGreeting(greetingText, greetingDeadline, greetingSender, v, r, s)
{
  const nonce = await web3.eth.getTransactionCount(BACKEND_WALLET_ADDRESS, 'latest'); // nonce starts counting from 0
  const transaction = {
   'from': BACKEND_WALLET_ADDRESS,
   'to': GREETER_CONTRACT_ADDRESS,
   'value': 0,
   'gas': 300000,
   'nonce': nonce,
   'data': greeterContract.methods.greet(
     [greetingText, greetingDeadline],
     greetingSender,
     v,
     r,
     s)
     .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)
    }
  });
}

app.get('/relay', (req, res) => {
  var greetingText = req.query["greetingText"]
  var greetingDeadline = req.query["greetingDeadline"]
  var greetingSender = req.query["greetingSender"]
  var v = req.query["v"]
  var r = req.query["r"]
  var s = req.query["s"]
  var message = greetingSender + " sent a greet: " + " " + greetingText
  relayGreeting(greetingText, greetingDeadline, greetingSender, v, r, s)
  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/Greeter.json

[
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "string",
                        "name": "text",
                        "type": "string"
                    },
                    {
                        "internalType": "uint256",
                        "name": "deadline",
                        "type": "uint256"
                    }
                ],
                "internalType": "struct Example.Greeting",
                "name": "greeting",
                "type": "tuple"
            },
            {
                "internalType": "address",
                "name": "sender",
                "type": "address"
            },
            {
                "internalType": "uint8",
                "name": "v",
                "type": "uint8"
            },
            {
                "internalType": "bytes32",
                "name": "r",
                "type": "bytes32"
            },
            {
                "internalType": "bytes32",
                "name": "s",
                "type": "bytes32"
            }
        ],
        "name": "greet",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "greetingSender",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "greetingText",
        "outputs": [
            {
                "internalType": "string",
                "name": "",
                "type": "string"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "string",
                        "name": "text",
                        "type": "string"
                    },
                    {
                        "internalType": "uint256",
                        "name": "deadline",
                        "type": "uint256"
                    }
                ],
                "internalType": "struct Example.Greeting",
                "name": "greeting",
                "type": "tuple"
            },
            {
                "internalType": "address",
                "name": "sender",
                "type": "address"
            },
            {
                "internalType": "uint8",
                "name": "v",
                "type": "uint8"
            },
            {
                "internalType": "bytes32",
                "name": "r",
                "type": "bytes32"
            },
            {
                "internalType": "bytes32",
                "name": "s",
                "type": "bytes32"
            }
        ],
        "name": "verify",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "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

Remember to add your .env files to your gitignore!

.gitignore

.env
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.

Top comments (7)

Collapse
 
gnurub profile image
Rubén

Esto se podria realizar a la inversa? Es decir que sea el back el que pone la firma y el cliente el q tenga que ejecutarlo (relay)? Por ejemplo decirle al cliente la cantidad que tiene que pagar por un producto, y que este envie la transaccion?

Collapse
 
turupawn profile image
Ahmed Castro

Sí, pienso que debería funcionar perfectamente bien hacerlo al revés 👍

Collapse
 
dripdrops profile image
Sauly Aries

keep getting this and no display of contract data at top ... signature work fine but when sending backend drops out and i get this:

$ node backend.js
Listening to port 8080
file:///home/sauly/sweet/backend.js:55
'data': greeterContract.methods.greet(
^

TypeError: greeterContract.methods.greet is not a function
at relayGreeting (file:///home/sauly/sweet/backend.js:55:36)
at processTicksAndRejections (node:internal/process/task_queues:96:5)

Collapse
 
turupawn profile image
Ahmed Castro

Oh there should be a problem with the Json ABI you get from remix and paste into json_abi/Greeter.json
Make sure it's idential as the blocg post

Collapse
 
chrehman23 profile image
Abdur Rehman

Sir its working fine. but on making transaction its changing the signer signatures like from address is changed. like your from address will be that address which is private key written in .env file. Is there any way to keep from address is in transaction logs who is signer of the transaction.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
mephistopheles9631 profile image
Mephistopheles9631

can you do on for the permit function