Skip to content

Instantly share code, notes, and snippets.

@spalladino
Created October 7, 2020 21:11
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save spalladino/7fb3533e36e9b9a833f8e5c568c86815 to your computer and use it in GitHub Desktop.
Save spalladino/7fb3533e36e9b9a833f8e5c568c86815 to your computer and use it in GitHub Desktop.
Example application for sending EIP2771 meta-txs through a Defender Relayer
// Sample recipient contract for the application.
// Refer to the https://github.com/opengsn/forwarder repo for more info.
pragma solidity ^0.6.8;
import "./gsnv2/BaseRelayRecipient.sol";
contract Boxes is BaseRelayRecipient {
mapping(address => uint256) values;
event ValueSet(address who, uint256 value);
constructor(address _trustedForwarder) public {
trustedForwarder = _trustedForwarder;
}
function getValue(address who) public view returns (uint256) {
return values[who];
}
function setValue(uint256 value) public {
address who = _msgSender();
values[who] = value;
emit ValueSet(who, value);
}
}
// User client-side code for signing a meta-tx request
// Note that, instead of sending the tx, the end-user signs the request and sends it to a relayer server
// This server will process the request, and if valid, send the tx via a Defender Relayer
import ethers from 'ethers';
import BoxesAbi from '../abis/Boxes.json';
import ForwarderAbi from '../abis/Forwarder.json';
const ZeroAddress = '0x0000000000000000000000000000000000000000';
const BoxesAddress = process.env.REACT_APP_BOXES_ADDRESS || ZeroAddress;
const ForwarderAddress = process.env.REACT_APP_FORWARDER_ADDRESS || ZeroAddress;
const RelayUrl = process.env.REACT_APP_RELAY_URL || '/relay';
const EIP712DomainType = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const ForwardRequestType = [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'gas', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'data', type: 'bytes' }
]
const TypedData = {
domain: {
name: 'Defender',
version: '1',
chainId: 4,
verifyingContract: ForwarderAddress,
},
primaryType: 'ForwardRequest',
types: {
EIP712Domain: EIP712DomainType,
ForwardRequest: ForwardRequestType
},
message: {}
};
export async function submit(number) {
// Initialize provider and signer from metamask
await window.ethereum.enable();
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const from = await signer.getAddress();
const network = await provider.getNetwork();
if (network.chainId !== 4) throw new Error(`Must be connected to Rinkeby`);
// Get nonce for current signer
const forwarder = new ethers.Contract(ForwarderAddress, ForwarderAbi, provider);
const nonce = await forwarder.getNonce(from).then(nonce => nonce.toString());
// Encode meta-tx request
const boxesInterface = new ethers.utils.Interface(BoxesAbi);
const data = boxesInterface.encodeFunctionData('setValue', [number]);
const request = {
from,
to: BoxesAddress,
value: 0,
gas: 1e6,
nonce,
data
};
const toSign = { ...TypedData, message: request };
// Directly call the JSON RPC interface, since ethers does not support signTypedDataV4 yet
// See https://github.com/ethers-io/ethers.js/issues/830
const signature = await provider.send('eth_signTypedData_v4', [from, JSON.stringify(toSign)]);
// Send request to the server
const response = await fetch(RelayUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...request, signature })
}).then(r => r.json());
return response;
}
// Server-side code for receiving meta-tx requests
// The server validates the request, and if accepted, will send the meta-tx via a Defender Relayer
const RelayerApiKey = process.env.APP_API_KEY;
const RelayerSecretKey = process.env.APP_SECRET_KEY;
const ForwarderAddress = process.env.APP_FORWARDER_ADDRESS;
const { Relayer } = require('defender-relay-client');
const { ethers } = require('ethers');
const ForwarderAbi = require('../artifacts/Forwarder.json').abi;
const { TypedDataUtils } = require('eth-sig-util');
const { bufferToHex } = require('ethereumjs-util');
const EIP712DomainType = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
]
const ForwardRequestType = [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'gas', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'data', type: 'bytes' }
];
const TypedData = {
domain: {
name: 'Defender',
version: '1',
chainId: 4,
verifyingContract: ForwarderAddress
},
primaryType: 'ForwardRequest',
types: {
EIP712Domain: EIP712DomainType,
ForwardRequest: ForwardRequestType
},
message: {}
};
const GenericParams = 'address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data';
const TypeName = `ForwardRequest(${GenericParams})`;
const TypeHash = ethers.utils.id(TypeName);
const DomainSeparator = bufferToHex(TypedDataUtils.hashStruct('EIP712Domain', TypedData.domain, TypedData.types));
const SuffixData = '0x';
async function relay(request) {
// Unpack request
const { to, from, value, gas, nonce, data, signature } = request;
// Validate request
const provider = new ethers.providers.InfuraProvider('rinkeby', process.env.APP_INFURA_KEY);
const forwarder = new ethers.Contract(ForwarderAddress, ForwarderAbi, provider);
const args = [
{ to, from, value, gas, nonce, data },
DomainSeparator,
TypeHash,
SuffixData,
signature
];
await forwarder.verify(...args);
// Send meta-tx through Defender
const forwardData = forwarder.interface.encodeFunctionData('execute', args);
const relayer = new Relayer(RelayerApiKey, RelayerSecretKey);
const tx = await relayer.sendTransaction({
speed: 'fast',
to: ForwarderAddress,
gasLimit: gas,
data: forwardData,
});
console.log(`Sent meta-tx: ${tx.hash}`);
return tx;
}
// Handler for lambda function
exports.handler = async function(event, context, callback) {
try {
const data = JSON.parse(event.body);
const response = await relay(data);
callback(null, { statusCode: 200, body: JSON.stringify(response) });
} catch (err) {
callback(err);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment