In this tutorial, you'll track ERC-20 token transfers from a specific address using the Web3 JavaScript library.
Prerequisites
- An Ethereum project on Infura
- Node.js installed
Steps
1. Create a project directory
Create a new directory for your project. This can be done from the command line:
mkdir trackERC20
Change into the new directory:
cd trackERC20
2. Install required packages
Install the web3
package in the project directory:
npm install web3
info
This example has been written for web3js v4.x. It may not work for earlier versions.
3. Set up the script
Create a file called trackERC20.js
. At the top of file, add the following lines to import the web3.js library and connect to the Infura WebSocket endpoint:
const { Web3 } = require("web3");
async function main(){
const web3 = new Web3("wss://mainnet.infura.io/ws/v3/<YOUR_API_KEY>");
...
}
main();
Make sure to replace <YOUR_API_KEY>
with your Infura API key.
4. Set the ABI
Define the ERC-20 ABI by adding the following to the script:
const abi = [
{
constant: true,
inputs: [],
name: "symbol",
outputs: [
{
name: "",
type: "string",
},
],
payable: false,
stateMutability: "view",
type: "function",
},
{
constant: true,
inputs: [],
name: "decimals",
outputs: [
{
name: "",
type: "uint8",
},
],
payable: false,
stateMutability: "view",
type: "function",
},
];
5. Subscribe to contract events
You can subscribe to the events that token contracts emit, allowing you to track every new token transfer as it occurs.
Add the following filter to the script, which tells the web3.eth.subscribe
function in web3.js which events to track:
let options = {
topics: [web3.utils.sha3("Transfer(address,address,uint256)")],
};
Then, initiate the subscription by passing along the filter:
let subscription = await web3.eth.subscribe("logs", options);
info
In step 3, you wrap the whole script in an async function main()
, because top level await is not allowed except in recent JavaScript versions.
You can also add the following lines to the script to see whether the subscription started successfully or if any errors occurred:
subscription.on("error", (err) => {
throw err;
});
subscription.on("connected", (nr) =>
console.log("Subscription on ERC-20 started with ID %s", nr),
);
6. Read ERC-20 transfers
You can set the listener for the subscription
created in step 5 by adding the following lines to the script:
subscription.on("data", (event) => {
if (event.topics.length == 3) {
...
}
});
info
To verify that the Transfer
event you catch is an ERC-20 transfer, these lines check to see whether the length of the topics
array equals 3. This is because ERC-721 events also emit a Transfer
event but contain four items instead.
Because you can't read the event topics on their own, you must decode them using the ERC-20 ABI. Edit the listener as follows:
subscription.on("data", (event) => {
if (event.topics.length == 3) {
let transaction = web3.eth.abi.decodeLog(
[
{
type: "address",
name: "from",
indexed: true,
},
{
type: "address",
name: "to",
indexed: true,
},
{
type: "uint256",
name: "value",
indexed: false,
},
],
event.data,
[event.topics[0], event.topics[1], event.topics[2]],
);
You can now retrieve the sender address (from
), receiving address (to
), and the number of tokens transferred (value
, though yet to be converted, see step 7) from the transaction
object.
7. Read contract data
Even though you retrieve a value
from the contract, this isn't the actual number of tokens transferred. ERC-20 tokens contain a decimal
value, which indicates the number of decimals a token should have. You can directly call the decimals
method of the smart contract to retrieve the decimal value, after which you can calculate the correct number of tokens sent.
note
It is optional for ERC-20 contracts to implement these methods (see EIP-20: ERC-20 Token Standard), so you check for errors and fall back to default values.
Outside the subscription.on()
listener created in step 6, define a new method that allows you to collect more information from the smart contract:
async function collectData(contract) {
try {
var decimals = await contract.methods.decimals().call();
}
catch {
decimals = 18n;
}
try {
var symbol = await contract.methods.symbol().call();
}
catch {
symbol = '???';
}
return { decimals, symbol };
}
info
Since you’re already requesting the decimals
value from the contract, you can also request the symbol
value to display the ticker of the token.
Inside the listener, call the collectData
function every time a new ERC-20 transaction is found. You can also calculate the correct decimal value:
subscription.on("data", (event) => {
if (event.topics.length == 3) {
let transaction = web3.eth.abi.decodeLog(
...
);
const contract = new web3.eth.Contract(abi, event.address);
collectData(contract).then((contractData) => {
var unit = Object.keys(web3.utils.ethUnitMap).find(
(key) => web3.utils.ethUnitMap[key] == (BigInt(10) ** contractData.decimals)
);
if (!unit) {
// Simplification for contracts that use "non-standard" units, e.g. REDDIT contract returns decimals==8
unit = "wei"
}
const value = web3.utils.fromWei(transaction.value, unit);
console.log(
`Transfer of ${value+' '.repeat(Math.max(0,30-value.length))} ${
contractData.symbol+' '.repeat(Math.max(0,10-contractData.symbol.length))
} from ${transaction.from} to ${transaction.to}`,
);
8. Track a specific address
You can track a specific sender address by reading the from
value of the decoded transaction
object. Add the following line to the listener created in step 6, replacing <SENDER_ADDRESS>
with the Ethereum address to track:
if (transaction.from == "<SENDER_ADDRESS>") {
console.log("Specified address sent an ERC-20 token!");
}
You can also track a specific recipient address receiving any tokens by tracking the transaction.to
value:
if (transaction.to == "<RECIEVING_ADDRESS>") {
console.log("Specified address received an ERC-20 token!");
}
9. Track a specific token
You can track a specific address sending a specific ERC-20 token, by checking for both transaction.from
(the token sender) and event.address
(the ERC-20 smart contract). Add the following line to the listener created in step 6, replacing <SENDER_ADDRESS>
with the Ethereum address to track, and <CONTRACT_ADDRESS>
with the smart contract address to track:
if (
transaction.from == "<SENDER_ADDRESS>" &&
event.address == "<CONTRACT_ADDRESS>"
) {
console.log("Specified address transferred specified token!");
}
You can also track any transactions for a specific ERC-20 token, regardless of the sender or recipient:
if (event.address == "<CONTRACT_ADDRESS>") {
console.log("Specified ERC-20 transfer!");
}
10. Run the script
Run the script using the following command:
- Command
- Example output
node trackERC20.js
Complete code overview
const { Web3 } = require("web3");
async function main(){
const web3 = new Web3("wss://mainnet.infura.io/ws/v3/<YOUR_API_KEY>");
let options = {
topics: [web3.utils.sha3("Transfer(address,address,uint256)")],
};
const abi = [
{
constant: true,
inputs: [],
name: "symbol",
outputs: [
{
name: "",
type: "string",
},
],
payable: false,
stateMutability: "view",
type: "function",
},
{
constant: true,
inputs: [],
name: "decimals",
outputs: [
{
name: "",
type: "uint8",
},
],
payable: false,
stateMutability: "view",
type: "function",
},
];
let subscription = await web3.eth.subscribe("logs", options);
async function collectData(contract) {
try {
var decimals = await contract.methods.decimals().call();
}
catch {
decimals = 18n;
}
try {
var symbol = await contract.methods.symbol().call();
}
catch {
symbol = '???';
}
return { decimals, symbol };
}
subscription.on("data", (event) => {
if (event.topics.length == 3) {
let transaction = web3.eth.abi.decodeLog(
[
{
type: "address",
name: "from",
indexed: true,
},
{
type: "address",
name: "to",
indexed: true,
},
{
type: "uint256",
name: "value",
indexed: false,
},
],
event.data,
[event.topics[0], event.topics[1], event.topics[2]],
);
const contract = new web3.eth.Contract(abi, event.address);
collectData(contract).then((contractData) => {
var unit = Object.keys(web3.utils.ethUnitMap).find(
(key) => web3.utils.ethUnitMap[key] == (BigInt(10) ** contractData.decimals)
);
if (!unit) {
// Simplification for contracts that use "non-standard" units, e.g. REDDIT contract returns decimals==8
unit = "wei"
}
// This is logging each transfer event found:
const value = web3.utils.fromWei(transaction.value, unit);
console.log(
`Transfer of ${value+' '.repeat(Math.max(0,30-value.length))} ${
contractData.symbol+' '.repeat(Math.max(0,10-contractData.symbol.length))
} from ${transaction.from} to ${transaction.to}`,
);
// Below are examples of testing for transactions involving particular EOA or contract addresses
if (transaction.from == "0x495f947276749ce646f68ac8c248420045cb7b5e") {
console.log("Specified address sent an ERC-20 token!");
}
if (transaction.to == "0x495f947276749ce646f68ac8c248420045cb7b5e") {
console.log("Specified address received an ERC-20 token!");
}
if (
transaction.from == "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D" &&
event.address == "0x6b175474e89094c44da98b954eedeac495271d0f"
) {
console.log("Specified address transferred specified token!");
} // event.address contains the contract address
if (event.address == "0x6b175474e89094c44da98b954eedeac495271d0f") {
console.log("Specified ERC-20 transfer!");
}
});
}
});
subscription.on("error", (err) => {
throw err;
});
subscription.on("connected", (nr) =>
console.log("Subscription on ERC-20 started with ID %s", nr),
);
}
main();