Blockchain is a brilliant technology that has gained the attention of corporations, tech leaders, and developers all around the world. A world of cryptocurrencies emerged out of a new hope for digital payments, and blockchain makes it all possible.
In this comprehensive tutorial, we’ll show you how to develop a blockchain app using Web3.js. We’ll also review the basics of Ethereum and decentralized blockchain in general and introduce key concepts to help you get started on your blockchain development journey.
Here’s what we’ll cover:
Let’s try to understand blockchain, in the simplest possible terms, by breaking down its etymology. Blockchain is a kind of database that stores information in blocks and then connect those blocks together with a chain. You can think of it as a link list where consecutive blocks are connected with their unique hashes.
Cryptocurrency uses the decentralized blockchain, which means the database is spread among a number of devices so that no single party owns it but all of them hold it. All transactions are immutable, meaning they can’t be changed once recorded. We’ll learn about transactions, smart contracts, coins, tokens and lot more in this article.
A decentralized blockchain is a trustless system where there is no need to trust. That means if you are the part of network, you will hold a copy of the database yourself. It’s a peer-to-peer network where all the peers are connected with each other. The data is immutable and can’t be changed. But new data can be added.
On the other hand, the client-server configuration requires handshaking. The data is under the authority of a server or group of servers. They can change it whenever they want.
Since blockchain is immutable, it is great for keeping data secure from changes. It can be used for contracts, ledgers, marksheets, degrees, licenses, and even voting in elections. Keep in mind, however, that the data is public and anybody can access it, so you shouldn’t load confidential data on public chains. Blockchain technology is better suited for information that is meant to be open and transparent.
Decentralized blockchain is quite secure compared to centralized servers. First, everything is highly encrypted with the SHA algorithm. Second, data is immutable and connected through the chain. If there is an attempt to change the data, the chain will break and whole transaction will be nulled.
For example, if a bad actor were to change the block, they would need to change the connected block as well because it holds the hash of the vulnerable block. If this connected block were updated with the hash of a changed block, its hash would change too, which would require a change in further connected blocks.
In other words, altering a single datum in any block requires all the blocks to be changed. If an incredibly persistent hacker were to somehow accomplish this, even then, the correct data would be persisted over thousands of peer nodes around the world and they would mark the faulty data invalid. The bad actor would need to hack thousands of nodes and change the data everywhere at the same time, which is nearly impossible.
Ethereum is a decentralized blockchain that supports smart contracts. Smart contracts are the byte codes written in Solidity language and compiled on Ethereum Virtual Machines (EVM). Due to these smart contracts, Ethereum allows external apps to build and run on its network.
The Ethereum blockchain runs a famous cryptocurrency known as Ether. Every transaction on this chain requires a gas fee to be paid in Ether.
A lot of decentralized finance (DeFi) apps are built on Ethereum. It is free from all the financial transaction intermediaries such as brokerage, exchange, banks, etc. It only costs a gas fee, which is not dependent on the amount. This gas fee is required to keep the network running.
Apart from Ether coins, many tokens are built over Ethereum using the ERC-20 protocol. This protocol defines all the functions that are required to run a token into the market. A token can represent anything, like a lottery ticket, currency, company’s share etc. That’s why startups are creating their own tokens and circulating in the market on exchanges such as Coinbase, Binance, Gate.io, etc.
Web3.js is a JavaScript library that talks over Ethereum node. This could be a locally deployed Ethereum network or the live chain. It can be used to access information about tokens and Ether coin.
You can also deploy your own application and access using Web3. This library enables you to connect your JavaScript-based frontend to the Ethereum network using HTTP, IPC, and WebSockets.
Web3 can be used to talk with the Ethereum node, but it needs a node to talk to. Just like when you want to connect to a torrent network, you need torrent client like BitTorrent. Here we need a running peer. Blockchain is a peer-to-peer network, so to connect with the network, you either need to connect to a peer or become a peer yourself.
To become a peer node, you can use Geth, also known as Go Ethereum. Geth is an open-source library written in Go language for implementing the Ethereum protocol. After running it on your system, you can connect your JavaScript app with the live network using Web3 by listening it to the localhost.
If you don’t want to run node, you can connect with another peer who is running the network. Infura is one of them. You can create a free account, create a new project, and get the API key.
Infura provides five endpoints: one for production and four for test networks. The production endpoint is Mainnet
and the test endpoints are Ropsten
, Kovan
, Rinkeby
, and goerli
.
After getting the mainnet
endpoint, we can connect with live the Ethereum network using Web3.js.
If you want to run your own Ethereum network locally, you can use Ganache. Ganache is not a live Ethereum network, but a clone that you own. This enables you to test out the protocol easily and without spending real money in gas fees. You won’t have access to any tokens because they are on a production chain and not on your own. However, Ether coin is implemented and it provides 10 accounts with 100 ether each.
You can see that the RPC server is running at 7545 port on localhost. You can configure the gas price and gas limit. Also, a few accounts are provided by default with 100 ETH.
Since Web3.js is a JavaScript library, you can include it in your webpages or in any JS-based framework. To test the functionality, we’re going to run it on a Node.js client.
Install Web3.js using this command:
npm install web3
After installing Web3.js, open your Node console and load it in a variable:
var Web3 = require('web3')
Now, let’s check what this variable holds:
This object shows all the functions that we can access through Web3.js. But Ethereum-specific functions are not yet available because we are not connected with the chain. We need to instantiate it with the Infura mainnet endpoint.
var web3 = new Web3('https://mainnet.infura.io/v3/b583160797e24b88a643ad9a38b0f5aa')
Now if you check web3
, you will find a huge list of functions. These functions provide all the information related to the Ethereum chain network.
Web3 { currentProvider: [Getter/Setter], _requestManager: RequestManager { provider: HttpProvider { withCredentials: false, timeout: 0, headers: undefined, agent: undefined, connected: false, host: 'https://mainnet.infura.io/v3/b583160797e24b88a643ad9a38b0f5aa', httpsAgent: [Agent] }, providers: { WebsocketProvider: [Function: WebsocketProvider], HttpProvider: [Function: HttpProvider], IpcProvider: [Function: IpcProvider] }, subscriptions: Map(0) {} }, givenProvider: null, providers: { WebsocketProvider: [Function: WebsocketProvider], HttpProvider: [Function: HttpProvider], IpcProvider: [Function: IpcProvider] }, _provider: HttpProvider { withCredentials: false, timeout: 0, headers: undefined, agent: undefined, connected: false, host: 'https://mainnet.infura.io/v3/b583160797e24b88a643ad9a38b0f5aa', httpsAgent: Agent { _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, defaultPort: 443, protocol: 'https:', options: [Object], requests: {}, sockets: {}, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: true, maxSockets: Infinity, maxFreeSockets: 256, scheduling: 'lifo', maxTotalSockets: Infinity, totalSocketCount: 0, maxCachedSessions: 100, _sessionCache: [Object], [Symbol(kCapture)]: false } }, setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], BatchRequest: [Function: bound Batch], extend: [Function: ex] { formatters: { inputDefaultBlockNumberFormatter: [Function: inputDefaultBlockNumberFormatter], inputBlockNumberFormatter: [Function: inputBlockNumberFormatter], inputCallFormatter: [Function: inputCallFormatter], inputTransactionFormatter: [Function: inputTransactionFormatter], inputAddressFormatter: [Function: inputAddressFormatter], inputPostFormatter: [Function: inputPostFormatter], inputLogFormatter: [Function: inputLogFormatter], inputSignFormatter: [Function: inputSignFormatter], inputStorageKeysFormatter: [Function: inputStorageKeysFormatter], outputProofFormatter: [Function: outputProofFormatter], outputBigNumberFormatter: [Function: outputBigNumberFormatter], outputTransactionFormatter: [Function: outputTransactionFormatter], outputTransactionReceiptFormatter: [Function: outputTransactionReceiptFormatter], outputBlockFormatter: [Function: outputBlockFormatter], outputLogFormatter: [Function: outputLogFormatter], outputPostFormatter: [Function: outputPostFormatter], outputSyncingFormatter: [Function: outputSyncingFormatter] }, utils: { _fireError: [Function: _fireError], _jsonInterfaceMethodToString: [Function: _jsonInterfaceMethodToString], _flattenTypes: [Function: _flattenTypes], randomHex: [Function: randomHex], BN: [Function], isBN: [Function: isBN], isBigNumber: [Function: isBigNumber], isHex: [Function: isHex], isHexStrict: [Function: isHexStrict], sha3: [Function], sha3Raw: [Function: sha3Raw], keccak256: [Function], soliditySha3: [Function: soliditySha3], soliditySha3Raw: [Function: soliditySha3Raw], encodePacked: [Function: encodePacked], isAddress: [Function: isAddress], checkAddressChecksum: [Function: checkAddressChecksum], toChecksumAddress: [Function: toChecksumAddress], toHex: [Function: toHex], toBN: [Function: toBN], bytesToHex: [Function: bytesToHex], hexToBytes: [Function: hexToBytes], hexToNumberString: [Function: hexToNumberString], hexToNumber: [Function: hexToNumber], toDecimal: [Function: hexToNumber], numberToHex: [Function: numberToHex], fromDecimal: [Function: numberToHex], hexToUtf8: [Function: hexToUtf8], hexToString: [Function: hexToUtf8], toUtf8: [Function: hexToUtf8], stripHexPrefix: [Function: stripHexPrefix], utf8ToHex: [Function: utf8ToHex], stringToHex: [Function: utf8ToHex], fromUtf8: [Function: utf8ToHex], hexToAscii: [Function: hexToAscii], toAscii: [Function: hexToAscii], asciiToHex: [Function: asciiToHex], fromAscii: [Function: asciiToHex], unitMap: [Object], toWei: [Function: toWei], fromWei: [Function: fromWei], padLeft: [Function: leftPad], leftPad: [Function: leftPad], padRight: [Function: rightPad], rightPad: [Function: rightPad], toTwosComplement: [Function: toTwosComplement], isBloom: [Function: isBloom], isUserEthereumAddressInBloom: [Function: isUserEthereumAddressInBloom], isContractAddressInBloom: [Function: isContractAddressInBloom], isTopic: [Function: isTopic], isTopicInBloom: [Function: isTopicInBloom], isInBloom: [Function: isInBloom], compareBlockNumbers: [Function: compareBlockNumbers], toNumber: [Function: toNumber] }, Method: [Function: Method] }, version: '1.5.3', utils: { _fireError: [Function: _fireError], _jsonInterfaceMethodToString: [Function: _jsonInterfaceMethodToString], _flattenTypes: [Function: _flattenTypes], randomHex: [Function: randomHex], BN: <ref *1> [Function: BN] { BN: [Circular *1], wordSize: 26, isBN: [Function: isBN], max: [Function: max], min: [Function: min], red: [Function: red], _prime: [Function: prime], mont: [Function: mont] }, isBN: [Function: isBN], isBigNumber: [Function: isBigNumber], isHex: [Function: isHex], isHexStrict: [Function: isHexStrict], sha3: [Function: sha3] { _Hash: [Object] }, sha3Raw: [Function: sha3Raw], keccak256: [Function: sha3] { _Hash: [Object] }, soliditySha3: [Function: soliditySha3], soliditySha3Raw: [Function: soliditySha3Raw], encodePacked: [Function: encodePacked], isAddress: [Function: isAddress], checkAddressChecksum: [Function: checkAddressChecksum], toChecksumAddress: [Function: toChecksumAddress], toHex: [Function: toHex], toBN: [Function: toBN], bytesToHex: [Function: bytesToHex], hexToBytes: [Function: hexToBytes], hexToNumberString: [Function: hexToNumberString], hexToNumber: [Function: hexToNumber], toDecimal: [Function: hexToNumber], numberToHex: [Function: numberToHex], fromDecimal: [Function: numberToHex], hexToUtf8: [Function: hexToUtf8], hexToString: [Function: hexToUtf8], toUtf8: [Function: hexToUtf8], stripHexPrefix: [Function: stripHexPrefix], utf8ToHex: [Function: utf8ToHex], stringToHex: [Function: utf8ToHex], fromUtf8: [Function: utf8ToHex], hexToAscii: [Function: hexToAscii], toAscii: [Function: hexToAscii], asciiToHex: [Function: asciiToHex], fromAscii: [Function: asciiToHex], unitMap: { noether: '0', wei: '1', kwei: '1000', Kwei: '1000', babbage: '1000', femtoether: '1000', mwei: '1000000', Mwei: '1000000', lovelace: '1000000', picoether: '1000000', gwei: '1000000000', Gwei: '1000000000', shannon: '1000000000', nanoether: '1000000000', nano: '1000000000', szabo: '1000000000000', microether: '1000000000000', micro: '1000000000000', finney: '1000000000000000', milliether: '1000000000000000', milli: '1000000000000000', ether: '1000000000000000000', kether: '1000000000000000000000', grand: '1000000000000000000000', mether: '1000000000000000000000000', gether: '1000000000000000000000000000', tether: '1000000000000000000000000000000' }, toWei: [Function: toWei], fromWei: [Function: fromWei], padLeft: [Function: leftPad], leftPad: [Function: leftPad], padRight: [Function: rightPad], rightPad: [Function: rightPad], toTwosComplement: [Function: toTwosComplement], isBloom: [Function: isBloom], isUserEthereumAddressInBloom: [Function: isUserEthereumAddressInBloom], isContractAddressInBloom: [Function: isContractAddressInBloom], isTopic: [Function: isTopic], isTopicInBloom: [Function: isTopicInBloom], isInBloom: [Function: isInBloom], compareBlockNumbers: [Function: compareBlockNumbers], toNumber: [Function: toNumber] }, eth: <ref *2> Eth { currentProvider: [Getter/Setter], _requestManager: RequestManager { provider: [HttpProvider], providers: [Object], subscriptions: Map(0) {} }, givenProvider: null, providers: { WebsocketProvider: [Function: WebsocketProvider], HttpProvider: [Function: HttpProvider], IpcProvider: [Function: IpcProvider] }, _provider: HttpProvider { withCredentials: false, timeout: 0, headers: undefined, agent: undefined, connected: false, host: 'https://mainnet.infura.io/v3/b583160797e24b88a643ad9a38b0f5aa', httpsAgent: [Agent] }, setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], BatchRequest: [Function: bound Batch], extend: [Function: ex] { formatters: [Object], utils: [Object], Method: [Function: Method] }, handleRevert: [Getter/Setter], defaultCommon: [Getter/Setter], defaultHardfork: [Getter/Setter], defaultChain: [Getter/Setter], transactionPollingTimeout: [Getter/Setter], transactionConfirmationBlocks: [Getter/Setter], transactionBlockTimeout: [Getter/Setter], defaultAccount: [Getter/Setter], defaultBlock: [Getter/Setter], maxListenersWarningThreshold: [Getter/Setter], clearSubscriptions: [Function: bound ], removeSubscriptionById: [Function: bound ], net: Net { currentProvider: [Getter/Setter], _requestManager: [RequestManager], givenProvider: null, providers: [Object], _provider: [HttpProvider], setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], BatchRequest: [Function: bound Batch], extend: [Function], getId: [Function], isListening: [Function], getPeerCount: [Function], getNetworkType: [Function: bound getNetworkType] }, accounts: Accounts { currentProvider: [Getter/Setter], _requestManager: [RequestManager], givenProvider: null, providers: [Object], _provider: [HttpProvider], setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], _ethereumCall: [Object], wallet: [Wallet] }, personal: Personal { currentProvider: [Getter/Setter], _requestManager: [RequestManager], givenProvider: null, providers: [Object], _provider: [HttpProvider], setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], BatchRequest: [Function: bound Batch], extend: [Function], net: [Net], defaultAccount: [Getter/Setter], defaultBlock: [Getter/Setter], getAccounts: [Function], newAccount: [Function], unlockAccount: [Function], lockAccount: [Function], importRawKey: [Function], sendTransaction: [Function], signTransaction: [Function], sign: [Function], ecRecover: [Function] }, Contract: [Function: Contract] { setProvider: [Function (anonymous)], defaultAccount: null, defaultBlock: 'latest', transactionBlockTimeout: 50, transactionConfirmationBlocks: 24, transactionPollingTimeout: 750, handleRevert: false, _requestManager: [RequestManager], _ethAccounts: [Accounts], currentProvider: [HttpProvider] }, Iban: [class Iban], abi: ABICoder {}, ens: ENS { eth: [Circular *2], _detectedAddress: null, _lastSyncCheck: null, registry: [Getter], resolverMethodHandler: [Getter], registryAddress: [Getter/Setter] }, getNodeInfo: [Function: send] { method: [Method], request: [Function: bound ], call: 'web3_clientVersion' }, getProtocolVersion: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_protocolVersion' }, getCoinbase: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_coinbase' }, isMining: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_mining' }, getHashrate: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_hashrate' }, isSyncing: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_syncing' }, getGasPrice: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_gasPrice' }, getFeeHistory: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_feeHistory' }, getAccounts: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_accounts' }, getBlockNumber: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_blockNumber' }, getBalance: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getBalance' }, getStorageAt: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getStorageAt' }, getCode: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getCode' }, getBlock: [Function: send] { method: [Method], request: [Function: bound ], call: [Function: blockCall] }, getUncle: [Function: send] { method: [Method], request: [Function: bound ], call: [Function: uncleCall] }, getBlockTransactionCount: [Function: send] { method: [Method], request: [Function: bound ], call: [Function: getBlockTransactionCountCall] }, getBlockUncleCount: [Function: send] { method: [Method], request: [Function: bound ], call: [Function: uncleCountCall] }, getTransaction: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getTransactionByHash' }, getTransactionFromBlock: [Function: send] { method: [Method], request: [Function: bound ], call: [Function: transactionFromBlockCall] }, getTransactionReceipt: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getTransactionReceipt' }, getTransactionCount: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getTransactionCount' }, sendSignedTransaction: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_sendRawTransaction' }, signTransaction: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_signTransaction' }, sendTransaction: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_sendTransaction' }, sign: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_sign' }, call: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_call' }, estimateGas: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_estimateGas' }, submitWork: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_submitWork' }, getWork: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getWork' }, getPastLogs: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getLogs' }, getChainId: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_chainId' }, requestAccounts: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_requestAccounts' }, getProof: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_getProof' }, getPendingTransactions: [Function: send] { method: [Method], request: [Function: bound ], call: 'eth_pendingTransactions' }, subscribe: [Function (anonymous)] }, shh: Shh { currentProvider: [Getter/Setter], _requestManager: RequestManager { provider: [HttpProvider], providers: [Object], subscriptions: Map(0) {} }, givenProvider: null, providers: { WebsocketProvider: [Function: WebsocketProvider], HttpProvider: [Function: HttpProvider], IpcProvider: [Function: IpcProvider] }, _provider: HttpProvider { withCredentials: false, timeout: 0, headers: undefined, agent: undefined, connected: false, host: 'https://mainnet.infura.io/v3/b583160797e24b88a643ad9a38b0f5aa', httpsAgent: [Agent] }, setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], BatchRequest: [Function: bound Batch], extend: [Function: ex] { formatters: [Object], utils: [Object], Method: [Function: Method] }, net: Net { currentProvider: [Getter/Setter], _requestManager: [RequestManager], givenProvider: null, providers: [Object], _provider: [HttpProvider], setProvider: [Function (anonymous)], setRequestManager: [Function (anonymous)], BatchRequest: [Function: bound Batch], extend: [Function], getId: [Function], isListening: [Function], getPeerCount: [Function] }, subscribe: [Function (anonymous)], getVersion: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_version' }, getInfo: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_info' }, setMaxMessageSize: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_setMaxMessageSize' }, setMinPoW: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_setMinPoW' }, markTrustedPeer: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_markTrustedPeer' }, newKeyPair: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_newKeyPair' }, addPrivateKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_addPrivateKey' }, deleteKeyPair: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_deleteKeyPair' }, hasKeyPair: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_hasKeyPair' }, getPublicKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_getPublicKey' }, getPrivateKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_getPrivateKey' }, newSymKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_newSymKey' }, addSymKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_addSymKey' }, generateSymKeyFromPassword: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_generateSymKeyFromPassword' }, hasSymKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_hasSymKey' }, getSymKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_getSymKey' }, deleteSymKey: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_deleteSymKey' }, newMessageFilter: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_newMessageFilter' }, getFilterMessages: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_getFilterMessages' }, deleteMessageFilter: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_deleteMessageFilter' }, post: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_post' }, unsubscribe: [Function: send] { method: [Method], request: [Function: bound ], call: 'shh_unsubscribe' } }, bzz: Bzz { givenProvider: null, currentProvider: null, isAvailable: [Function (anonymous)], upload: [Function (anonymous)], download: [Function (anonymous)] } }
Out of this whole object, we are interested in the eth
and utils
keys. web3.utils
holds the utility functions, which will help in working with the blockchain, and web3.eth
holds all Ethereum blockchain-related functions.
It’s important to know about the different units in ether. If you look at the above object, you can find the unitMap
property in utils
. You can also get the list by running this command:
web3.utils.unitMap
Unit | Weight |
---|---|
noether |
0 |
wei |
1 |
kwei / babbage / femtoether |
1,000 |
mwei / lovelace / picoether |
1,000,000 |
gwei / shannon / nanoether / nano |
1,000,000,000 |
szabo / microether / micro |
1,000,000,000,000 |
finney / milliether / milli |
1,000,000,000,000,000 |
ether |
1,000,000,000,000,000,000 |
kether / grand |
1,000,000,000,000,000,000,000 |
mether |
1,000,000,000,000,000,000,000,000 |
gether |
1,000,000,000,000,000,000,000,000,000 |
tether |
1,000,000,000,000,000,000,000,000,000,000 |
The smallest unit is wei
and the largest is tether
. There are three important units, which are used for different tasks: wei
, Gwei
, and ether
(1 ether = 1,000,000,000,000,000,000 wei):
Unit | Use Case |
---|---|
wei |
Everything is represented in wei internally. So, if you are looking for the number of coins an account holds, it will be returned in wei by the library. |
Gwei |
The gas fees are represented in Gwei unit. Every transaction needs to pay the gas fees. These fees are not fixed and transaction processing time depends on how much it can pay for gas. |
ether |
The cryptocurrency is represented in this unit. |
web3.utils
provides two functions that can be used to convert one unit to another: toWei
and fromWei
. toWei
converts from another unit to Wei.
web3.utils.toWei(count_to_convert, unit_to_convert_from)
count_to_convert
is the amount that needs to be converted. It should be in the string. unit_to_convert_from
is the current unit of the amount (for example, Ether). Use it like this:
> web3.utils.toWei('34', 'ether') '34000000000000000000'
fromWei
is for converting from Wei to another unit.
web3.utils.fromWei(count_to_convert, unit_to_convert_to)
For example:
> web3.utils.fromWei('48593000000000000000', 'micro') '48593000'
You may use these functions in combination to convert any unit to another unit.
web3.utils.fromWei(web3.utils.toWei(count_to_convert, unit_to_convert_from), unit_to_convert_to)
Suppose, for example, you want to convert 23 Ether to micro:
> web3.utils.fromWei(web3.utils.toWei('23', 'ether'), 'micro') '23000000'
We will review a number of other functions, but before that, we need go over Etherscan.
Etherscan is the Ethereum blockchain explorer. You can get all kinds of information regarding Ether coin, tokens, smart contracts, accounts, etc. So all the operations that Web3 can do can be confirmed by looking into Etherscan.
The homepage displays the information related to Ether coin. You can get information regarding tokens by searching in the bar. At the time of writing, the stats were as follows:
Stat | Value |
---|---|
Price | $2,923 |
Transactions | 1300 M |
Gas Price | 61 Gwei |
Market Cap | ~ $343 B |
Difficulty | 9,043 TH |
Hash Rate | 718,380 GH/s |
Latest Block | 133192777 |
Let’s see these stats through Web3.js — except price because it’s not a technical figure. The price is a matter of trading and blockchain has nothing to do with it. It is determined by supply and demand. If you want the price, you will need to call the APIs of exchanges like Poloniex, Binance, Coinmarketcap, etc.
To get a count of all the transactions, you will need to get the transaction count from each block in the chain. Ethereum does not directly provide the total transaction count. Etherscan must keep this count in its database separately.
First, get the latest block number:
web3.eth.getBlock('latest').then(console.log)
Notice that we’re using the then
block in this function. This is because all the eth
branch functions make network requests on the chain. They return a promise and the then
function is used to get the response.
This will display the information about the latest block.
Here you can see some useful information about a single block. First of all, it is the latest block at the time of running the code.
gasLimit
and gasUsed
You can also see the gasLimit
and gasUsed
. gasLimit
is the maximum amount that the users are willing to pay to mine this block and gasUsed
is the amount that was actually used.
This amount is multiplied by a base gas fee, which is baseFeePerGas
, to get the total fees. In this object, baseFeePerGas
is represented in hexadecimal. You can use the web3.utils
function to convert it into a decimal:
> web3.utils.toDecimal('0x998388ca6') 45677054459
Remember, all the amounts are stored in wei
unit in this chain. So, the per-gas fee for this block is 45677054459 Wei. But how much gas was used that is listed in the gasUsed
key? To get the total gas fee of this block, we can use the following:
> var singleGasFee = web3.utils.toDecimal('0x998388ca6') > var totalGas = 6156205 > var totalFeeInWei = singleGasFee * totalGas > totalFeeInWei 253688272429254180 > var totalFeeInEther = web3.utils.fromWei(totalFeeInWei.toString(), 'ether') > totalFeeInEther '0.25368827242925418'
We can see that this block has paid 0.2537 Ether to the miners.
There are other parameters, such as difficulty
, which determines the complexity of the puzzle the miners need to solve. This keeps on increasing and making it difficult for miners to mine the coins. That’s why more computation and heavy hardware are required, not to mention a lot of time.
For this block, the difficulty is 8,937,488,432,688,686. If we compare it with the difficulty of the first block, we’ll see another huge number.
Difficulty of the first block:
> web3.eth.getBlock(1).then(res => {console.log(res.difficulty)}) 17171480576
The difficulty of first block was 17,171,480,576, but for our block it is 8,937,488,432,688,686.
The object also lists the block number in the number
key. This means that 13,320,123 blocks have already been mined.
Run the loop over all blocks to get the total transactions in each of them and then add them to get the total transactions in the Ethereum chain.
var totalTransactions = 0; for(var i=1; i <= 13320123; i++){ web3.eth.getBlockTransactionCount(i).then(res => { totalTransactions += res; }) }
Here I am using the getBlockTransactionCount
function, but you can also use the getBlock
function and count the transactions from the provided array. You can see in the above image that we are getting an array of transactions in the block. You can use the length
property of JavaScript to get the total transactions.
Note: DO NOT run the above loop. I’m showing it to demonstrate how to get the total transactions, but it’s very costly. There will be 13 million API calls, which is a disaster. If you need the total transaction count, simply get it from the Etherscan and store it in a database. For all additional blocks, you can hit the API and add the transactions to the stored count.
To get the current gas price of the network, you can use the getGasPrice
function of web3.eth
.
> web3.eth.getGasPrice().then(console.log) 110063065122 > web3.utils.fromWei('110063065122', 'gwei') '110.063065122'
The gas price of the network at the time of writing is 110 Gwei.
Everything in blockchain has an address represented in a SHA3 string. This could be an account, token, coin, smart contract, or anything else. If you have an account on the Ethereum blockchain, it must be represented in a unique SHA3 string.
These are the top 10 accounts holding Ether coin. You can see that there are smart contract accounts too. Contracts can also hold the coins.
Let’s perform some operations on an account. We will use the third account from the list, which has a name tag of Binance 7
.
First, store the account address in a variable:
> var accountAddress = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8'
You can see that the address is a hexadecimal string encoded in SHA3 algorithm.
To read the Ether balance of this account:
> web3.eth.getBalance(accountAddress).then(console.log) 2296896444430813427853147
The number is in wei
, so we need to convert it to Ether
:
> web3.utils.fromWei('2296896444430813427853147', 'ether') '2296896.444430813427853147'
This account holds 2,296,896 Ether:
To get the total number of transactions sent by this account, use the following:
> web3.eth.getTransactionCount(accountAddress).then(console.log) 891
Etherscan is showing 966 transactions because it includes received transactions as well.
You can create a new account on the Ethereum blockchain using Web3.js:
> web3.eth.accounts.create() { address: '0x8F0cE820cecf482D0909501508E730cb0d8C806d', privateKey: '0x1912e5bc09cc0def85ae95cdd******************35c96230a59a834ee', signTransaction: [Function: signTransaction], sign: [Function: sign], encrypt: [Function: encrypt] }
You can see the address and private key. Although I am not going to use this account for anything but to safeguard others from doing transactions through this account, I have obfuscated some characters. Do not add any coin or token to this account as it will stay orphan.
This account will be available on Etherscan. It will be empty but available:
Suppose you are developing an application and want to test it. If you test it on live chain, you will have to pay a gas fee for any operation you perform. These operations are recorded as transactions and thus require gas fees.
To solve this issue, you can run your private chain locally using Ganache and deploy your application on it. That way, you won’t need to pay real money as gas fees. You will need to pay the gas fees, but you will have all the Ethers free of cost.
We already installed Ganache and saw that it is running on 7545 port on localhost. Let’s connect with it using Web3.js.
> var web3G = new Web3('http://127.0.0.1:7545')
We are instantiating Ganache in the web3G
variable because web3
is used for the Infura live chain. Also, there are few accounts already created by Ganache.
Let’s create one more account. Here we will need to use the personal
property of Web3.js because we can create an account on our node.
First, check how many accounts we have on Ganache:
> web3G.eth.getAccounts().then(console.log) [ '0xF148563F7968cEcea44957aDa1A9aF795A880718', '0x0AF35629772CFDEb2DE80db66D7D5Bb855965b77', '0x6df34da7BB12eD848AD9f6e169DD27fcCDb06FEd', '0x60B9f90C7C2896Ba7C46f7eE761725E449ef3E89', '0xfA099835F789210A108E8A0A181173a14a9aac63', '0x17188DF995D9ec32D26633DDeFAa3a1644B3e0f2', '0x396D3a9138E39E6F5D5a6e09ab6425eA330AfDbD', '0x17d8c3dDe00Fb8f686591430cCB6a517FB97Ea96', '0xDF5CC5f9Fed1B9a091C6B73984C52D2555d62BEe', '0x55a4AdC17c6903f064E181f3BD2A34374B7C6543' ]
There are 10 accounts. You can see these accounts on Ganache UI too.
Let’s create one more using Web3.js:
> web3G.eth.personal.newAccount('this|is|password').then(console.log) 0x84D8FA3a23ac9066066Dd6c63680BC0FBd31aEe6
This is the new account and it should be available in the list:
> web3G.eth.getAccounts().then(console.log) [ '0xF148563F7968cEcea44957aDa1A9aF795A880718', '0x0AF35629772CFDEb2DE80db66D7D5Bb855965b77', '0x6df34da7BB12eD848AD9f6e169DD27fcCDb06FEd', '0x60B9f90C7C2896Ba7C46f7eE761725E449ef3E89', '0xfA099835F789210A108E8A0A181173a14a9aac63', '0x17188DF995D9ec32D26633DDeFAa3a1644B3e0f2', '0x396D3a9138E39E6F5D5a6e09ab6425eA330AfDbD', '0x17d8c3dDe00Fb8f686591430cCB6a517FB97Ea96', '0xDF5CC5f9Fed1B9a091C6B73984C52D2555d62BEe', '0x55a4AdC17c6903f064E181f3BD2A34374B7C6543', '0x84D8FA3a23ac9066066Dd6c63680BC0FBd31aEe6' ]
The UI might not show the new account because it shows only the autogenerated ones.
We can easily check the balance of any account like we did for the live chain:
> web3G.eth.getBalance('0xF148563F7968cEcea44957aDa1A9aF795A880718').then(console.log) 100000000000000000000
The amount is in wei
and is equivalent to 100 Ether.
Since we are on Ganache, we can see this functionality without spending any real money.
Here, we will send 1 ether from the first account to the second account in the list. We’ll need to provide the gas price and gas limit, so it is important to know these values. The network has a gas limit that changes, but we need to keep our value lower than that and not so much that it can’t be mined.
If you see in the above image, Ganache is providing the following values.
In our transaction, we will set the limit as 6500000 and price as shown:
web3G.eth.sendTransaction({ from: '0xF148563F7968cEcea44957aDa1A9aF795A880718', gasPrice: "20000000000", gas: "6500000", to: '0x0AF35629772CFDEb2DE80db66D7D5Bb855965b77', value: "1000000000000000000", data: "" }, 'this|is|password').then(console.log);
This will return the transaction information:
{ transactionHash: '0x7652e3cf6a4dcb522c897385d1a0acf092f890a4f38685d2141878a9a981e650', transactionIndex: 0, blockHash: '0x7bef068531cba78022cd01bd160663f27f2ccb2a2cebf94f03be08d09f19c111', blockNumber: 1, from: '0xf148563f7968cecea44957ada1a9af795a880718', to: '0x0af35629772cfdeb2de80db66d7d5bb855965b77', gasUsed: 21000, cumulativeGasUsed: 21000, contractAddress: null, logs: [], status: true, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' }
As you can see, the gas used was 21,000 — although we put 6,500,000 because the network takes what is spent. So the total cost paid in gas = 21000 * 20000000000 wei = 0.00042 Ether.
Let’s check the balance of both accounts:
> web3G.eth.getBalance('0xF148563F7968cEcea44957aDa1A9aF795A880718').then(console.log) 98999580000000000000 > web3G.eth.getBalance('0x0AF35629772CFDEb2DE80db66D7D5Bb855965b77').then(console.log) 101000000000000000000
The first account has a balance of 98.99958 Ether because 1 eth is transferred and 0.00042 is spent in gas fees.
The same will be reflected on GUI:
You can get more information in the Blocks and Transactions tabs:
Ether is a coin and everything else implemented on the chain using smart contracts are tokens. The tokens that are used for trading need to be implemented using the ERC-20 protocol. This defines the set of rules to follow.
If you check on Etherscan, you will find the list of all the ERC-20 tokens on the network.
Let’s choose one token and perform the operations over it. For this tutorial, I am choosing BNB.
When you open it on Etherscan, you will find information such as contract address, total token supply, holders, etc.
If you click on the contract address (outlined in red), you will get useful information about it.
We didn’t talk about creating a smart contract because that’s outside the scope of this article. But to put it simply, it is the code of the application. If we’re talking about a token, then the smart contract is the compiled code. This code is written in Solidity language.
This contract page on Etherscan will show you the Solidity code of the BNB token:
When this code is compiled, a bytecode and a JSON object are created, which is known as contract ABI. This ABI file holds the function definitions, which were declared in the Solidity code.
You can also find the ABI for BNB on the following page on Etherscan:
This Solidity code and ABI are not specific to the BNB token because these are standard functions defined in ERC-20 protocol.
You might be thinking that if the Solidity code is not specific to BNB, then how did it get its symbol, total supply, and all other properties? This information was passed as arguments to the constructor.
Here are the arguments:
But how are they passed to the constructor? By appending to the byte code of contract.
First, these arguments are converted to bytecodes:
These different bytecodes are joined together into a single string. Let’s call the following argument bytecode:
We know that the contract source code is compiled into bytecode and ABI. So its bytecode looks like this:
This bytecode is a low-level representation of Solidity code for smart contracts. It is created when Solidity code is compiled by Ethereum Virtual Machine (EVM). To pass the arguments to a Solidity constructor, the argument bytecode is appended to this contract bytecode.
The complete contract bytecode will look like this:
To work with contracts in Web3.js, we need two things: the ABI and contract address. We already learned what ABI is and how to get it and we know the contract address too.
Web3.js provides a Contract
property on the eth
property. We need to instantiate it and store it in a variable. The format of the function is as follows:
> var newContract = new web3.eth.Contract(ABI, contractAddress)
Let’s store the ABI and contract address in their respective variables:
> var ABI = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"withdrawEther","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"burn","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"unfreeze","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"freezeOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_value","type":"uint256"}],"name":"freeze","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initialSupply","type":"uint256"},{"name":"tokenName","type":"string"},{"name":"decimalUnits","type":"uint8"},{"name":"tokenSymbol","type":"string"}],"payable":false,"type":"constructor"},{"payable":true,"type":"fallback"},{"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"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Freeze","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Unfreeze","type":"event"}]
> var contractAddress = '0xB8c77482e45F1F44dE1745F52C74426C631bDD52'
Now we can perform different operations over this newContract
object. The various methods are defined in the newContract.methods
branch. You can run it on the node and check the list:
If you want to get the name of the token, run:
> newContract.methods.name().call().then(console.log) BNB
To get the total supply, run:
> newContract.methods.totalSupply().call().then(console.log) 16579517055253348798759097
Similarly, we can perform all other operation related to a token.
In this tutorial, we covered the basics of Web3.js, blockchain, Ethereum, Infura, Ganache, smart contracts, tokens, and much more.
If you’re looking to enter the world of blockchain and DApp development, your next steps should include familiarizing yourself with Solidity and MetaMask and learning how to create and deploy smart contracts, build DeFi apps on the blockchain, and create tokens using ERC-20.
The blockchain world is massive, rapidly evolving, and full of possibilities. I hope this tutorial gave you the insight and motivation you need to move forward in your blockchain journey.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.