Skip to content

Instantly share code, notes, and snippets.

@crystalin
Last active February 12, 2022 12:58
Show Gist options
  • Save crystalin/b2ce44a208af60d62b5ecd1bad513bce to your computer and use it in GitHub Desktop.
Save crystalin/b2ce44a208af60d62b5ecd1bad513bce to your computer and use it in GitHub Desktop.
Script to listen for balance transfer (Ethereum & Substrate)

Intro

There are multiple scenarios to trigger MOVRs transfer. You will find quick description of those below. The web3 SDK (or Ethereum API) is not sufficient to monitor all the transfers, you should use the PolkadotJs SDK (also known as the Substrate API)

Vocabulary

First, we have to consider there are 2 elements associated with a block:

  • Extrinsics (those can be considered "transactions", they are ordered by execution)
  • Events (those are generated from the extrinsic, and can be multiple per extrinsic. They are also ordered)

Scenario 1: Substrate Transfer

  • This will create an extrinsic balances.transfer or _balances.transferKeepAlive _
  • This will create an event balances.Transfer

Scenario 2: Substrate Feature (resulting in MOVRs being transferred)

(Some features, like "Treasury", would also send Tokens to an address)

  • This will create an extrinsic (ex: treasury.proposeSpend)
  • This will create 1 or multiple events balances.Transfer

Scenario 3: Ethereum Transfer

  • This will create an extrinsic ethereum.transact (with input being empty)
  • This will create 1 event balances.Transfer

Scenario 4: Ethereum Smart Contract

  • This will create an extrinsic ethereum.transact (with input having data)
  • This will create 1 or multiple events balances.Transfer

All those scenario will effectively transfer MOVRs, and the easiest way to monitor them is to rely on the event balances.Transfer See the provided scripts

import { typesBundle } from "moonbeam-types-bundle";
import { ApiPromise, WsProvider } from "@polkadot/api";
// This script will listen to all MOVRs transfers (Substrate & Ethereum)
// It will also retrieve the Ethereum Hash (if existing) and the Substrate Hash (always existing)
const main = async () => {
const wsProvider = new WsProvider("wss://wss.moonriver.moonbeam.network");
const polkadotApi = await ApiPromise.create({
provider: wsProvider,
typesBundle: typesBundle as any,
});
await polkadotApi.rpc.chain.subscribeNewHeads(async (lastHeader) => {
const [{ block }, records] = await Promise.all([
polkadotApi.rpc.chain.getBlock(lastHeader.hash),
polkadotApi.query.system.events.at(lastHeader.hash),
]);
block.extrinsics.forEach((extrinsic, index) => {
const {
method: { args, method, section },
} = extrinsic;
const isEthereum = section == "ethereum" && method == "transact";
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));
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 #${lastHeader.number})`);
console.log(` - Triggered by extrinsic: ${substrateHash}`);
if (isEthereum) {
console.log(` - Ethereum (isTransfer: ${isEthereumTransfer}) hash: ${ethereumHash}`);
}
}
});
});
});
};
main();
import { typesBundle } from "moonbeam-types-bundle";
import { ApiPromise, WsProvider } from "@polkadot/api";
// This script will listen to all MOVRs transfers (Substrate & Ethereum)
const main = async () => {
const wsProvider = new WsProvider("wss://wss.moonriver.moonbeam.network");
const polkadotApi = await ApiPromise.create({
provider: wsProvider,
typesBundle: typesBundle as any,
});
polkadotApi.query.system.events((events: any) => {
// Loop through the Vec<EventRecord>
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].toBigInt();
console.log(`Transfer from ${from} to ${to} of ${balance}`);
}
});
});
};
main();
// This script retrieves the fees of a moonbeam block
// Requirements (NodeJS 14+, ts-node)
// npm install @polkadot/api moonbeam-types-bundle
// Ex:
// └────╼ node_modules/.bin/ts-node src/block-fees-simple.ts
// #811083 : 0xbcdf1344362e8f963d8de2aa26eaed213428c2e9739e25b254a7e4f3d5fb1c92
// fees : 9933615642168512
// burnt : 7946892513734808
// supply diff : 7946892513734808
import { ApiPromise, WsProvider } from "@polkadot/api";
import { DispatchInfo } from "@polkadot/types/interfaces";
import { typesBundle } from "moonbeam-types-bundle";
const URL = "wss://moonriver.api.onfinality.io/public-ws";
const BLOCK = 811083;
const main = async () => {
const api = await ApiPromise.create({
provider: new WsProvider(URL),
typesBundle: typesBundle as any,
});
const blockHash = (await api.rpc.chain.getBlockHash(BLOCK)).toString();
const [{ block }, records] = await Promise.all([
api.rpc.chain.getBlock(blockHash),
api.query.system.events.at(blockHash),
]);
let totalFees = 0n;
let totalBurnt = 0n;
for (let index = 0; index < block.extrinsics.length; index++) {
const extrinsic = block.extrinsics[index];
let fee = 0n;
if (extrinsic.method.section == "ethereum") {
// For Ethereum transaction, we take the gas used * gas price
// 1 gas is 25000 weight in current implementation;
// Retrieve the event for the extrinsic info
const { event } = records.find(
({ phase, event }) =>
phase.isApplyExtrinsic &&
phase.asApplyExtrinsic.eq(index) &&
event.section === "system" &&
(event.method === "ExtrinsicSuccess" || event.method === "ExtrinsicFailed")
);
// Retrieve the dispatch info (where the used weight is stored) from the type of event
const dispatchInfo =
event.method === "ExtrinsicSuccess"
? (event.data[0] as DispatchInfo)
: (event.data[1] as DispatchInfo);
const gasPrice = (extrinsic.method.args[0] as any).gasPrice.toBigInt();
fee = dispatchInfo.weight.toBigInt() * gasPrice / 25000n;
} else if (!extrinsic.signer.isEmpty) {
// For Substrate transaction, we only take signed transaction
// we rely on the node payment calculation
const paymentDetails = await api.rpc.payment.queryInfo(
extrinsic.toHex(),
block.header.parentHash
);
fee = paymentDetails.partialFee.toBigInt();
}
totalFees += fee;
totalBurnt += (fee * 80n) / 100n; // 20% is given to treasury, 80% is burnt
}
console.log(
`${`#${block.header.number.toString()}`.padStart(12, " ")} : ${blockHash.toString()}`
);
console.log(` fees : ${totalFees.toString().padStart(30, " ")}`);
console.log(` burnt : ${totalBurnt.toString().padStart(30, " ")}`);
const preSupply = await (await api.at(block.header.parentHash)).query.balances.totalIssuance();
const postSupply = await (await api.at(block.header.hash)).query.balances.totalIssuance();
// This value is often equal to the burnt value,
// EXCEPT when there is staking payout (every round)
console.log(
` supply diff : ${(preSupply.toBigInt() - postSupply.toBigInt()).toString().padStart(30, " ")}`
);
await api.disconnect();
};
main();
import { typesBundle } from "moonbeam-types-bundle";
import { ApiPromise, WsProvider } from "@polkadot/api";
// This script will listen ONLY to transfers initiated from a Substrate Extrinsic, excluding Ethereum Transactions.
const main = async () => {
const wsProvider = new WsProvider("wss://moonriver.api.onfinality.io/public-ws");
const polkadotApi = await ApiPromise.create({
provider: wsProvider,
typesBundle: typesBundle as any,
});
const startBlockNumber = 400458n; // transfer-enabled runtime upgrade block
const endBlockNumber = (await polkadotApi.rpc.chain.getBlock()).block.header.number.toBigInt(); // or for exemple 414465n
for (
let currentBlockNumber = startBlockNumber;
currentBlockNumber <= endBlockNumber;
currentBlockNumber++
) {
let blockHash = await polkadotApi.rpc.chain.getBlockHash(currentBlockNumber);
let { block } = await polkadotApi.rpc.chain.getBlock(blockHash);
const records = await polkadotApi.query.system.events.at(block.header.hash);
block.extrinsics.forEach((extrinsic, index) => {
const {
method: { args, method, section },
} = extrinsic;
// We only want Substrate extrinsics
if (section == "ethereum" && method == "transact") {
return;
}
// Retrieve all events for this extrinsic
const events = records.filter(
({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index)
);
// 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(
`#${block.header.number}: Substrate Transfer from ${from} to ${to} of ${balance} (${substrateHash})`
);
}
});
});
}
};
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment