Advanced GSN: GSNRecipientSignature.sol

Background:

(Note: this tutorial assumes relative familiarity with the Gas Stations Network and OpenZeppelin SDK)

Introduction:

Using GSN allows developers to build (d)apps that don’t require end users to have ETH or even have their own wallet. It does this by allowing for a GSN enabled smart contract to pay for its users transactions itself. The question then of course is: how does a smart contract determine who is allowed to have their transactions subsidized? If there is no criteria, it would be trivial for a user in bad faith to exploit a (d)app and obtain unlimited free transactions.

OpenZeppelin provides two sample strategy contracts which can be used to help (d)app developers decide who their recipient contacts are willing to pay for. The focus of this tutorial is GSNRecipientSignature.sol, which provides an enhanced acceptRelayedCall() function which requires that a trustedKey sign the transaction request parameters in order for the relay request to be accepted. Before starting this tutorial, I recommend reading more about strategies and their purpose as well as how to developers can create meta-transaction powered (d)apps using the GSN.

What we are building:

This will be a multi-part tutorial explaining the process to:

  1. Incorporate the GSNRecipientSignature.sol contract into our GSN enabled smart contract.
  2. Build an Express server as a backend that signs transactions in a format accepted by the GSNRecipientSignature.sol contract.
  3. Modify our front end application so that our Express server is required to sign messages prior to the GSN accepting our users transactions.

To try and keep the length of this tutorial to a minimum, we will be using the OpenZeppelin GSN Starter Kit as a basis for starting our project. Starter Kits are a fantastic way to get up and running quickly with a complete (d)app in a standardized and convenient way. This tutorial will assume the reader has completed the GSN Starter Kit tutorial and has the application running.

Part 1: GSNRecipientSignature.sol

To use the GSNRecipientSignature.sol we need only to inherit it into our contract. If we take a look at the GSNRecipientSignature.sol code, we should take a close look at the acceptRelayedCall() which is essentially “where the magic happens” as this function determines if our contract will accept or deny a relayed call (and pay the resulting cost).

function acceptRelayedCall(
    address relay,
    address from,
    bytes calldata encodedFunction,
    uint256 transactionFee,
    uint256 gasPrice,
    uint256 gasLimit,
    uint256 nonce,
    bytes calldata approvalData,
    uint256
)
external
view
returns(uint256, bytes memory)
{
    bytes memory blob = abi.encodePacked(
        relay,
        from,
        encodedFunction,
        transactionFee,
        gasPrice,
        gasLimit,
        nonce, // Prevents replays on RelayHub
        getHubAddr(), // Prevents replays in multiple RelayHubs
        address(this) // Prevents replays in multiple recipients
    );
    if (keccak256(blob).toEthSignedMessageHash().recover(approvalData) == _getTrustedSigner()) {
        return _approveRelayedCall();
    } else {
        return _rejectRelayedCall(uint256(GSNRecipientSignatureErrorCodes.INVALID_SIGNER));
    }
}

Here the function takes in a number of parameters including:

bytes calldata approvalData

This approvalData is a signed version of all of these parameters put together, signed by what is call the trustedSigner. In the body of the function the parameters are abi.encodePacked() and then hashed together. The result is then used to recover the signer of the approvalData which is compared with the result of the call _getTrustedSigner(). If the address that _getTrustedSigner() returns equals the recovered signature from the encoded and hashed parameters, then the contract knows our trustedSigner has signed off on this transaction and returns _approveRelayedCall() likewise, if the signatures do not match, the function returns _rejectRelayedCall with the failure of the signatures to match as the parameter.

Getting Started:

Be sure you have working the OpenZeppelin GSN Starter Kit, we will start from it as a base. We have a tutorial on the OpenZeppelin forum on how to get started with the GSN Starter Kit.

Add the GSNSignatureRecipient.sol

Once you have completed the tutorial you should see this screen where you have a deployed counter instance that you can interact with by clicking the Increase or Decrease Counter buttons.

Here we can start making some modifications. First, the tutorial is running in the GSN Developer mode, you can find this around line 33 of App.js in the /client/src folder.

The dev:true enables a special developer mode for OpenZeppelin-network that allows you to continue developing and testing a GSN powered experience without needing to run a full relayer. It’s great for testing and front end development, but for our project, we are going to need to run a full GSN relayer as well as deploy a relayHub to our development blockchain Ganache.

If you have completed the tutorial we will need to start fresh. Restart your ganache so we have a fresh blockchain before running a Relayer.

Run a Relayer

At OpenZeppelin we make some fantastic tools to support the GSN Network, in this case we’re going to use the OpenZeppelin-gsn-helpers which allow us to do many different typical development tasks such as deploy a relayHub , run a Relayer, register a Relayer and more.

First install the gsn-helpers:

npm install @openzeppelin/gsn-helpers

Now we need to deploy a live relayHub , run a Relayer, and then register our Relayer in the relayHub. While we can do each of these steps individually with the gsn-helpers running the command run-relayer will take care of all these steps at once. In a new terminal window in your project folder, (with ganache running, after you restarted it, in another terminal) type:

npx oz-gsn run-relayer --quiet

You should see output similar to the following:

Starting relayer
/Users/dennison/Library/Caches/gsn-nodejs/gsn-relay-v0.1.4
-EthereumNodeUrl [http://localhost:8545](http://localhost:8545/)
-RelayHubAddress 0xd216153c06e857cd7f72665e0af1d7d82172f494
-Port 8090
-Url [http://localhost:8090](http://localhost:8090/)
-GasPricePercent 0
-Workdir /var/folders/pf/8knbxmfd6n3_cn5glssbp5580000gn/T/tmp-17513m3fru4uoIQW8
-DevMode
Funding GSN relay at [http://localhost:8090](http://localhost:8090/)
Will wait up to 30s for the relay to be ready
Relay is funded and ready!

Great. Now we need to redeploy the GSN Starter Kit contract counter.sol remembering to call initialize() at the end of the process.

oz create

And follow the cli prompts:

If you still have your client server running from the tutorial, now is also a good time to restart it. Once you do, you should see this screen again which informs you that the recipient has no funds.

This time when we fund the recipient, we will be funding our smart contract counter.sol on the relayHub we deployed manually with the gsn-helpers. Go ahead and fund your recipient.

npx oz-gsn fund-recipient --recipient <<Your contract address here>>

Once you’ve done this, refresh the tutorial webpage and ensure the buttons work again. This will be our demonstration setup.

Part 2: Alter the counter.sol contract

Now that we’ve got our demo-setup running we can start with the next steps.

Update Contract

On our counter.sol contract we need to alter the acceptRelayedCall() function to use the GSNSignatureRecipient provided acceptRelayedCall(). This will enable our counter.sol contract to check if transactions have been signed by our ‘yet to be built’ express server.

At the top of our contract we need to import the GSNSignatureRecipient from the @openzeppelin/contracts-ethereum-package/ and inherit from it:

pragma solidity ^ 0.5.0;

import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipientSignature.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";

contract Counter is Initializable, GSNRecipientSignature { 
    function initialize(uint num, address trustedSigner) 
    public initializer { 
        GSNRecipientSignature.initialize(trustedSigner); 
        _owner = _msgSender(); 
        _owner = msg.sender; 
        count = num; 
    }
    
    // omitted for brevity

We can also now comment out (or delete) the acceptRelayedCall() listed in the Counter.sol code because the acceptRelayedCall() function we want is inherited from GSNRecipientSignature.sol , but we also need to be sure we properly initialize our new GSNRecipientSignature.sol with a trustedSigner which will be the address our Express server uses to sign off on transactions.

contract Counter is Initializable, GSNRecipientSignature {
  //it keeps a count to demonstrate stage changes
  uint private count;
  address private _owner;

  function initialize(uint num, address trustedSigner) public initializer {
    GSNRecipientSignature.initialize(trustedSigner);
    _owner = _msgSender();
    count = num;
  }

  // // accept all requests
  // function acceptRelayedCall(
  //   address,
  //   address,
  //   bytes calldata,
  //   uint256,
  //   uint256,
  //   uint256,
  //   uint256,
  //   bytes calldata,
  //   uint256
  //   ) external view returns (uint256, bytes memory) {
  //   return _approveRelayedCall();
  // }

Part 3: Build our Express Server

Now that we have our updated Counter.sol contract ready to relay only signed transactions, we need to get to work on building our Express Server to sign them!

First, install Express

npm install express --save

Create a new folder in our project called signServer

mkdir signServer && cd signServer

The goal of our server will be to create an API that accepts a list of parameters, signs them, and then returns the signature back to the front end. When users interacting with a (d)app want to make a transaction, they will first need to send all the parameters to our Server for signing, and then submit the signed parameters to a Relayer.

While there are quite a number of steps for this, we will get to all of them in good time. Lets start by creating our Express Server:

create a new index.js file and add the following code:

require("dotenv").config();
const port = process.env.PORT || 3000;
const express = require("express");
const asyncHandler = require("express-async-handler");
const app = express();
const { signMessage } = require("./utils/signUtils.js");

app.use(express.json());

app.post("/checkSig", asyncHandler(async (req, res, next) => {
    res.setHeader("Access-Control-Allow-Origin", "localhost:3001");
    const obj = req.body;
    let result;
    try {
        result = await signMessage(obj);
    } catch (error) {
        console.log(error);
    } if (result) {
        return res.status(200).json(result);
    } else {
        return res.status(400).send();
    }
}));

var server = app.listen(3001, function () {
    var port = server.address().port;
    console.log("Example app listening at port %s", port);
});

module.exports = server;

Lets install the dependencies for this file:

npm install dotenv express-async-handler

So lets walk through the code:

require("dotenv").config();
const port = process.env.PORT || 3000;
const express = require("express");
const asyncHandler = require("express-async-handler");
const app = express();
const { signMessage } = require("./utils/signUtils.js");

Here we are mostly just requiring in our dependencies. If you noticed the line const { signMessage } = require("./utils/signUtils.js"); it’s because to keep things clean, the signing code will be separate from our Server code. We will need to make this.

The next section we setup Express to natively handle JSON with app.us(express.json(). Then app.post("/checkSign"... handles our post route /checkSig. This is the route we are going set for our API. When a user hits our /checkSig route, we create an object from the request object which contains our parameters sent from the front end. We then pass it in our yet-to-be-create signMessage() function which will returned a signed result. If the result is valid, we append a status 200 to our res object and send it back with our result appended as JSON. If we don’t get a valid signature back from our function, we append a status 400 to our res object and send that back instead.

app.use(express.json());

app.post("/checkSig",asyncHandler(async (req, res, next) => {
    res.setHeader("Access-Control-Allow-Origin", "localhost:3001");
    const obj = req.body;
    let result;
    
    try {
        result = await signMessage(obj);
    } catch (error) {
        console.log(error);
    }
    
    if (result) {
        return res.status(200).json(result);
    } else {
        return res.status(400).send();
    }
}));

Finally we start our server on port 3001 and export our server. This export we don’t need to worry about here, we need it when running tests on our server.

var server = app.listen(3001, function() {
    var port = server.address().port;
    console.log("Example app listening at port %s", port);
});

module.exports = server;

Great, so now that we have our server, let’s build our signing utility.

Create a new folder in your signServer folder:

mkdir utils && cd utils

Create a file inside utils called signUtils.js and add the following code:

const Web3 = require("web3");

const { utils: { toBN, soliditySha3 } } = require("web3");
const web3 = new Web3("ws://localhost:8545");

function fixSignature(signature) {
    let v = parseInt(signature.slice(130, 132), 16);

    if (v < 27) {
        v += 27;
    }

    const vHex = v.toString(16);

    return signature.slice(0, 130) + vHex;
}

const signMessage = async data => {
    let accounts = await web3.eth.getAccounts();

    return fixSignature(await web3.eth.sign(soliditySha3(
        data.relayerAddress,
        data.from,
        data.encodedFunctionCall,
        toBN(data.txFee),
        toBN(data.gasPrice),
        toBN(data.gas),
        toBN(data.nonce),
        data.relayHubAddress,
        data.to),
        accounts[0]));
};

module.exports = { signMessage };

First up, you’ll notice at the top we are requiring in web3 so lets go ahead and install it.

npm install web3

One we require in web3 we’re then creating an instance of it, connected to ganache (or in production- your ethereum node). We are also pealing out the functions toBN , and soliditySha3 from the web3.utils as a convenience.

const Web3 = require("web3");
const { utils: { toBN, soliditySha3 } } = require("web3");
const web3 = new Web3("ws://localhost:8545");

The next chunk of code basically fixes the signatures we get back when using Ganache as our ethereum node so they are not replay-able. To learn more why, read here.

function fixSignature(signature) {
    let v = parseInt(signature.slice(130, 132), 16);
    if (v < 27) {v += 27;}const vHex = v.toString(16);
    return signature.slice(0, 130) + vHex;
}

The last part is where the magic happens. Here we are assuming that our trusted signer is the first account in our list of accounts from our Ganache instance. This is for convenience, in production you will want to make some decisions about who the signer is, and rather than instantiate your web3 provider via a connection to ganache, you will want to connect to a real ethereum node with an unlocked account.

Our function signMessage() first hashes all of our parameters together at soliditySha3() , the result of which is signed by web3.eth.sign(). The resulting signature is then returned by signMessage(). We then export our signMessage() function so that we can require it in for our Express server.

const signMessage = async data => {
    let accounts = await web3.eth.getAccounts();
    return fixSignature(await web3.eth.sign(soliditySha3(
        data.relayerAddress,
        data.from,
        data.encodedFunctionCall,
        toBN(data.txFee),
        toBN(data.gasPrice),
        toBN(data.gas),
        toBN(data.nonce),
        data.relayHubAddress,
        data.to),
        accounts[0])
    );
};

module.exports = { signMessage };

Great, so we have all the pieces we need, but at this point it’s hard to know if what we have built is actually working without creating the full front end. Before we do that, lets be responsible programmers and create some tests:

Part 4: Test our Express Server

Create a new folder test in the project folder if there isn’t already one.

mkdir test && cd test

Here create a file called, unimaginatively, expressServer.test.js, we will put our testing code here.

If you’re not so familiar with testing, then the following might seem like a lot of information to take in all at once. Basically, to run unit tests against an Express server we need to create a new server for each test, run our tests, and then shut down our server before creating a new one again for our next test. This is easier said than down, and in writing this tutorial I relied heavily on this tutorial by Gleb Bahmutov for reference. I highly recommend it.

Here is the code for our tests, copy and paste it into your code editor for much better formatting than I can get here on Medium.

const request = require("supertest");
const Web3 = require("web3");
const chai = require("chai");
const { utils: { toBN } } = require("web3");
const web3 = new Web3("ws://localhost:8545");
const { signMessage } = require("../signServer/utils/signUtils.js");
const expect = chai.expect;

const mochaAsync = fn => {
    return done => {
        fn.call().then(done, err => {
            done(err);
        });
    };
};

describe("loading express", async () => {
    let server;
    let accounts;
    let relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
    let relayerAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
    let to = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
    let from;
    let encodedFunctionCall = "0xe0b6fcfc";
    let txFee = toBN(10);
    let gasPrice = toBN(10);
    let gas = toBN(10);
    let nonce = toBN(10);

    beforeEach(() => {
        delete require.cache[require.resolve("../signServer/index.js")];
        server = require("../signServer/index.js");
    });

    afterEach(done => {
        server.close(done);
    });

    it("Signs a message", mochaAsync(async () => {
        accounts = await web3.eth.getAccounts();
        from = accounts[1];
        //SIGN MESSAGE LOCALLY TO COMPARE SIGNATURES
        let testSig = await signMessage({ relayerAddress, from, encodedFunctionCall, txFee, gasPrice, gas, nonce, relayHubAddress, to });
        let res = await request(server).post("/checkSig").send({ relayerAddress, from, encodedFunctionCall, txFee, gasPrice, gas, nonce, relayHubAddress, to });

        expect(res.status).to.equal(200); expect(res.body).to.equal(testSig);
    }));
});

So lets take this bit by bit:

const request = require("supertest"); 
const Web3 = require("web3"); 
const chai = require("chai"); 
const { utils: { toBN } } = require("web3"); 
const web3 = new Web3("ws://localhost:8545"); 
const { signMessage } = require("../signServer/utils/signUtils.js"); 
const expect = chai.expect;

In our first chunk of code we are requiring in our dependencies, in this case: web3 , supertest , and chai. We should already have web3 installed from earlier, so lets get the others plus mocha a testing framework.

npm install supertest chai mocha --dev

Our first dependency, supertest will make testing against our express server easier, because it helps us to make calls to our express server over a HTTP connection while chai is a common BDD / TDD assertion library that is often paired with the testing frame work mocha.

Additionally in this chunk of code we create a new web3 instance and require in the signMessage() function we created in our /utils folder.

const mochaAsync = fn => {
    return done => {
        fn.call().then(done, err => {
            done(err);
        });
    };
};

This mochaAsync() function allows us to do async actions inside our tests while still properly calling the done() function that Mocha needs to properly complete each test.

describe("loading express", async () => {
    let server;
    let accounts;
    let relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
    let relayerAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
    let to = "0xD216153c06E857cD7f72665E0aF1d7D82172F494";
    let from;
    let encodedFunctionCall = "0xe0b6fcfc";
    let txFee = toBN(10);
    let gasPrice = toBN(10);
    let gas = toBN(10);
    let nonce = toBN(10);

Here at the top of our describe() block we are just setting some variables. With the exception of server , accounts , and from , the actual value is not so important as we are only using them to craft a message. We are going to be writing only a single test- to check that the express server sends us back a signed message and that this message matches our own signature, so the contents of the message, for now, are not so important.

beforeEach(() => {
    delete require.cache[require.resolve("../signServer/index.js")];
    server = require("../signServer/index.js");
});

afterEach(done => {
    server.close(done);
});

In this chunk of code, we are doing something very interesting. In our beforeEach() we are purging a reference to our index.js file which is our express server. Otherwise, we won’t be able to destroy and recreate our express server for each test: Node will keep a cached reference to our server instead of creating a new one each time.

it("Signs a message", mochaAsync(async () => {
    accounts = await web3.eth.getAccounts();
    from = accounts[1];
    //SIGN MESSAGE LOCALLY TO COMPARE SIGNATURES
    let testSig = await signMessage({
        relayerAddress, from, encodedFunctionCall, txFee, gasPrice, gas, nonce, relayHubAddress, to
    });
    let res = await request(server).post("/checkSig").send({
        relayerAddress, from, encodedFunctionCall, txFee, gasPrice, gas, nonce, relayHubAddress, to
    });

    expect(res.status).to.equal(200);
    expect(res.body).to.equal(testSig);
}));
});

Finally, we get to the test itself. As mentioned before, we are checking only that our express server actually signs a message and returns the signature. Ideally we would want to check that this signature is also valid in the context of the GSN, but this tutorial is getting too long as it is. Instead you should have a look at the tests used in the openzeppelin-contracts package itself as an idea how to take your testing further.

In the test we first construct our own signature using our imported signMessage() function and assign it’s value to testSig. We then make a request to our server, posting to the /checkSig route, the same parameters we just used to sign a message for testSig. We check that the status returned equals 200 (this means, everything okay) and that the res.body equals our testSig.

Finally we will need to be sure that we have mocha setup in our package.json scripts. Open the package.json file at the top level of your project and check for this line:

"test": "echo \"Error: no test specified\" && exit 1",

and change it to:

"test": "mocha",

If all has gone well, we can test our test! From the top level folder of our project:

npm run test test/expressServer.test.js

And we should see:

Great!

Part 5: Build our Front End

Building a Front end is no small task, luckily for us we are going to reuse the front end that is included with the GSN Starter kit to try and minimize the amount of work we need to do to get our Signing Server wired up to our React App.

Before we start writing code however, let’s talk a little bit more about how GSN works using OpenZeppelin tools on the front end.

If you’ve looked through the front end code for GSN Starter Kit you will see that we are using the OpenZeppelin toolset: openzeppelin-network.js which is used to setup our web3 provider. Deeper in the stack openzeppelin-network.js uses openzeppelin/gsn-provider to allow us to instantiate a web3 network connection with GSN support. I recommend checking out both repos on GitHub for a deeper understanding of how they work.

Let’s take a look again at our GSNRecipientSignature.sol file to understand a bit better about how the signature from our express server is used in the smart contract.

You can see that the approvalData ( approvalData is our signature that our express server is creating for us) on line 9 is passed in when we call the acceptRelayedCall() function. However we the user , never call this function, it is called as part of the transaction flow initiated by Relayers. The question then you might be wondering then is, “how do the Relayers get it?” if we look at the front end code (in this case, /client/src/components/Counter/index.js line 74, the component that increments our counter) you can see that our contract call from the front end looks the same like a regular contract call.

Our contract call on line 6 looks normal- how do we send approval data?

What needs to be done is that we need to inject our approvalData into the tx flow so that when our GSN enabled call is intercepted by openzeppelin-gsn-provider and sent to a Relayer, the approvalData is included.

Fortunately, openzeppelin-gsn-provider provides the option to inject an approvalFunction which then returns the injected approval data into the tx flow for us. In the documentation the approvalFunction() works to sign the data locally, in our case, we will build an approvalFunction() that calls out to our express server for the signed data.

Injecting an approvalFunction can be done at the time we setup our web3 connection like this:

const gsnProvider = new GSNProvider("http://localhost:8545", { approveFunction });

Or it can be added to each function call that you want to have parameters signed like so:

const receipt = await instance.methods.increaseCounter(number).send({ 
    from: accounts[0], approveFunction: approveFunction , 
});

So let’s build our approveFunction().

First install axios which we will need to make our request to our express server.

npm install axios

And then include it somewhere at the top of our App.js file in the /client/src directory with the imports.

import axios from 'axios';

Next is our approveFunction()

const approveFunction = async ({
    from, to, encodedFunctionCall, txFee, gasPrice, gas, nonce, relayerAddress, relayHubAddress,
}) => {
    let response;
    try {
        response = await axios.post('http://localhost:3001/checkSig', {
            from, to, encodedFunctionCall, txFee, gasPrice, gas, nonce, relayerAddress, relayHubAddress,
        });
    } catch (error) {
        console.error(error);
    }

    console.log(response.data);
    return response.data;
};

Here our approveFunction takes in our parameters, then it uses axios to post JSON data with these parameters to our express server, which in turn signs them and returns them on our response object as data.

Add this approveFunction above where we declare context and add it to the gsn object after signKey.

Great!

Part 6: Tie it all Together (and make it work)

So now we should have everything together and theoretically working. Lets try it out!

First to give us a clean slate, lets kill our relay-server, our ganache, express server, our front end, etc… to start fresh.

Starting over, lets first start ganache in it’s own terminal window:

ganache-cli --deterministic

Then deploy our relay-hub and start our relayer. In another terminal window (inside our project)

npx oz-gsn run-relayer --quiet

In another terminal window, inside our /client folder, lets run our react server.

npm run start

In yet ANOTHER terminal window from your project folder run the express server.

node signServer/index.js

And in our FINAL terminal window, we deploy our counter.col contract and initialize it with the address of our trustedSigner. If you remember, our express server is signing messages using the accounts[0] , the first account available from our ethereum node, which in this case, is ganache. If you don’t have that handy, it’s easy to use the OpenZeppelin CLI to get this.

oz accounts

Select development as the network and copy the first account at the top of the list.

Now we need to deploy our contract:

oz create

Follow the prompts and at the point were it asks you if you want to call a function, select “yes” and choose the initialize() function that takes both a number and an address for trustedSigner , enter a number and the address you obtained from the top of the list from oz accounts.

Moving to your web browser visit the tutorial website, localhost://3000 it should be loaded, recognize the contract has been deployed and be prompting you that the recipient has no funds in the relayHub.

(you’re address might be different so copy/paste from your instance)

Fund the recipient, refresh your browser and test your counter.

Tada! It doesn’t work!

There is one more step here necessary in development. Your browsers CORS policy will most likely be blocking access to the Express server and you’ll see a message like this in your console:

To get around this, I use a Chrome extension that allows me to turn CORS off. The internet has plenty of opinions about what the best way is to deal with CORS when doing development, but for me this is the simplest.

So, thank you for coming with me on this voyage of a tutorial. It’s been long, but hopefully worth it. Lets recap what we have learned:

  1. Using the GSN Starter Kit, we have modified a stock GSN enabled application to require external signatures before accepting transactions.
  2. We have built an API using an Express server to sign transactions.
  3. We have created an approvalFunction which calls out to our API to get signed approvals which then get forwarded to our GSN Relayers for our transactions.

While this is just the basics, by building on our express server we can build complicated and secure methods for tracking our users, understanding who they are, and selectively allowing them to interact with (d)apps without needing to have ETH or even a wallet.

6 Likes

2 posts were split to a new topic: Pay the transaction gas fee using any ERC20 (Gas Station Network)?