Balance Transfers on Moonbeam¶
Introduction¶
While Moonbeam strives to be compatible with Ethereum's Web3 API and EVM, there are some important Moonbeam differences that developers should know and understand in terms of balance transfers of the base network token (for example, GLMR and MOVR).
Token holders have two ways of initiating a balance transfer on Moonbeam. On the one hand, users can use the Ethereum API via apps like MetaMask, MathWallet, or any other tools that use the Ethereum JSON-RPC. On the other hand, users can use the Substrate API via the Polkadot.js Apps website or directly using the Substrate RPC.
Developers need to be aware that token holders can leverage both APIs to transfer the base-layer network token. Note that these comments do not apply to transfers of other assets, like ERC-20 based assets in the Moonriver or Moonbeam EVMs. Transfers of these assets are only done via the Ethereum APIs since these are smart contract interactions.
This guide will outline some of the main differences between both APIs for base-layer network token balance transfers and what to expect when using Moonbeam for the first time.
Ethereum Transfers¶
A simple balance transfer using the Ethereum API relies on the eth_sendRawTransaction
JSON-RPC. This can be done directly from one account to another or via a smart contract.
There are different strategies to listen for transfers or balance changes on Ethereum, which are not covered in this documentation. But they are all focused on different strategies using the Ethereum JSON-RPC.
Moonbeam Transfers¶
As stated before, Moonbeam enables token holders to execute base-layer network token transfers via both the Ethereum and Substrate API. There are multiple scenarios to trigger token transfers on Moonbeam. Consequently, to monitor all transfers, you should use the Polkadot.js SDK (Substrate API).
Before going over the different scenarios, there are two different elements associated with a block:
- Extrinsic — refers to state changes that originated outside of the system itself. The most common form of extrinsic is a transaction. They are ordered by execution
- Events — refers to logs generated from the extrinsic. There can be multiple events per extrinsic. They are ordered by execution
The different transfer scenarios are:
- Substrate transfer — it will create an extrinsic, either
balances.transferAllowDeath
orbalances.transferKeepAlive
. It will trigger onebalances.Transfer
event - Substrate feature — some native Substrate features can create extrinsic that would send tokens to an address. For example, Treasury can create an extrinsic such as
treasury.proposeSend
, which will trigger one or multiplebalances.Transfer
events - Ethereum transfer — it will create an
ethereum.transact
extrinsic with an empty input. It will trigger onebalances.Transfer
event - Ethereum transfers via smart contracts — it will create an
ethereum.transact
extrinsic with some data as input. It will trigger one or multiplebalances.Transfer
events
All the scenarios described above will effectively transfer base-layer network tokens. The easiest way to monitor them all is to rely on the balances.Transfer
event.
Monitor Native Token Balance Transfers¶
The following code samples will demonstrate how to listen to both types of native token transfers, sent via the Substrate or Ethereum API, using either the Polkadot.js API library or Substrate API Sidecar. The following code snippets are for demo purposes only and should not be used without modification and further testing in a production environment.
Using Polkadot.js API¶
The Polkadot.js API package provides developers a way to interact with Substrate chains using JavaScript.
The following code snippet uses subscribeFinalizedHeads
to subscribe to new finalized block headers, loops through extrinsics fetched from the block, and retrieves the events of each extrinsic. Then, it checks if any event corresponds to a balances.Transfer
event. If so, it will extract the from
, to
, amount
, and the tx hash
of the transfer and display it on the console. Note that the amount
is shown in the smallest unit (Wei). You can find all the available information about Polkadot.js and the Substrate JSON-RPC on their official documentation site.
import { typesBundlePre900 } from 'moonbeam-types-bundle';
import { ApiPromise, WsProvider } from '@polkadot/api';
// This script will listen to all GLMR transfers (Substrate & Ethereum) and extract the tx hash
// It can be adapted for Moonriver or Moonbase Alpha
const main = async () => {
// Define the provider for Moonbeam
const wsProvider = new WsProvider('wss://wss.api.moonbeam.network');
// Create the provider using Moonbeam types
const polkadotApi = await ApiPromise.create({
provider: wsProvider,
typesBundle: typesBundlePre900 as any,
});
// Subscribe to finalized blocks
await polkadotApi.rpc.chain.subscribeFinalizedHeads(
async (lastFinalizedHeader) => {
const [{ block }, records] = await Promise.all([
polkadotApi.rpc.chain.getBlock(lastFinalizedHeader.hash),
polkadotApi.query.system.events.at(lastFinalizedHeader.hash),
]);
block.extrinsics.forEach((extrinsic, index) => {
const {
method: { args, method, section },
} = extrinsic;
const isEthereum = section == 'ethereum' && method == 'transact';
// Gets the transaction object
const tx = args[0] as any;
// Convert to the correct Ethereum Transaction format
const ethereumTx =
isEthereum &&
((tx.isLegacy && tx.asLegacy) ||
(tx.isEip1559 && tx.asEip1559) ||
(tx.isEip2930 && tx.asEip2930));
// Check if the transaction is a transfer
const isEthereumTransfer =
ethereumTx &&
ethereumTx.input.length === 0 &&
ethereumTx.action.isCall;
// Retrieve all events for this extrinsic
const events = records.filter(
({ phase }) =>
phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index)
);
// This hash will only exist if the transaction was executed through Ethereum.
let ethereumHash = '';
if (isEthereum) {
// Search for Ethereum execution
events.forEach(({ event }) => {
if (event.section == 'ethereum' && event.method == 'Executed') {
ethereumHash = event.data[2].toString();
}
});
}
// Search if it is a transfer
events.forEach(({ event }) => {
if (event.section == 'balances' && event.method == 'Transfer') {
const from = event.data[0].toString();
const to = event.data[1].toString();
const balance = (event.data[2] as any).toBigInt();
const substrateHash = extrinsic.hash.toString();
console.log(
`Transfer from ${from} to ${to} of ${balance} (block #${lastFinalizedHeader.number})`
);
console.log(` - Triggered by extrinsic: ${substrateHash}`);
if (isEthereum) {
console.log(
` - Ethereum (isTransfer: ${isEthereumTransfer}) hash: ${ethereumHash}`
);
}
}
});
});
}
);
};
main();
In addition, you can find more sample code snippets related to more specific cases around balance transfers on this GitHub page.
Using Substrate API Sidecar¶
Developers can also retrieve Moonbeam blocks and monitor transactions sent via both the Substrate and Ethereum APIs using Substrate API Sidecar, a REST API service for interacting with blockchains built with the Substrate framework.
The following code snippet uses the Axios HTTP client to query the Sidecar endpoint /blocks/head
for the latest finalized block and then decodes the block for the from
, to
, value
, tx hash
, and transaction status
of native token transfers at both the EVM and Substrate API level.
import axios from 'axios';
// This script will decode all native token transfers (Substrate & Ethereum) in a given Sidecar block, and extract the tx hash. It can be adapted for any Moonbeam network.
// Endpoint to retrieve the latest block
const endpoint = 'http://127.0.0.1:8080/blocks/head';
async function main() {
try {
// Retrieve the block from the Sidecar endpoint
const response = await axios.get(endpoint);
// Retrieve the block height of the current block
console.log('Block Height: ' + response.data.number);
// Iterate through all extrinsics in the block
response.data.extrinsics.forEach((extrinsic) => {
// Retrieve Ethereum Transfers
if (
extrinsic.method.pallet === 'ethereum' &&
extrinsic.method.method === 'transact'
) {
// Get the value for any of the three EIP transaction standards supported
const value =
(extrinsic.args.transaction.legacy &&
extrinsic.args.transaction.legacy.value) ||
(extrinsic.args.transaction.eip1559 &&
extrinsic.args.transaction.eip1559.value) ||
(extrinsic.args.transaction.eip2930 &&
extrinsic.args.transaction.eip2930.value);
// Iterate through the events to get transaction details
extrinsic.events.forEach((event) => {
if (
event.method.pallet === 'ethereum' &&
event.method.method === 'Executed'
) {
console.log('From: ' + event.data[0]);
console.log('To: ' + event.data[1]);
console.log('Tx Hash: ' + event.data[2]);
console.log('Value: ' + value);
// Check the execution status
if (event.data[3].succeed) {
console.log('Status: Success');
} else {
console.log('Status: Failed');
}
}
});
}
// Retrieve Substrate Transfers
if (
extrinsic.method.pallet === 'balances' &&
(extrinsic.method.method === 'transferKeepAlive' ||
extrinsic.method.method === 'transferAllowDeath')
) {
// Iterate through the events to get transaction details
extrinsic.events.forEach((event) => {
if (
event.method.pallet === 'balances' &&
event.method.method === 'Transfer'
) {
console.log('From: ' + event.data[0]);
console.log('To: ' + event.data[1]);
console.log('Tx Hash: ' + extrinsic.hash);
console.log('Value: ' + event.data[2]);
// Check the execution status
if (extrinsic.success) {
console.log('Status: Success');
} else {
console.log('Status: Failed');
}
}
});
}
});
} catch (err) {
console.log(err);
}
}
main();
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.
| Created: September 22, 2021