Transacciones sin Pagar Gas en Ethereum
Ahmed Castro
Posted on January 6, 2023
El Account Abstraction, los Rollups y la privacidad en el blockchain son posibles gracias a la capacidad de ejecutar transacciones en nombre de otra persona de manera segura. En este video, vamos a crear una transacción sin pagar gas que será procesada por un Relayer utilizando un Smart Contract Verificador. Todo esto se realiza de manera segura gracias a la criptografía. Espero que este video les ayude a comprender hacia dónde se está dirigiendo el blockchain.
Antes de comenzar
Para este tutorial ocuparás NodeJs que recomiendo descargarlo en Linux via NVM, y también necesitarás Metamask u otra wallet compatible con fondos en Goerli que puedes obetener desde un faucet. Adicionalmente ocuparás una llave RPC que puedes conseguir gratuitamente en Infura.
El Smart Contract Verificador
Primero lanzaremos el contrato verificador en 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;
}
}
El Frontend
Luego construimos el frontend que consta del archivo de HTML y JS. El frontend es la interfaz que nos permite firmar transacciones y enviarlas al 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>
En Javascript, recuerda establecer la variable MY_CONTRACT_ADDRESS
con el contrato que recién lanzaste.
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!")
}
El Relayer Backend
Ahora un ejemplo de un backend que se encarga de transmitir transacciones al blockchain.
Recuerda establecer la variable CONTRACT_ADDRESS
con el contrato que recién lanzaste. Y BACKEND_WALLET_ADDRESS
con la wallet que pagará los fondos.
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()
Necesitaremos agregar también el archivo json_abi/Contract.json
que contiene el Json ABI del contrato que recién lanzamos.
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"
}
]
Y recuerda también agregar un .env
con tu url RPC y tu llave privada.
.env
GOERLI_RPC_URL=YOURURLHERE
PRIVATE_KEY=YOURKEYHERE
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"
}
}
Finalmente instalamos las dependencias:
npm install
O alternativamente instalamos las dependencias manualmente: npm install @alch/alchemy-web3 dotenv node-fetch
.
Probar la DApp
Para levantar el frontend.
npm install -g lite-server
lite-server
Para levantar el relayer backend
node backend.js
Ahora puedes firmar y relayear transacciones.
¡Gracias por ver este tutorial!
Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.
Posted on January 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024