Calculating Transaction Fees on Moonbeam¶
Introduction¶
Similar to the Ethereum and Substrate APIs for sending transfers on Moonbeam, the Substrate and EVM layers on Moonbeam also have distinct transaction fee models that developers should be aware of when they need to calculate and keep track of the transaction fees of their transactions.
This guide assumes you are interacting with Moonbeam blocks via the Substrate API Sidecar service. There are other ways of interacting with Moonbeam blocks, such as using the Polkadot.js API library. The logic is identical once the blocks are retrieved.
You can reference the Substrate API Sidecar page for information on installing and running your own Sidecar service instance, as well as more details on how to decode Sidecar blocks for Moonbeam transactions.
Note that the information on this page assumes you are running version 14.1.1 of the Substrate Sidecar REST API.
Substrate API Transaction Fees¶
All the information around fee data for transactions sent via the Substrate API can be extracted from the following block endpoint:
GET /blocks/{blockId}
The block endpoints will return data relevant to one or more blocks. You can read more about the block endpoints on the official Sidecar documentation. Read as a JSON object, the relevant nesting structure is as follows:
RESPONSE JSON Block Object:
...
|--number
|--extrinsics
|--{extrinsic_number}
|--method
|--signature
|--nonce
|--args
|--tip
|--hash
|--info
|--era
|--events
|--{event_number}
|--method
|--pallet: "transactionPayment"
|--method: "TransactionFeePaid"
|--data
|--0
|--1
|--2
...
The object mappings are summarized as follows:
Tx Information | Block JSON Field |
---|---|
Fee paying account | extrinsics[extrinsic_number].events[event_number].data[0] |
Total fees paid | extrinsics[extrinsic_number].events[event_number].data[1] |
Tip | extrinsics[extrinsic_number].events[event_number].data[2] |
The transaction fee related information can be retrieved under the event of the relevant extrinsic where the method
field is set to:
pallet: "transactionPayment", method: "TransactionFeePaid"
And then the total transaction fee paid for this extrinsic is mapped to the following field of the block JSON object:
extrinsics[extrinsic_number].events[event_number].data[1]
Ethereum API Transaction Fees¶
To calculate the fee incurred on a Moonbeam transaction sent via the Ethereum API, the following formula can be used:
GasPrice = BaseFee + MaxPriorityFeePerGas < MaxFeePerGas ?
BaseFee + MaxPriorityFeePerGas :
MaxFeePerGas;
Transaction Fee = (GasPrice * TransactionWeight) / 25000
Transaction Fee = (GasPrice * TransactionWeight) / 25000
Transaction Fee = (GasPrice * TransactionWeight) / 25000
Note
If calculating the transaction fee for RT2100 or later on Moonbase Alpha only, you'll need to calculate the BaseFee
using: BaseFee = NextFeeMultiplier * 1250000000 / 10^18
. For Moonbeam or Moonriver, or Moonbase Alpha prior to RT2100, you can use the constant BaseFee
values outlined below.
The following sections describe in more detail each of the components to calculate the transaction fee.
Base Fee¶
The BaseFee
was introduced in EIP-1559, and is a value set by the network itself.
The BaseFee
is static on Moonbeam and Moonriver, and as of RT2100, is dynamic on Moonbase Alpha. The static base fee for each network has the following assigned value:
Variable | Value |
---|---|
BaseFee | 100 Gwei |
Variable | Value |
---|---|
BaseFee | 1 Gwei |
Variable | Value |
---|---|
BaseFee | 1 Gwei |
RT2100 introduced a new dynamic fee mechanism to Moonbase Alpha only that closely resembles EIP-1559, where the BaseFee
is adjusted based on block congestion. Consequently, you need to estimate the BaseFee
for each block using NextFeeMultiplier
. The value of NextFeeMultiplier
can be retrieved from the Substrate Sidecar API, via the following endpoint:
GET /pallets/transaction-payment/storage/nextFeeMultiplier?at={blockId}
The pallets endpoints for Sidecar returns data relevant to a pallet, such as data in a pallet's storage. You can read more about the pallets endpoint in the official Sidecar documentation. The data at hand that's required from storage is the nextFeeMultiplier
, which can be found in the transaction-payment
pallet. The stored nextFeeMultiplier
value can be read directly from the Sidecar storage schema. Read as a JSON object, the relevant nesting structure is as follows:
RESPONSE JSON Storage Object:
|--at
|--hash
|--height
|--pallet
|--palletIndex
|--storageItem
|--keys
|--value
The relevant data will be stored in the value
key of the JSON object. This value is a fixed point data type, hence the real value is found by divided the value
by 10^18
. This is why the calculation of BaseFee
includes such an operation. Please review the RT2100 sample code provided at the end of this page.
GasPrice, MaxFeePerGas and MaxPriorityFeePerGas¶
The values of GasPrice
, MaxFeePerGas
and MaxPriorityFeePerGas
for the applicable transaction types can be read from the block JSON object according to the structure described in the Sidecar API page. The data for an Ethereum transaction in a particular block can be extracted from the following block endpoint:
GET /blocks/{blockId}
The paths to the relevant values have also truncated and reproduced below:
EVM Field | Block JSON Field |
---|---|
MaxFeePerGas | extrinsics[extrinsic_number].args.transaction.eip1559.maxFeePerGas |
MaxPriorityFeePerGas | extrinsics[extrinsic_number].args.transaction.eip1559.maxPriorityFeePerGas |
EVM Field | Block JSON Field |
---|---|
GasPrice | extrinsics[extrinsic_number].args.transaction.legacy.gasPrice |
EVM Field | Block JSON Field |
---|---|
GasPrice | extrinsics[extrinsic_number].args.transaction.eip2930.gasPrice |
Transaction Weight¶
TransactionWeight
is a Substrate mechanism used to measure the execution time a given transaction takes to be executed within a block. For all transactions types, TransactionWeight
can be retrieved under the event of the relevant extrinsic where the method
field is set to:
pallet: "system", method: "ExtrinsicSuccess"
And then TransactionWeight
is mapped to the following field of the block JSON object:
extrinsics[extrinsic_number].events[event_number].data[0].weight
Key Differences with Ethereum¶
As seen in the sections above, there are some key differences between the transaction fee model on Moonbeam and the one on Ethereum that developers should be mindful of when developing on Moonbeam:
-
With the introduction of RT2100, the dynamic fee mechanism used in Moonbase Alpha resembles that of EIP-1559 but the implementation is different
-
The amount of gas used in Moonbeam's transaction fee model is mapped from the transaction's Substrate extrinsic weight value via a fixed factor of 25000. This value is then multiplied with the unit gas price to calculate the transaction fee. This fee model means it can potentially be significantly cheaper to send transactions such as basic balance transfers via the Ethereum API than the Substrate API
Fee History Endpoint¶
Moonbeam networks implement the eth_feeHistory
JSON-RPC endpoint as a part of the support for EIP-1559.
eth_feeHistory
returns a collection of historical gas information from which you can reference and calculate what to set for the MaxFeePerGas
and MaxPriorityFeePerGas
fields when submitting EIP-1559 transactions.
The following curl example will return the gas information of the last 10 blocks starting from the latest block on the respective Moonbeam network using eth_feeHistory
:
curl --location
--request POST 'RPC-API-ENDPOINT-HERE' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
curl --location
--request POST 'RPC-API-ENDPOINT-HERE' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
curl --location
--request POST 'https://rpc.api.moonbase.moonbeam.network' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
curl --location
--request POST 'http://127.0.0.1:9933' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_feeHistory",
"params": ["0xa", "latest"]
}'
Sample Code for Calculating Transaction Fees¶
The following code snippet uses the Axios HTTP client to query the Sidecar endpoint /blocks/head
for the latest finalized block. It then calculates the transaction fees of all transactions in the block according to the transaction type (for Ethereum API: legacy, EIP-1559 or EIP-2930 standards, and for Substrate API), as well as calculating the total transaction fees in the block.
The dynamic fee calculation snippet below is only applicable to Moonbase Alpha as of RT2100. So, if you're calculating fees for Moonbase Alpha prior to RT2100, or for Moonbeam or Moonriver, please use the static fee calculation snippet.
The following code sample is for demo purposes only and should not be used without modification and further testing in a production environment.
const axios = require("axios");
// This script calculates the transaction fees of all transactions in a Moonbeam, Moonriver
// or Moonbase Alpha (prior to RT2100) block according to the transaction type (For Ethereum API:
// legacy, EIP-1559 or EIP-2930 standards, and Substrate API), as well as calculating
// the total fees in the block.
// Define static base fee per network
const baseFee = {
moonbeam: 100000000000n,
moonriver: 1000000000n,
moonbase: 1000000000n,
};
// Endpoint to retrieve the latest block
const endpointBlock = "http://127.0.0.1:8080/blocks/head";
// Endpoint to retrieve the node client's information
const endpointNodeVersion = "http://127.0.0.1:8080/node/version";
async function main() {
try {
// Create a variable to sum the transaction fees in the whole block
let totalFees = 0n;
// Find which Moonbeam network the Sidecar is pointing to
const response_client = await axios.get(endpointNodeVersion);
const network = response_client.data.clientImplName;
// Retrieve the block from the Sidecar endpoint
const response_block = await axios.get(endpointBlock);
// Retrieve the block height of the current block
console.log("Block Height: " + response_block.data.number);
// Iterate through all extrinsics in the block
response_block.data.extrinsics.forEach((extrinsic) => {
// Create an object to store transaction information
let transactionData = new Object();
// Set the network field
transactionData["network"] = network;
// Filter for Ethereum Transfers
if (extrinsic.method.pallet === "ethereum" && extrinsic.method.method === "transact") {
// Iterate through the events to get non type specific parameters
extrinsic.events.forEach((event) => {
if (event.method.pallet === "ethereum" && event.method.method === "Executed") {
// Get Transaction Hash
transactionData["hash"] = event.data[2];
}
if (event.method.pallet === "system" && event.method.method === "ExtrinsicSuccess") {
// Add correction weight if needed to Transaction Weight!
transactionData["weight"] = BigInt(event.data[0].weight.refTime);
}
});
// Get the transaction type and type specific parameters and compute the transaction fee
if (extrinsic.args.transaction.legacy) {
transactionData["txType"] = "legacy";
transactionData["gasPrice"] = BigInt(extrinsic.args.transaction.legacy.gasPrice);
transactionData["txFee"] = (transactionData["gasPrice"] * transactionData["weight"]) / 25000n;
} else if (extrinsic.args.transaction.eip1559) {
transactionData["txType"] = "eip1599";
transactionData["maxFeePerGas"] = BigInt(extrinsic.args.transaction.eip1559.maxFeePerGas);
transactionData["maxPriorityFeePerGas"] = BigInt(extrinsic.args.transaction.eip1559.maxPriorityFeePerGas);
transactionData["baseFee"] = baseFee[transactionData["network"]];
// Gas price dependes on the MaxFeePerGas and MaxPriorityFeePerGas set
transactionData["gasPrice"] =
transactionData["baseFee"] + transactionData["maxPriorityFeePerGas"] < transactionData["maxFeePerGas"]
? transactionData["baseFee"] + transactionData["maxPriorityFeePerGas"]
: transactionData["maxFeePerGas"];
transactionData["txFee"] = (transactionData["gasPrice"] * transactionData["weight"]) / 25000n;
} else if (extrinsic.args.transaction.eip2930) {
transactionData["txType"] = "eip2930";
transactionData["gasPrice"] = BigInt(extrinsic.args.transaction.eip2930.gasPrice);
transactionData["txFee"] = (transactionData["gasPrice"] * transactionData["weight"]) / 25000n;
}
// Increment totalFees
totalFees += transactionData["txFee"];
// Display the tx information to console
console.log(transactionData);
}
// Filter for Substrate transactions, check if the extrinsic has a "TransactionFeePaid" event
else {
extrinsic.events.forEach((event) => {
if (event.method.pallet === "transactionPayment" && event.method.method === "TransactionFeePaid") {
transactionData["txType"] = "substrate";
transactionData["txFee"] = event.data[1];
transactionData["tip"] = event.data[1];
}
if (event.method.pallet === "system" && event.method.method === "ExtrinsicSuccess") {
transactionData["weight"] = event.data[0].weight.refTime;
}
});
}
});
// Output the total amount of fees in the block
console.log("Total fees in block: " + totalFees);
} catch (err) {
console.log(err);
}
}
main();
const axios = require('axios');
// This script calculates the transaction fees of all transactions in a Moonbase Alpha
// block (as of RT2100) according to the transaction type (For Ethereum API: legacy,
// EIP-1559 or EIP-2930 standards, and Substrate API), as well as calculating the total
// fees in the block.
// Endpoint to retrieve the latest block
const endpointBlock = 'http://127.0.0.1:8080/blocks/head';
// Endpoint to retrieve the latest nextFeeMultiplier
const endpointPallet = 'http://127.0.0.1:8080/pallets/transaction-payment/storage/nextFeeMultiplier?at=';
// Endpoint to retrieve the node client's information
const endpointNodeVersion = 'http://127.0.0.1:8080/node/version';
async function main() {
try {
// Create a variable to sum the transaction fees in the whole block
let totalFees = 0n;
// Find which Moonbeam network the Sidecar is pointing to
const response_client = await axios.get(endpointNodeVersion);
const network = response_client.data.clientImplName;
// Retrieve the block from the Sidecar endpoint
const response_block = await axios.get(endpointBlock);
// Retrieve the block height of the current block
console.log('Block Height: ' + response_block.data.number);
// Find the block's nextFeeMultiplier
const response_pallet = await axios.get(endpointPallet + response_block.data.number);
// Iterate through all extrinsics in the block
response_block.data.extrinsics.forEach((extrinsic) => {
// Create an object to store transaction information
let transactionData = new Object();
// Set the network field
transactionData['network'] = network;
// Filter for Ethereum Transfers
if (extrinsic.method.pallet === 'ethereum' && extrinsic.method.method === 'transact') {
// Iterate through the events to get non type specific parameters
extrinsic.events.forEach((event) => {
if (event.method.pallet === 'ethereum' && event.method.method === 'Executed') {
// Get Transaction Hash
transactionData['hash'] = event.data[2];
}
if (event.method.pallet === 'system' && event.method.method === 'ExtrinsicSuccess') {
// Add correction weight if needed to Transaction Weight!
transactionData['weight'] = BigInt(event.data[0].weight.refTime);
}
});
// Get the transaction type and type specific parameters and compute the transaction fee
if (extrinsic.args.transaction.legacy) {
transactionData['txType'] = 'legacy';
transactionData['gasPrice'] = BigInt(extrinsic.args.transaction.legacy.gasPrice);
transactionData['txFee'] = (transactionData['gasPrice'] * transactionData['weight']) / 25000n;
} else if (extrinsic.args.transaction.eip1559) {
transactionData['txType'] = 'eip1599';
transactionData['maxFeePerGas'] = BigInt(extrinsic.args.transaction.eip1559.maxFeePerGas);
transactionData['maxPriorityFeePerGas'] = BigInt(extrinsic.args.transaction.eip1559.maxPriorityFeePerGas);
transactionData['baseFee'] =
(BigInt(response_pallet.data.value) * 1250000000n) / BigInt('1000000000000000000');
// Gas price dependes on the MaxFeePerGas and MaxPriorityFeePerGas set
transactionData['gasPrice'] =
transactionData['baseFee'] + transactionData['maxPriorityFeePerGas'] < transactionData['maxFeePerGas']
? transactionData['baseFee'] + transactionData['maxPriorityFeePerGas']
: transactionData['maxFeePerGas'];
transactionData['txFee'] = (transactionData['gasPrice'] * transactionData['weight']) / 25000n;
} else if (extrinsic.args.transaction.eip2930) {
transactionData['txType'] = 'eip2930';
transactionData['gasPrice'] = BigInt(extrinsic.args.transaction.eip2930.gasPrice);
transactionData['txFee'] = (transactionData['gasPrice'] * transactionData['weight']) / 25000n;
}
// Increment totalFees
totalFees += transactionData['txFee'];
// Display the tx information to console
console.log(transactionData);
}
// Filter for Substrate transactions, check if the extrinsic has a "TransactionFeePaid" event
else {
extrinsic.events.forEach((event) => {
if (event.method.pallet === 'transactionPayment' && event.method.method === 'TransactionFeePaid') {
transactionData['txType'] = 'substrate';
transactionData['txFee'] = event.data[1];
transactionData['tip'] = event.data[1];
}
if (event.method.pallet === 'system' && event.method.method === 'ExtrinsicSuccess') {
transactionData['weight'] = event.data[0].weight.refTime;
}
});
}
});
// Output the total amount of fees in the block
console.log('Total fees in block: ' + totalFees);
} catch (err) {
console.log(err);
}
}
main();