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;
}
}
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>
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!")
}
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()
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"
}
]
And also remember to add a .env
with your RPC url and your private key.
.env
GOERLI_RPC_URL=YOURURLHERE
PRIVATE_KEY=YOURKEYHERE
Remember to add your .env files to your gitignore!
.gitignore
.env
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"
}
}
Finally, we install the dependencies:
npm install
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
To start the relayer backend
node backend.js
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.