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 2 versions of relayed transactions: v1 and v2. In the end, they both have the same effect, but v2 comes with optimisations in terms of gas usage, making it our recommendation.
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
andReceiverAddress
have to be byte arrays instead of bech32 string addressesValue
has to be a big integer, instead of a stringChainID
has to be a byte array instead of a stringSignature
has to be a byte array instead of the hex version of it
Preparing relayed v1 transaction using erdjs
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:
RelayedV1Transaction {
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>
}
Noticing the arguments needed, there are some limitations for the inner transaction: it cannot have call value or a custom gas price.
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)