0

I am trying to sign a meta-transaction so I can execute a function from a smart contract with 1 account but pay for gas fees with another.

This is the code I have (this works when using MetaMask in a front-end, but now I want to do it in a node.js application using ethers):

const config = require("../config/config");
const ethers = require('ethers');
const MinimalForwarder = require("../util/abis/MinimalForwarder.json");
const GameEvents = require("../util/abis/GameEvents.json");
const networkRpcURL = config.web3.networkRpcUrl;
const provider = new ethers.providers.JsonRpcProvider(networkRpcURL);
const { signTypedMessage } = require('eth-sig-util');

const EIP712Domain = [
  { name: 'name', type: 'string' },
  { name: 'version', type: 'string' },
  { name: 'chainId', type: 'uint256' },
  { name: 'verifyingContract', type: 'address' }
];

const ForwardRequest = [
  { 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 getMetaTxTypeData = (chainId, verifyingContract) => {
  return {
    types: {
      EIP712Domain,
      ForwardRequest,
    },
    domain: {
      name: 'MinimalForwarder',
      version: '0.0.1',
      chainId,
      verifyingContract,
    },
    primaryType: 'ForwardRequest',
  }
};

const createInstance = (address, abi) => {
  return new ethers.Contract(address, abi, provider);
}

const getSigner = (i) => {

  // Use the mnemonic to create an HDNode instance
  const account = ethers.utils.HDNode.fromMnemonic(config.web3.mnemonic).derivePath("m/44'/60'/0'/0/" + i);
  const signer = new ethers.Wallet(account, provider);

  return signer;

};

const sendMetaTx = async (iterator, userAddress, sessionId, sessionType) => {

  const url = config.web3.webhookUrl;
  if (!url) throw new Error(`Missing relayer url`);

  const forwarder = createInstance(config.web3.minimalForwarder, MinimalForwarder.abi);
  const gameEvents = createInstance(config.web3.gameEvents, GameEvents.abi);
  const signer = await getSigner(iterator);
  const from = await signer.getAddress();
  const data = gameEvents.interface.encodeFunctionData('startGameSession', [userAddress, sessionId, sessionType]);
  const to = gameEvents.address;
  const request = await signMetaTxRequest(signer.provider, forwarder, { to, from, data });

  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(request),
    headers: { 'Content-Type': 'application/json' },
  });

}

const signMetaTxRequest = async (signer, forwarder, input) => {
  const request = await buildRequest(signer, input);
  const toSign = await buildTypedData(forwarder, request);
  const signature = await signTypedData(signer, input.from, toSign);
  return { type: 'forward', forwarderAddress: forwarder.address, signature, request };
}

const buildRequest = async (provider, input) => {
  let nonce = await provider.getTransactionCount(input.from);
  nonce = nonce.toString();
  return { value: 0, gas: 1e6, nonce, ...input };
}

const buildTypedData = async (forwarder, request) => {
  const chainId = await forwarder.provider.getNetwork().then((n) => n.chainId);
  const typeData = getMetaTxTypeData(chainId, forwarder.address);
  return { ...typeData, message: request };
}

const signTypedData = async (signer, from, data) => {
  // If signer is a private key, use it to sign
  if (typeof(signer) === 'string') {
    const privateKey = Buffer.from(signer.replace(/^0x/, ''), 'hex');
    return signTypedMessage(privateKey, { data });
  }

  // Otherwise, send the signTypedData RPC call
  // Note that hardhatvm and metamask require different EIP712 input
  const isHardhat = data.domain.chainId == 31337;
  const [method, argData] = isHardhat
    ? ['eth_signTypedData', data]
    : ['eth_signTypedData_v4', JSON.stringify(data)]
  return await signer.send(method, [from, argData]);
}

I am getting the following error:

{
    "jsonrpc": "2.0",
    "id": 46,
    "error": {
        "code": -32601,
        "message": "The method eth_signTypedData_v4 does not exist/is not available"
    }
}

What is it I am doing wrong?

1 Answer 1

0

The way you sign a message using ethers is with function _signTypedData, like this:

await signer._signTypedData(data.domain, newTypes, data.message);

Keep in mind that you don't need EIP712Domain in your types, as that is already assumed by ethers.

This is a working example of a message signed with this function:

const EIP712Domain = [
  { name: 'name', type: 'string' },
  { name: 'version', type: 'string' },
  { name: 'chainId', type: 'uint256' },
  { name: 'verifyingContract', type: 'address' }
];

const ForwardRequest = [
  { 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 getMetaTxTypeData = (chainId, verifyingContract) => {
  return {
    types: {
      EIP712Domain,
      ForwardRequest,
    },
    domain: {
      name: 'MinimalForwarder',
      version: '0.0.1',
      chainId,
      verifyingContract,
    },
    primaryType: 'ForwardRequest',
  }
};

const createInstance = (address, abi) => {
  return new ethers.Contract(address, abi, provider);
}

const getSigner = (i) => {
  const account = ethers.utils.HDNode.fromMnemonic(config.web3.mnemonic).derivePath("m/44'/60'/0'/0/" + i);
  const signer = new ethers.Wallet(account, provider);
  return signer;
};

const sendMetaTx = async (iterator, userAddress, sessionId, sessionType) => {

  const url = config.web3.webhookUrl;
  if (!url) throw new Error(`Missing relayer url`);

  const forwarder = createInstance(config.web3.minimalForwarder, MinimalForwarder.abi);
  const gameEvents = createInstance(config.web3.gameEvents, GameEvents.abi);
  const signer = await getSigner(iterator);
  const from = await signer.getAddress();
  const data = gameEvents.interface.encodeFunctionData('startGameSession', [userAddress, sessionId, sessionType]);
  const to = gameEvents.address;
  const request = await signMetaTxRequest(signer, forwarder, { to, from, data });

  return await fetch(url, {
    method: 'POST',
    body: JSON.stringify(request),
    headers: { 'Content-Type': 'application/json' },
  });

}

const signMetaTxRequest = async (signer, forwarder, input) => {
  const request = await buildRequest(forwarder, input);
  const toSign = await buildTypedData(forwarder, request);
  const signature = await signTypedData(signer, toSign);
  return { type: 'forward', forwarderAddress: forwarder.address, signature, request };
}

const buildRequest = async (forwarder, input) => {
  const nonce = await forwarder.getNonce(input.from);
  return { value: 0, gas: 1e6, nonce, ...input };
}

const buildTypedData = async (forwarder, request) => {
  const chainId = await forwarder.provider.getNetwork().then((n) => n.chainId);
  const typeData = getMetaTxTypeData(chainId, forwarder.address);
  return { ...typeData, message: request };
}

const signTypedData = async (signer, data) => {
  const newTypes = {
    ForwardRequest: data.types.ForwardRequest
  }
  return await signer._signTypedData(data.domain, newTypes, data.message);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.