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
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 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>
}
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
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 a new optional field InnerTransactions
, which is a collection of inner transactions. That being said, relayed transactions v3 allow the user to send multiple inner transactions on the same relayed transaction which will be executed as normal transactions, without the gas consuming data field of the old relayed transactions versions.
In terms of gas limit computation, let's consider the following example: relayed transaction with one inner transaction of type move balance, that also has a data field test
of length 4.
gasLimitInnerTxs = <base_cost> + <cost_per_byte> * length(txData)
gasLimitInnerTxs = 50_000 + 4 * 1_500
gasLimitInnerTxs = 56_000
gasLimitRelayedTx = <move_balance_cost> * len(inner_transactions) + <gasLimitInnerTxs>
gasLimitRelayedTx = 50_000 * 1 + 56_000
gasLimitRelayedTx = 106_000
Similar for a relayed transaction v3 that has 3 inner transactions of type move balance(empty data field on each):
gasLimitInnerTxs = <move_balance_cost> + <move_balance_cost> + <move_balance_cost>
gasLimitInnerTxs = 50_000 + 50_000 + 50_000
gasLimitInnerTxs = 150_000
gasLimitRelayedTx = <move_balance_cost> * len(inner_transactions) + <gasLimitInnerTxs>
gasLimitRelayedTx = 50_000 * 3 + 150_000
gasLimitRelayedTx = 300_000
It would look like:
RelayedV3Transaction {
Sender: <Relayer address>
Receiver: <Relayer address>
Value: 0
GasLimit: <move_balance_cost> * len(InnerTransactions) + sum(<gas needed for each inner transaction>)
InnerTransactions: []Transaction {
{
"nonce": <Nonce>,
"value": <Value>,
"receiver": <Receiver address>,
"sender": <Sender address>,
"gasLimit": <Gas limit>,
"data": <Data>,
"relayer": <Relayer address>,
},
{
"nonce": <Nonce>,
"value": <Value>,
"receiver": <Receiver address>,
"sender": <Sender address>,
"gasLimit": <Gas limit>,
"data": <Data>,
"relayer": <Relayer address>,
},
...
}
}
Therefore, in order to build such a transaction, one has to follow the next steps:
- create the inner transactions. Make sure that:
- all inner transactions have the
relayer
field set to the address that would pay the gas - all inner transactions senders are in the same shard with the relayer
- in case there are multiple inner transactions from the same sender, the transactions are in the order of the execution, with increasing correct nonces, starting from the current sender's nonce
- all inner transactions are valid and signed
- all inner transactions have the
- create the relayed transactions:
- the receiver of the relayed transactions is the relayer
- value must be 0 and data field empty
- add all inner transactions on the
innerTransactions
field - compute the gas limit:
move balance cost * number of inner transactions + sum of all inner transactions gas limits
- If the relayed transaction v3 is guarded, the cost of the guarded transaction will be consumed only once, not for each inner transaction.
- There are situations where inner transactions are initially accepted, but before their execution, the underlying state changes (due to the execution of other transactions), rendering some or all of these transactions invalid. In such cases, the relayer will still incur gas costs (pays the fee), even though these inner transactions produce no effects.
Example
Here's an example of a relayed v3 transaction. Its intent is:
- 2 sc calls of method
add@01
of a previously deployed adder contract - 1 move balance
{
"nonce": 0,
"value": "0",
"receiver": "erd1dcad0dlle658mggthhpvaypjag2et25yk3c2r7p9gjqkgqpxtukqcqakt0",
"sender": "erd1dcad0dlle658mggthhpvaypjag2et25yk3c2r7p9gjqkgqpxtukqcqakt0",
"gasPrice": 1000000000,
"gasLimit": 10200000,
"signature": "...",
"chainID": "T",
"version": 2,
"innerTransactions": [
{
"nonce": 1,
"value": "0",
"receiver": "erd1qqqqqqqqqqqqqpgq3tssqmcde5w6626mkrm9cehcqgquwmvwl0jq3umv7s",
"sender": "erd1ws52y4t8pmwx9zcuwg0swxymxkg6dwsfpcf3qxmsjvzjtmwkl0jqpn44dn",
"gasPrice": 1000000000,
"gasLimit": 5000000,
"data": "YWRkQDAx",
"signature": "...",
"chainID": "T",
"version": 2,
"relayer": "erd1dcad0dlle658mggthhpvaypjag2et25yk3c2r7p9gjqkgqpxtukqcqakt0"
},
{
"nonce": 2,
"value": "0",
"receiver": "erd1qqqqqqqqqqqqqpgq3tssqmcde5w6626mkrm9cehcqgquwmvwl0jq3umv7s",
"sender": "erd1ws52y4t8pmwx9zcuwg0swxymxkg6dwsfpcf3qxmsjvzjtmwkl0jqpn44dn",
"gasPrice": 1000000000,
"gasLimit": 5000000,
"data": "YWRkQDAx",
"signature": "...",
"chainID": "T",
"version": 2,
"relayer": "erd1dcad0dlle658mggthhpvaypjag2et25yk3c2r7p9gjqkgqpxtukqcqakt0"
},
{
"nonce": 0,
"value": "1000000000000000000",
"receiver": "erd1v7p7uefggz04qst7fzd8hwrl95v03g5uh5fj9vka6ydmdguqy6ws28at85",
"sender": "erd1xfr34s4yft7pk4gf8mt7a2kkfz4z2f3rvuy9pwlgkeymue8pcfkqfxv2mv",
"gasPrice": 1000000000,
"gasLimit": 50000,
"signature": "...",
"chainID": "T",
"version": 2,
"relayer": "erd1dcad0dlle658mggthhpvaypjag2et25yk3c2r7p9gjqkgqpxtukqcqakt0"
}
]
}