ERC20 is a standard for Ethereum contracts to create tokens living on the Ethereum chain. Each ERC20 token is a smart contract consist of some functions defined in its ABI. I don’t know if you are familiar with the Ethereum terminology so I’ll explain these alien words to you briefly.
ABI
ABI, Application Binary Interface is a pretty common concept in Computer Science and its applications. Usually referred to in OS development, an ABI is a binary interface that provides functions that you can call. In Ethereum an ABI is a description of what a smart contract can store and do. Calling these functions from ABI is done by send bytecodes to Ethereum Virtual Machine. Then the Ethereum Virtual machine, acting as a worldwide computer, will store and manipulate the data smart contract is referring to. An example and most common ABI for ERC20 tokens is:
[ { "constant": true, "inputs": [], "name": "name", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "balance", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" } ], "name": "allowance", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "payable": true, "stateMutability": "payable", "type": "fallback" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]
Please check all events and functions to know what an ERC20 smart contract can do. We will use this ABI in JSON format for sending and learning the balance of tokens. You can use it for many other things, like listening to any balance change of an account. I believe the best way to learn is practicing so let’s dive into the ethers.js.
Setup Ethers.js
First, we need to install the library from npm. We can do it by running this code in console while in the project directory. For earlier npm versions you should run npm install ethers --save
and for the latest npm install ethers
will suffice for installing the library and saving it to your package.json as a dependency.
Second, we need to require ethers in our code, type the ABI, select the provider, connect to contract and send TRYB. A TRYB is a BiLira co. ERC20 token, of the place I work. You can use any contract address for a different token.
const ethers = require('ethers') const TRYB_CONTRACT_ADDRESS = '0x3196638becfba39ac0c7e5094ebf915203e46c6f' const TRYB_SENDER_PRIVATE_KEY = 'write_here_sender_wallet_private_key' // rinkeby is a test network const provider = ethers.getDefaultProvider('rinkeby') const trybSenderWallet = new ethers.Wallet(TRYB_SENDER_PRIVATE_KEY, provider) // We used the ABI we saw from the beginning of the article const abi = `[ { "constant": true, "inputs": [], "name": "name", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "totalSupply", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "balance", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [ { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" } ], "name": "allowance", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "payable": true, "stateMutability": "payable", "type": "fallback" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]` const contract = new ethers.Contract(TRYB_CONTRACT_ADDRESS, abi, trybSenderWallet); const receiverWallet = 'write_address_of_the_wallet_which_receive_ERC20_token' // We send 0.01 TRYB const howMuchTokens = ethers.utils.parseUnits('0.01', 6) async function init() { await contract.transfer(receiverWallet, howMuchTokens) console.log(`Sent ${ethers.utils.formatUnits(howMuchTokens, 6)} TRYB to address ${receiverWallet} `) } init()
Fancy devs are using async IIFEs for executing async code at the end. My habit is as you see above till top-level await is accepted as a proposal.
We need the private key for sending tokens as you can guess or you know. parseUnits function turns an ether to wei, increasing orders of magnitude. In our case we said to the computer that turn our ether to wei for a 6 decimal point ERC20 token as it’s written in token’s smart contract, ethers.js requires this to proceed. We sent tokens to the receiver wallet from the sender wallet by executing the contract. Then we logged the operation to the console for information purposes. You need await before contract.transfer() function otherwise it won’t work.
Did you read the ABI as I said before? If so, you will recongnize that transfer function as in contract.transfer() is from token’s ABI! And look at the parameters, they are as described in the ABI! So basically you can use ABI functions in that format with ethers.js as they are described in the ABI. You will see this better with our next example.
Check ERC20 token balance in a wallet
How do we know that the receiver wallet received the ERC20 tokens? We check a wallet’s balance. As you read in the ABI(hmm?) there is also a “balanceOf” function that allows you to check the balance of a wallet. Let’s check our receiver wallet’s balance:
const ethers = require('ethers') const TRYB_RECEIVER_PKEY = 'the_receiver_wallet_private_key' const TRYB_CONTRACT_ADDRESS = '0x3196638becfba39ac0c7e5094ebf915203e46c6f' const provider = ethers.getDefaultProvider('rinkeby') const getTRYBBalance = async (wallet) => { const abi = [ { name: 'balanceOf', type: 'function', inputs: [ { name: '_owner', type: 'address', }, ], outputs: [ { name: 'balance', type: 'uint256', }, ], constant: true, payable: false, }, ]; const contract = new ethers.Contract(TRYB_CONTRACT_ADDRESS, abi, wallet); const balance = await contract.balanceOf(wallet.address) return balance } async function init() { const receiverWallet = new ethers.Wallet(TRYB_RECEIVER_PKEY, provider) const receiverTRYBBalance = await getTRYBBalance(receiverWallet) console.log('Reciever balance: ', ethers.utils.formatUnits(receiverTRYBBalance, 6) + ' TRYB', receiverWallet.address) } init()
As you can see here in the “abi” variable, you don’t have to define all ABI. You can just use some part of it according to your needs. our “getTRYBBalance” function calls the “balanceOf(walletAddress)” function from the ABI. The usage can be derived from the ABI if you follow the inputs. There is a very important thing in this ABI. The “constant: true” part. If you don’t do that, you contract will want gas from you because it will think it’s a mutating function. By doing “constant: true” you tell to Ethereum Virtual Machine that you are doing a read-only operation so gas is not needed.
We used “rinkeby” testnet for our code. For operating on the ETH mainnet you should use const provider = ethers.getDefaultProvider()
instead of const provider = ethers.getDefaultProvider('rinkeby')
.
That’s it! You learned how to send and check balance of an ERC20 token. You can apply this article to thousands of ERC20 tokens. I am planning to write more about blockchain, finance and machine learning of them in the future so you can subscribe if you would like to. See ya!
Curiosity. I believe in the power knowledge gives, amplified by the interconnected net: the internet. Since my childhood, I am into programming and coding almost every day for 19 years. Complexity and the sophistication of the problems I would like to tackle only increased. I want to understand. I am a happy man when I understand the underlying mechanism of the Sha256 algorithm. I am a happy man when I invent and discover. Intellectuality and goodwill are my bedrock in my life.
throw this.makeError(message, code, params);
^
Error: invalid hexlify value (argument=”value”, value=”b2ac6866b192fcb416b227599f7efd77c4361713963285f67bd4e7″, code=INVALID_ARGUMENT, version=bytes/5.0.2)
I love you, man! U save my life, thanks