And I'm sure everyone has their reasons. They just never told me.
When building frontends to Ethereum, you might find yourself using ethers.js without really appreciating why you need it. So, in the name of appreciation, let's not need it.
I chose ethers.js, but web3.js offers a similar tool set. See this comparison if you need help deciding between the two.
Talking To Nodes
One of ethers main gigs is to play translator for our conversations with nodes. A node is a running instance of some blockchain client. A client is an implementation of the protocol that runs the chain. All nodes on the chain should store all the data we're interested in, and the rules by which it may be updated. That is, apparently, the whole point.
- on-chain data + on-chain API = decentralized protocol = sweet sweet programmable disintermediated cooperation
To interface with decentralized protocols, we have to interface with some node. And to interface with some node, we have to speak ETH JSON-RPC.
JSON-RPC is transport agnostic, but all the cool popular nodes support HTTP. You may, though probably won't, have reliable access to some node at some reliable URL. For those of us that don't, there are node service providers.
Whose node?
Node service providers, like Infura or Alchemy, host URLs we can all use to talk to nodes. Here are the Infura URLs for mainnet and Rinkeby (key gated, but the keys are free):
You can send these URLs HTTP POST requests with valid ETH JSON-RPC request bodies, trust they forward those requests to an Ethereum node, and trust they forward an unmodified response back to you. Decentralization!
And say what?
Any valid ETH JSON-RPC request. For example, to ask for our balance:
We specify the method name, eth_getBalance, along with two parameters:
- address: a hex-encoded address
- block: a hex-encoded block number, or block tag (earliest, latest, or pending)
The response will look something like this:
And like a good little engineer, we’ll write a getBalance function for our ethers-allergic app:
Note the result 0x0de0b6b3a7640000 doesn't exactly look like a number. It is, like most inputs/outputs in the ETH JSON-RPC, a hex-encoded unsigned integer. To get at the real balance, we can:
Note that our balance looks slightly larger than 1. The integer representation of our balance is indeed 10^18, just not 10^18 Ether.
Math on-chain is still just math on some computer, so we still need to avoid the imprecision inherent to floating point arithmetic, especially when building financial tools. To address this, Ethereum and other EVM chains represent balances in "wei", where:
This allows math to stay in the realm of wei integers while representing precise decimal amounts of ether.
Okay, little annoying. But not too bad. We can just:
Great. But didn't we just say working with floats (all numbers in javascript are encoded as IEEE 754 double-precision floating point numbers) for balances is dangerous? And won't our balance in wei be greater than Number.MAX_SAFE_INTEGER? Shut up, nerd.
Talking to Decentralized Protocols
Okay, so we can ask for our balance. Fun, but not that fun. To build the UIs of the disintermediated global economy, we need more. We need to interface with decentralized protocols, or smart contracts.
ETH JSON-RPC is stateless and knows nothing of the APIs defined by the smart contracts for which it plays courier. Like how HTTP knows nothing of ETH JSON-RPC. To interact with smart contracts, we specify the actions to take as data within an ETH JSON-RPC request.
Take this AddX contract - shown implemented in solidity. Deployed on Rinkeby at 0x1b1b40ac6d0F20AF5CcCDD6B7636DdF31CF50605, initialized with x = 2.
Let's try the addX method. It's a view function, which means it doesn't update any contract state. Which means we can call it without signing a transaction. Which is great, because we don't know how to do that. We'll use eth_call.
eth_call - Executes a new message call immediately without creating a transaction on the block chain.
And specify a single parameter, with the following fields
- to: the address of the contract
- input: "input data", or "call data", whatever that means
Our request will look like this:
The result being:
It worked!
An input of "0x36d3dc4b0000...0002" means call addX with the first (and only) parameter set to 2. If that isn't immediately obvious, maybe web3 just isn't for you.
So how do we get this input? Here's what I reverse engineered then later confirmed with the docs
So, putting it all together, we can make this great app that gets our balance and adds it to x - riveting!
So after all that tedious, pretty use-case-specific javascripting, we still didn't get it right. We could fix our "app" by avoiding native js numbers altogether. And, instead, use our favorite arbitrary precision arithmetic library, like bignumber.js or decimal.js. But we'd still have to manage translation to and from ETH JSON-RPC - which was the real pain. And we haven't even tried:
- encoding dynamic inputs (like arrays)
- decoding struct responses
- signing transactions
- or handling events
Maybe we need ethers
Here's the same app, but with ether's help:
Yes, importing the contract factory does make this seem shorter than it really is. But that’s autogenerated, and therefore not a direct burden. And less code is the least of the benefits gained.
Just in this snippet, Ethers gives us:
- well-typed, human-readable Contract objects for interacting with smart contracts
- a BigNumber type + utilities for safely working with on-chain values
And, not shown:
- utilities for registering event listeners, with filters
- ability to manage wallet connections, and sign transactions on the user's behalf
- excellent docs
- ens support
- general cryptography utilities
- all these things are well-tested
So, in conclusion, thank you ricmoo for making my life much easier. I do need ethers.