Skip to main content

Network Agnostic Transactions

Goal

Execute transactions on Polygon chain, without changing provider on MetaMask (this tutorial caters to metamask's inpage provider, can be modified to execute transactions from any other provider)

Under the hood, user signs on an intent to execute a transaction, which is relayed by a simple relayer to execute it on a contract deployed on Polygon chain.

What is enabling transaction execution?

The client that the user interacts with (web browser, mobile apps, etc) never interacts with the blockchain, instead it interacts with a simple relayer server (or a network of relayers), similar to the way GSN or any meta-transaction solution works ( see: Meta Transactions: An Introduction).

For any action that requires blockchain interaction,

  • Client requests an EIP712 formatted signature from the user
  • The signature is sent to a simple relayer server (should have a simple auth/spam protection if used for production, or biconomy's mexa sdk can be used: https://github.com/bcnmy/mexa-sdk)
  • The relayer interacts with the blockchain to submit user's signature to the contract. A function on the contract called executeMetaTransaction processes the signature and executes the requested transaction (via an internal call).
  • The relayer pays for the gas making the transaction effectively free 🤑

Integrate Network Agnostic Transactions in your dApp


let data = await web3.eth.abi.encodeFunctionCall({
name: 'getNonce',
type: 'function',
inputs: [{
name: "user",
type: "address"
}]
}, [accounts[0]]);

let _nonce = await web3.eth.call ({
to: token["80001"],
data
});

const dataToSign = getTypedData({
name: token["name"],
version: '1',
salt: '0x0000000000000000000000000000000000000000000000000000000000013881',
verifyingContract: token["80001"],
nonce: parseInt(_nonce),
from: accounts[0],
functionSignature: functionSig
});

const msgParams = [accounts[0], JSON.stringify(dataToSign)];

let sig = await eth.request ({
method: 'eth_signTypedData_v3',
params: msgParams
});

  • Once you have a relayer and the contracts setup, what is required is for the client to be able to fetch an EIP712 formatted signature and simply call the API with the required parameters

    ref: https://github.com/angelagilhotra/ETHOnline-Workshop/blob/6b615b8a4ef00553c17729c721572529303c8e1b/2-network-agnostic-transfer/sign.js#L47


    let data = await web3.eth.abi.encodeFunctionCall({
    name: 'getNonce',
    type: 'function',
    inputs: [{
    name: "user",
    type: "address"
    }]
    }, [accounts[0]]);

    let _nonce = await web3.eth.call ({
    to: token["80001"],
    data
    });

    const dataToSign = getTypedData({
    name: token["name"],
    version: '1',
    salt: '0x0000000000000000000000000000000000000000000000000000000000013881',
    verifyingContract: token["80001"],
    nonce: parseInt(_nonce),
    from: accounts[0],
    functionSignature: functionSig
    });
    const msgParams = [accounts[0], JSON.stringify(dataToSign)];

    let sig = await eth.request ({
    method: 'eth_signTypedData_v3',
    params: msgParams
    });

    Calling the API, ref: https://github.com/angelagilhotra/ETHOnline-Workshop/blob/6b615b8a4ef00553c17729c721572529303c8e1b/2-network-agnostic-transfer/sign.js#L110

    const response = await request.post(
    'http://localhost:3000/exec', {
    json: txObj,
    },
    (error, res, body) => {
    if (error) {
    console.error(error)
    return
    }
    document.getElementById(el).innerHTML =
    `response:`+ JSON.stringify(body)
    }
    )

    If using Biconomy, the following should be called:

    const response = await request.post(
    'https://api.biconomy.io/api/v2/meta-tx/native', {
    json: txObj,
    },
    (error, res, body) => {
    if (error) {
    console.error(error)
    return
    }
    document.getElementById(el).innerHTML =
    `response:`+ JSON.stringify(body)
    }
    )

    where the txObj should look something like:

    {
    "to": "0x2395d740789d8C27C139C62d1aF786c77c9a1Ef1",
    "apiId": <API ID COPIED FROM THE API PAGE>,
    "params": [
    "0x2173fdd5427c99357ba0dd5e34c964b08079a695",
    "0x2e1a7d4d000000000000000000000000000000000000000000000000000000000000000a",
    "0x42da8b5ac3f1c5c35c3eb38d639a780ec973744f11ff75b81bbf916300411602",
    "0x32bf1451a3e999b57822bc1a9b8bfdfeb0da59aa330c247e4befafa997a11de9",
    "27"
    ],
    "from": "0x2173fdd5427c99357ba0dd5e34c964b08079a695"
    }
  • If you use the custom API it executes the executeMetaTransaction function on the contract:

    (ref: https://github.com/angelagilhotra/ETHOnline-Workshop/blob/6b615b8a4ef00553c17729c721572529303c8e1b/2-network-agnostic-transfer/server/index.js#L40)

    try {
    let tx = await contract.methods.executeMetaTransaction(
    txDetails.from, txDetails.fnSig, r, s, v
    ).send ({
    from: user,
    gas: 800000
    })
    req.txHash = tx.transactionHash
    } catch (err) {
    console.log (err)
    next(err)
    }

    is using biconomy, the client side call looks like:

    // client/src/App.js
    import React from "react";
    import Biconomy from "@biconomy/mexa";

    const getWeb3 = new Web3(biconomy);
    biconomy
    .onEvent(biconomy.READY, () => {
    // Initialize your dapp here like getting user accounts etc
    console.log("Mexa is Ready");
    })
    .onEvent(biconomy.ERROR, (error, message) => {
    // Handle error while initializing mexa
    console.error(error);
    });

    /**
    * use the getWeb3 object to define a contract and calling the function directly
    */