Building a Meta Transaction Relayer
Relayers serve to delegate gas fees to a web service, allowing users to transact on NEAR without the need to acquire the token themselves while still retaining the security of signing their own transactions. This guide will lead you through the components necessary to construct a relayer capable of handling meta transactions.
If you're already acquainted with the technology and you just want to run your own Relayer, you can fast track to a complete Rust Relayer server open-source implementation.
How it works
A basic relayer consists of a web server housing a funded NEAR account. This account receives an encoded signed transaction, which can subsequently be decoded into a SignedDelegate
format and transmitted on-chain.
The client can then generate a SignedDelegateAction
(a signed message that hasn't yet been sent), encode it, and transmit it to this server, where it will be relayed onto the blockchain.
Relayer (server)
- near-api-js
- @near-relay/server
Here's a simple express endpoint that deserializes the body, instantiates the relayer account and then sends the transaction.
Loading...
You can easily get the account object used to send the transactions from its private key using this snippet
Loading...
The code in the example only works from the following versions onwards
"near-api-js": "3.0.4"
"@near-js/transactions": "1.1.2",
"@near-js/accounts": "1.0.4"
@near-relay
simplifies meta transactions making it easier to get started for a beginner.
To start, call the relay method inside an endpoint to automatically deserialize the transaction and send it with the account defined in the environment variables.
Loading...
If you're interested in relaying account creation as well, it's quite straightforward. Simply create another endpoint and directly call the createAccount method with the accountId and publicKey. These parameters are automatically included in the body when using the corresponding client library.
Loading...
Client
- near-api-js
- @near-relay/client
In this method we are creating an arbitrary smart contract call, instantiating an account and using it to sign but not send the transaction. We can then serialize it and send it to the relayer where it will be delegated via the previously created endpoint.
Loading...
As mentioned in the above note in order to be able to relay on the client side it's necessary to have access to signing transactions directly on the client. Luckily leveraging the near biometric library it's possible to do so in a non custodial way.
By calling this method and passing in the URL for the account creation endpoint (mentioned in the server section) as well as the accountId
everything is handled under the hood to successfully create an account.
Loading...
On the client side, you just need to create an Action
and pass it into the relayTransaction
method along with the URL of the relayer endpoint discussed in the server section and the id of the receiverId
.
Loading...
Relaying with wallets
At the moment, wallet selector standard doesn't support signing transactions without immediately sending them. This functionality is essential for routing transactions to a relayer. Therefore, to smoothly integrate relaying on the client side, it's necessary to be able to sign transactions without relying on wallets. Progress is being made to make this possible in the future.
High volume parallel processing
When running a relayer that handles a large number of transactions, you will quickly run into a nonce
collision problem. At the protocol level, transactions have a unique number that identifies them (nonce) that helps to mitigate reply attacks. Each key on an account has its own nonce, and the nonce is expected to increase with each signature the key creates.
When multiple transactions are sent from the same access key simultaneously, their nonces might collide. Imagine the relayer creates 3 transactions Tx1
, Tx2
, Tx3
and send them in very short distance from each other, and lets assume that Tx3
has the largest nonce. If Tx3
ends up being processed before Tx1
(because of network delays, or a node picks Tx3
first), then Tx3
will execute, but Tx1
and Tx2
will fail, because they have smaller nonce!
One way to mitigate this is to sign each transaction with a different key. Adding multiple full access keys to the NEAR account used for relaying (up to 20 keys can make a significant difference) will help.
Adding keys
const keyPair = nearAPI.KeyPair.fromRandom("ed25519");
const receipt = await account.addKey(keyPair.getPublicKey().toString())
After saving these keys, its possible to rotate the private keys randomly when instantiating accounts before relaying ensuring you won't create a nonce collision.
Gating the relayer
In most production applications it's expected that you want to be able to gate the relayer to only be used in certain cases.
This can be easily accomplished by specifying constraints inside the SignedDelegate.delegateAction
object.
export declare class DelegateAction extends Assignable {
senderId: string;
receiverId: string;
actions: Array<Action>;
nonce: BN;
maxBlockHeight: BN;
publicKey: PublicKey;
}