ethers
beta-exports.16
DOCUMENTATION
Getting Started
Some Common Terminology
Connecting to Ethereum
User Interaction
Interacting with the Blockchain
Contracts
Signing Messages
Ethereum Basics
Application Programming Interface
Migrating from v5
Contributings and Hacking
License and Copyright
Documentation »Getting Started
Getting Started

This is a very short introduction to Ethers, but covers many of the most common operations that developers require and provides a starting point for those newer to Ethereum.

Getting Ethers

If using NPM, you must first install Ethers.

installing via NPM
# Install ethers /home/ricmoo/test-ethers> npm install ethers@beta-exports

Everything in Ethers is exported from its root as well as on the ethers object. There are also exports in the package.json to facilitate more fine-grained importing.

Generally this documentation will presume all exports from ethers have been imported in the code examples, but you may import the necessary objects any way you wish.

importing in Node.js
// Import everything import { ethers } from "ethers"; // Import just a few select items import { BrowserProvider, parseUnits } from "ethers"; // Import from a specific export import { HDNodeWallet } from "ethers/wallet";
importing ESM in a browser
<script type="module"> import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.min.js"; // Your code here... </script>
Some Common Terminology

To begin, it is useful to have a basic understanding of the types of objects available and what they are responsible for, at a high level.

Provider

A Provider is a read-only connection to the blockchain, which allows querying the blockchain state, such as accout, block or transaction details, querying event logs or evaluating read-only code using call.

If you are coming from Web3.js, you are used to a Provider offering both read and write access. In Ethers, all write operations are further abstracted into another Object, the Signer.

Signer

A Signer wraps all operations that interact with an account. An account generally has a private key located somewhere, which can be used to sign a variety of types of payloads.

The private key may be located in memory (using a Wallet) or protected via some IPC layer, such as MetaMask which proxies interaction from a website to a browser plug-in, which keeps the private key out of the reach of the website and only permits interaction after requesting permission from the user and receiving authorization.

Transaction

To make any state changes to the blockchain, a transaction is required, which requires a fee be paid, where the fee covers the associated costs with executing the transaction (such as reading the disk and performing maths) and storing the updated information.

If a transaction reverts, a fee must still be paid, since the validator still had to expend resources to try running the transaction to determine that it reverted and the details of its failure are still be recorded.

Transactions include sending ether from one user to another, deploying a Contract or executing a state-changing operation against a Contract.

Contract

A Contract is a program that has been deployed to the blockchain, which includes some code and has allocated storage which it can read from and write to.

It may be read from when it is connected to a Provider or state-changing operations can be called when connected to a Signer.

Receipt

Once a Transaction has been submitted to the blockchain, it is placed in the memory pool (mempool) until a validator decides to include it.

A transaction's changes are only made once it has been included in the blockchain, at which time a receipt is available, which includes details about the transaction, such as which block it was included in, the actual fee paid, gas used, all the events that it emitted and whether it was successful or reverted.

Connecting to Ethereum

This very first thing needed to begin interacting with the blockchain is connecting to it using a Provider.

MetaMask (and other injected providers)

The quickest and easiest way to experiment and begin developing on Ethereum is to use MetaMask, which is a browser extension that injects objects into the window, providing:

  • read-only access to the Ethereum network (a Provider)
  • authenticated write access backed by a private key (a Signer)

When requesting access to the authenticated methods, such as sending a transaction or even requesting the private key addess, MetaMask will show a pop-up to the user asking for permission.

let signer = null; let provider; if (window.ethereum == null) { // If MetaMask is not installed, we use the default provider, // which is backed by a variety of third-party services (such // as INFURA). They do not have private keys installed so are // only have read-only access console.log("MetaMask not installed; using read-only defaults") provider = ethers.getDefaultProvider() } else { // Connect to the MetaMask EIP-1193 object. This is a standard // protocol that allows Ethers access to make all read-only // requests through MetaMask. provider = new ethers.BrowserProvider(window.ethereum) // It also provides an opportunity to request access to write // operations, which will be performed by the private key // that MetaMask manages for the user. signer = await provider.getSigner(); }
Custom RPC Backend

If you are running your own Ethereum node (e.g. Geth) or using a custom third-party service (e.g. INFURA), you can use the JsonRpcProvider directly, which communicates using the link-jsonrpc protocol.

When using your own Ethereum node or a developer-base blockchain, such as Hardhat or Ganache, you can get access the accounts with JsonRpcProvider-getSigner.

connecting to a JSON-RPC URL
// If no %%url%% is provided, it connects to the default // http://localhost:8545, which most nodes use. provider = new ethers.JsonRpcProvider(url) // Get write access as an account by getting the signer signer = await provider.getSigner()
User Interaction

All units in Ethereum tend to be integer values, since dealing with decimals and floating points can lead to inprecise and non-obvious results when performing mathematic operations.

As a result, the internal units used (e.g. wei) which are suited for machine-readable purposes and maths are often very large and not terribly human-readable.

For example, imagine dealing with dollars and cents; you would show values like "$2.56". In the blockchain world we would keep all values as cents, so that would be 256 cents, internally.

So, when accepting data that a user types, it must be converted from its decimal string representation (e.g. "2.56") to its lowest-unit integer representation (e.g. 256). And when displaying a value to a user the opposite operation is necessary.

In Ethereum, one ether is equal to 10 ** 18 wei and one gwei is equal to 10 ** 9 wei, so the values get very large very quickly, so some convenience functions are provided to help convert between representations.

// Convert user-provided strings in ether to wei for a value eth = parseEther("1.0") // 1000000000000000000n // Convert user-provided strings in gwei to wei for max base fee feePerGas = parseUnits("4.5", "gwei") // 4500000000n // Convert a value in wei to a string in ether to display in a UI formatEther(eth) // '1.0' // Convert a value in wei to a string in gwei to display in a UI formatUnits(feePerGas, "gwei") // '4.5'
Interacting with the Blockchain

Querying State

Once you have a Provider, you have a read-only connection to the data on the blockchain. This can be used to query the current account state, fetch historic logs, look up contract code and so on.

// Look up the current block number (i.e. height) await provider.getBlockNumber() // 16540202 // Get the current balance of an account (by address or ENS name) balance = await provider.getBalance("ethers.eth") // 182334002436162568n // Since the balance is in wei, you may wish to display it // in ether instead. formatEther(balance) // '0.182334002436162568' // Get the next nonce required to send a transaction await provider.getTransactionCount("ethers.eth") // 3
Sending Transactions

To write to the blockchain you require access to a private key which controls some account. In most cases, those private keys are not accessible directly to your code, and instead you make requests via a Signer, which dispatches the request to a service (such as MetaMask) which provides strictly gated access and requires feedback to the user to approve or reject operations.

// When sending a transaction, the value is in wei, so parseEther // converts ether to wei. tx = await signer.sendTransaction({ to: "ethers.eth", value: parseEther("1.0") }); // Often you may wish to wait until the transaction is mined receipt = await tx.wait();
Contracts

A Contract is a meta-class, which means that its definition its derived at run-time, based on the ABI it is passed, which then determined what mehods and properties are available on it.

Application Binary Interface (ABI)

Since all operations that occur on the blockchain must be encoded as binary data, we need a concise way to define how to convert between common objects (like strings and numbers) and its binary representation, as well as encode the ways to call and interpret the Contract.

For any method, event or error you wish to use, you must include a Fragment to inform Ethers how it should encode the request and decode the result.

Any methods or events that are not needed can be safely excluded.

There are several common formats available to describe an ABI. The Solidity compiler usually dumps a JSON representation but when typing an ABI by hand it is often easier (and more readable) to use the human-readable ABI, which is just the Solidity signautre.

simplified ERC-20 ABI
abi = [ "function decimals() returns (string)", "function symbol() returns (string)", "function balanceOf(address addr) returns (uint)" ] // Create a contract contract = new Contract("dai.tokens.ethers.eth", abi, provider)
Read-only methods (i.e. view and pure)

A read-only method is one which cannot change the state of the blockchain, but often provide a simple interface to get important data about a Contract.

reading the DAI ERC-20 contract
// The contract ABI (fragments we care about) abi = [ "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function balanceOf(address a) view returns (uint)" ] // Create a contract; connected to a Provider, so it may // only access read-only methods (like view and pure) contract = new Contract("dai.tokens.ethers.eth", abi, provider) // The symbol name for the token sym = await contract.symbol() // 'DAI' // The number of decimals the token uses decimals = await contract.decimals() // 18n // Read the token balance for an account balance = await contract.balanceOf("ethers.eth") // 189668770000000000000000n // Format the balance for humans, such as in a UI formatUnits(balance, decimals) // '189668.77'
State-changing Methods

change state on an ERC-20 contract
abi = [ "function transfer(address to, uint amount)" ] // Connected to a Signer; can make state changing transactions, // which will cost the account ether contract = new Contract("dai.tokens.ethers.eth", abi, signer) // Send 1 DAI amount = parseUnits("1.0", 18); // Send the transaction tx = await contract.transfer("ethers.eth", amount) // Currently the transaction has been sent to the mempool, // but has not yet been included. So, we... // ...wait for the transaction to be included. await tx.wait()
forcing a call (simulation) of a state-changing method
abi = [ "function transfer(address to, uint amount) returns (bool)" ] // Connected to a Provider since we only require read access contract = new Contract("dai.tokens.ethers.eth", abi, provider) amount = parseUnits("1.0", 18) // There are many limitations to using a static call, but can // often be useful to preflight a transaction. await contract.transfer.staticCall("ethers.eth", amount) // true // We can also simulate the transaction as another account other = new VoidSigner("0x643aA0A61eADCC9Cc202D1915D942d35D005400C") contractAsOther = contract.connect(other.connect(provider)) await contractAsOther.transfer.staticCall("ethers.eth", amount) // true
Listening to Events

When adding event listeners for a named event, the event parameters are destructed for the listener.

There is always one additional parameter passed to a listener, which is an EventPayload, which includes more information about the event including the filter and a method to remove that listener.

listen for ERC-20 events
abi = [ "event Transfer(address indexed from, address indexed to, uint amount)" ] // Create a contract; connected to a Provider, so it may // only access read-only methods (like view and pure) contract = new Contract("dai.tokens.ethers.eth", abi, provider) // Begin listening for any Transfer event contract.on("Transfer", (from, to, _amount, event) => { const amount = formatEther(_amount, 18) console.log(`${ from } => ${ to }: ${ amount }`); // The `event.log` has the entire EventLog // Optionally, convenience method to stop listening event.removeListener(); }); // Same as above contract.on(contract.filters.Transfer, (from, to, amount, event) => { // See above }) // Listen for any Transfer to "ethers.eth" filter = contract.filters.Transfer("ethers.eth") contract.on(filter, (from, to, amount, event) => { // `to` will always be equal to the address of "ethers.eth" }); // Listen for any event, whether it is present in the ABI // or not. Since unknown events can be picked up, the // parameters are not destructed. contract.on("*", (event) => { // The `event.log` has the entire EventLog });
Query Historic Events

When querying within a large range of blocks, some backends may be prohibitively slow, may return an error or may truncate the results without any indication. This is at the discretion of each backend.

query historic ERC-20 events
abi = [ "event Transfer(address indexed from, address indexed to, uint amount)" ] // Create a contract; connected to a Provider, so it may // only access read-only methods (like view and pure) contract = new Contract("dai.tokens.ethers.eth", abi, provider) // Query the last 100 blocks for any transfer filter = contract.filters.Transfer events = await contract.queryFilter(filter, -100) // The events are a normal Array events.length // 105 // The first matching event events[0] // EventLog { // address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // args: Result(3) [ // '0x9Ad03462506035DD0B8E18083292B499C4a4d2a7', // '0x74de5d4FCbf63E00296fd95d33236B9794016631', // 110000000000000000000n // ], // blockHash: '0x6a2a16931639ee6ed7765090d355a2101b66ede3b06881b447d7313890afe4c4', // blockNumber: 16540105, // data: '0x000000000000000000000000000000000000000000000005f68e8131ecf80000', // fragment: EventFragment { ... }, // index: 78, // interface: Interface { ... }, // provider: InfuraProvider { ... }, // removed: false, // topics: [ // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // '0x0000000000000000000000009ad03462506035dd0b8e18083292b499c4a4d2a7', // '0x00000000000000000000000074de5d4fcbf63e00296fd95d33236b9794016631' // ], // transactionHash: '0x51492a5d4b4bd66b879eaeba1f10c945da10bfab4452e4d70ef53f53b9e8e71b', // transactionIndex: 50 // } // Query all time for any transfer to ethers.eth filter = contract.filters.Transfer("ethers.eth") events = await contract.queryFilter(filter) // The first matching event events[0] // EventLog { // address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // args: Result(3) [ // '0x643aA0A61eADCC9Cc202D1915D942d35D005400C', // '0x8ba1f109551bD432803012645Ac136ddd64DBA72', // 1230000000000000000n // ], // blockHash: '0x3ce00ad556efbeb520775e82ef3afc9d425d65040f7c87de02e3d4a5c2a64a15', // blockNumber: 15986046, // data: '0x0000000000000000000000000000000000000000000000001111d67bb1bb0000', // fragment: EventFragment { ... }, // index: 236, // interface: Interface { ... }, // provider: InfuraProvider { ... }, // removed: false, // topics: [ // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // '0x000000000000000000000000643aa0a61eadcc9cc202d1915d942d35d005400c', // '0x0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba72' // ], // transactionHash: '0xe9376d6c4f80e441ca3882a38a576ae511bf863266efdc4c3e8b27672772994f', // transactionIndex: 128 // }
Signing Messages

A private key can do a lot more than just sign a transaction to authorize it. It can also be used to sign other forms of data, which are then able to be validated for other purposes.

For example, signing a message can be used to prove ownership of an account which a website could use to authenicate a user and log them in.

// Our signer; Signing messages does not require a Provider signer = new Wallet(id("test")) // Wallet { // address: '0xC08B5542D177ac6686946920409741463a15dDdB', // provider: null // } message = "sign into ethers.org?" // Signing the message sig = await signer.signMessage(message); // '0xefc6e1d2f21bb22b1013d05ecf1f06fd73cdcb34388111e4deec58605f3667061783be1297d8e3bee955d5b583bac7b26789b4a4c12042d59799ca75d98d23a51c' // Validating a message; notice the address matches the signer verifyMessage(message, sig) // '0xC08B5542D177ac6686946920409741463a15dDdB'

Many other more advanced protocols built on top of signed messages are used to allow a private key to authorize other users to transfer their tokens, allowing the transaction fees of the transfer to be paid by someone else.

← Documentation (BETA)
Ethereum Basics→
The content of this site is licensed under the Creative Commons License. Generated on January 17, 2023, 7:05pm.