Skip to main content

Relayed Transactions

On this page, you will find comprehensive information on all aspects of relayed transactions.

Introduction

Relayed transactions (or meta-transactions) are transactions with the fee paid by a so-called relayer. In other words, if a relayer is willing to pay for an interaction, it is not mandatory that the address interacting with a Smart Contract has any EGLD for fees.

More details and specifications can be found on MultiversX Specs.

Types of relayed transactions

Currently, there are 3 versions of relayed transactions: v1, v2 and v3. In the end, they all have the same effect.

Relayed v2 was meant to bring optimisations in terms of gas usage. But v3 reduces the costs even further, making it our recommendation.

Once all applications will completely transition to relayed v3 model, v1 and v2 will be removed.

Relayed transactions version 1

A relayed transaction version 1 relies on having the inner transaction JSON serialized and given as an argument to the relayedTx protocol function.

It would look like:

RelayedV1Transaction {
Sender: <Relayer address>
Receiver: <Address that signed the inner transaction>
Value: 0
GasLimit: <move_balance_cost> + length(Data) * <gas_per_data_byte> + <inner transaction gas limit>
Data: "relayedTx" +
"@" + <JSON serialized inner transaction in hexadecimal encoding>
}

The inner transaction can have a format like this:

RelayedV1InnerTransaction {
Sender: <Receiver of the relayed transaction>
Receiver: <Smart Contract address>
Value: 0
GasLimit: <to be determined for each case>
Data: "functionName" +
"@" + <argument in hexadecimal encoding>
...
}

However, unlike regular transactions' JSON serialization, the inner transaction that has to be signed has a different structure:

type Transaction struct {
Nonce uint64
Value *math_big.Int
ReceiverAddress []byte
SenderAddress []byte
GasPrice uint64
GasLimit uint64
Data []byte
ChainID []byte
Version uint32
Signature []byte
Options uint32
}

Notice that there are some differences as compared to the regular frontend transaction structure, such:

  • SenderAddress and ReceiverAddress have to be byte arrays instead of bech32 string addresses
  • Value has to be a big integer, instead of a string
  • ChainID has to be a byte array instead of a string
  • Signature has to be a byte array instead of the hex version of it

Preparing relayed v1 transaction using mx-sdk-js-core

mx-sdk-js-core has built-in support for relayed transactions version 1, by using a builder which allows one to prepare such a transaction.

Resources:

Example

Here's an example of a relayed v1 transaction. Its intent is:

erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx will call the function add of the contract erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd, while erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th will pay the computation fee

{
"nonce": 2627,
"value": "0",
"receiver": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"sender": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
"gasPrice": 1000000000,
"gasLimit": 61040000,
"data": "cmVsYXllZFR4QDdiMjI2ZTZmNmU2MzY1MjIzYTMxMzkzODJjMjI3MzY1NmU2NDY1NzIyMjNhMjI2NzQ1NmU1NzRmNjU1NzZkNmQ0MTMwNjMzMDZhNmI3MTc2NGQzNTQyNDE3MDdhNjE2NDRiNDY1NzRlNTM0ZjY5NDE3NjQzNTc1MTYzNzc2ZDQ3NTA2NzNkMjIyYzIyNzI2NTYzNjU2OTc2NjU3MjIyM2EyMjQxNDE0MTQxNDE0MTQxNDE0MTQxNDE0NjQxNDIzNDc1NTk1MjcxNjMzNDY1NDQ0OTM0Nzk2NzM4N2E0ODc3NjI0NDMwNWE2ODZiNTg0MjM1NzAzMTc3M2QyMjJjMjI3NjYxNmM3NTY1MjIzYTMwMmMyMjY3NjE3MzUwNzI2OTYzNjUyMjNhMzEzMDMwMzAzMDMwMzAzMDMwMzAyYzIyNjc2MTczNGM2OTZkNjk3NDIyM2EzNjMwMzAzMDMwMzAzMDMwMmMyMjY0NjE3NDYxMjIzYTIyNTk1NzUyNmIyMjJjMjI3MzY5Njc2ZTYxNzQ3NTcyNjUyMjNhMjI0ZTMwNzIzMTcwNmYzNzZiNzY0ZjU0NGI0OTQ3NDcyZjc1NmI2NzcyMzg1YTYyNTc2NDU4NjczMTY2NTEzMDc2NmQ3NTYyMzU3OTM0NGY3MzUzNDE3MTM0N2EyZjU5Mzc2YzQ2NTI3OTU3NzM2NzM0NGUyYjZmNGE2OTQ5NDk1Nzc3N2E2YjZkNmM2YTQ5NDE3MjZkNjkzMTY5NTg0ODU0NzkzNDRiNjc0MTQxM2QzZDIyMmMyMjYzNjg2MTY5NmU0OTQ0MjIzYTIyNTY0MTNkM2QyMjJjMjI3NjY1NzI3MzY5NmY2ZTIyM2EzMTdk",
"chainID": "T",
"signature": "44889e788581c8913a00e03f711f9ed3522119030a48fe6c1b3434656670b4b93867213f7a7b5453eafe0884f7447361e1154d26c6e7b2cfa40510159e0e1008",
"version": 1
}

The data field (after decoding from base64 to string) is converted to:

relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414146414234755952716334654449347967387a48776244305a686b5842357031773d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225957526b222c227369676e6174757265223a224e307231706f376b764f544b4947472f756b6772385a625764586731665130766d75623579344f73534171347a2f59376c465279577367344e2b6f4a69494957777a6b6d6c6a4941726d69316958485479344b6741413d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a317d

Furthermore, the inner transaction can be easily decoded (hex string to string), resulting in:

{
"nonce": 198,
"sender": "gEnWOeWmmA0c0jkqvM5BApzadKFWNSOiAvCWQcwmGPg=",
"receiver": "AAAAAAAAAAAFAB4uYRqc4eDI4yg8zHwbD0ZhkXB5p1w=",
"value": 0,
"gasPrice": 1000000000,
"gasLimit": 60000000,
"data": "YWRk",
"signature": "N0r1po7kvOTKIGG/ukgr8ZbWdXg1fQ0vmub5y4OsSAq4z/Y7lFRyWsg4N+oJiIIWwzkmljIArmi1iXHTy4KgAA==",
"chainID": "VA==",
"version": 1
}

Decoding the base64 fields, we'll get:

  • sender: erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx
  • receiver: erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd
  • data: add
  • chain ID: T
  • signature: 374af5a68ee4bce4ca2061bfba482bf196d67578357d0d2f9ae6f9cb83ac480ab8cff63b9454725ac83837ea09888216c33926963200ae68b58971d3cb82a000

Regarding the relayed transaction's gas limit, let's check the math.

gasLimit = <move_balance_cost> + length(Data) * <gas_per_data_byte> + <inner transaction gas limit>
gasLimit = 50_000 + 660 * 1500 + 60_000_000
gasLimit = 61040000 // just like the gas limit set in the relayed transaction

Relayed transactions version 2

In contrast with version 1, relayed transactions version 2 have only certain fields of the inner transaction included in the data field, making the payload smaller, therefore the tx fee smaller. It also eliminates the need of calculating the matching gas limit values between the relayed and inner transactions.

It would look like:

RelayedV2Transaction {
Sender: <Relayer address>
Receiver: <Address that signed the inner transaction>
Value: 0
GasLimit: <move_balance_cost> + length(Data) * <gas_per_data_byte> + <gas needed for the inner transaction>
Data: "relayedTxV2" +
"@" + <Smart Contract address to be called in hexadecimal encoding>
"@" + <nonce of the receiver in hexadecimal encoding>
"@" + <data field (function name + args) in hexadecimal encoding>
"@" + <the signature of the inner transaction in hexadecimal encoding>
}
note

Noticing the arguments needed, there are some limitations for the inner transaction: it cannot have call value, a custom gas price or a guardian.

Therefore, when one wants to build such a transaction, the steps would be:

  • create the inner transaction (make sure gasLimit is set to 0)
  • sign it
  • fetch the receiver, nonce, data and signature fields and use them in the relayed transaction

Preparing relayed v2 transaction using mx-sdk-js-core

mx-sdk-js-core has built-in support for relayed transactions version 2, by using a builder which allows one to prepare such a transaction.

Resources:

Example

Here's an example of a relayed v2 transaction. Its intent is:

erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx will call the function add of the contract erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd, while erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th will pay the computation fee

{
"nonce": 37,
"value": "0",
"receiver": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"sender": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
"gasPrice": 1000000000,
"gasLimit": 60372500,
"data": "cmVsYXllZFR4VjJAMDAwMDAwMDAwMDAwMDAwMDA1MDAxZTJlNjExYTljZTFlMGM4ZTMyODNjY2M3YzFiMGY0NjYxOTE3MDc5YTc1Y0AwZkA2MTY0NjRAOWFiZDEzZjRmNTNmM2YyMzU5Nzc0NGQ2NWZjNWQzNTFiYjY3NzNlMDVhOTU0YjQxOWMwOGQxODU5M2QxYzY5MjYyNzlhNGQxNjE0NGQzZjg2NmE1NDg3ODAzMTQyZmNmZjBlYWI2YWQ1ODgyMDk5NjlhY2I3YWJlZDIxMDIwMGI=",
"chainID": "T",
"signature": "2a448b92c16a564a0b1dc8d02fb3a73408decc0aa47d0780a4faa108234d767dc262057b376a9f3c4d9283018c90cb751b55d27c42f59d63cce3ca6213a5ac0a",
"version": 1
}

After decoding the data field (base64 to string) we'll get:

relayedTxV2@000000000000000005001e2e611a9ce1e0c8e3283ccc7c1b0f4661917079a75c@0f@616464@9abd13f4f53f3f23597744d65fc5d351bb6773e05a954b419c08d18593d1c6926279a4d16144d3f866a5487803142fcff0eab6ad588209969acb7abed210200b

Decoding the arguments (useful resources here) we'll get:

  • 1st argument: 000000000000000005001e2e611a9ce1e0c8e3283ccc7c1b0f4661917079a75c => erd1qqqqqqqqqqqqqpgqrchxzx5uu8sv3ceg8nx8cxc0gesezure5awqn46gtd (sc address to be called)
  • 2nd argument: 0f => 15 (nonce of the inner transaction)
  • 3rd argument: 616464 => add (function to be called - no argument needed in this example)
  • 4th argument: 9abd13f4f53f3f23597744d65fc5d351bb6773e05a954b419c08d18593d1c6926279a4d16144d3f866a5487803142fcff0eab6ad588209969acb7abed210200b (the signature of the inner transaction)

Relayed transactions version 3

note

This feature is not yet available on Mainnet. See Spica Protocol Upgrade.

Relayed transactions v3 feature comes with a change on the entire transaction structure, adding two new optional fields:

  • relayer, which is the relayer address that will pay the fees.
  • relayerSignature, the signature of the relayer that proves the agreement of the relayer.

That being said, relayed transactions v3 will look and behave very similar to a regular transaction, the only difference being the gas consumption from the relayer. It is no longer needed to specify the user transaction in the data field.

In terms of gas limit computation, an extra base cost will be consumed. Let's consider the following example: relayed transaction with inner transaction of type move balance, that also has a data field test of length 4.

    gasLimitInnerTx = <base_cost> + <cost_per_byte> * length(txData)
gasLimitInnerTx = 50_000 + 4 * 1_500
gasLimitInnerTx = 56_000

gasLimitRelayedTx = <base_cost> + <gasLimitInnerTx>
gasLimitRelayedTx = 50_000 + 56_000
gasLimitRelayedTx = 106_000

It would look like:

RelayedV3Transaction {
Sender: <Sender address>
Receiver: <Receiver address>
Value: <value>
GasLimit: <base_cost> + <base_cost> + <cost_per_byte> * length(txData)
Relayer: <Relayer address>
RelayerSignature: <Relayer signature>
Signature: <Sender signature>
}

Therefore, in order to build such a transaction, one has to follow the next steps:

  • set the relayer field to the address that would pay the gas
  • add the extra base cost for the relayed operation
  • add sender's signature
  • add relayer's signature
note
  1. For a guarded relayed transaction, the guarded operation fee will also be consumed from the relayer.
  2. Relayer must be different from guardian, in case of guarded sender.
  3. Guarded relayers are not allowed.

Example

Here's an example of a relayed v3 transaction. Its intent is to call the add method of a previously deployed adder contract, with parameter 01

{
"nonce": 0,
"value": "0",
"receiver": "erd1qqqqqqqqqqqqqpgqeunf87ar9nqeey5ssvpqwe74ehmztx74qtxqs63nmx",
"sender": "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"data": "YWRkQDAx",
"gasPrice": 1000000000,
"gasLimit": 5000000,
"signature": "...",
"chainID": "T",
"version": 2,
"relayer": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
"relayerSignature": "..."
}