ethers.js/docs.wrm/getting-started.wrm

506 lines
17 KiB
Plaintext

_section: Getting Started @<getting-started> @priority<100>
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.
_heading: Getting Ethers
If using NPM, you must first install Ethers.
_code: installing via NPM @lang<shell>
# Install ethers
/home/ricmoo/test-ethers> npm install ethers
_null:
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 in any way you wish.
_code: importing in Node.js @lang<script>
// 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";
_code: importing ESM in a browser @lang<script>
<script type="module">
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
// Your code here...
</script>
_subsection: Some Common Terminology @<starting-glossary>
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.
_heading: Provider
A [[Provider]] is a read-only connection to the blockchain, which allows
querying the blockchain state, such as account, 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**.
_heading: 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.
_heading: Transaction
To make any state changes to the blockchain, a transaction is required,
which requires a fee to 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**.
_heading: 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]].
_heading: 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.
_subsection: Connecting to Ethereum @<starting-connecting>
This very first thing needed to begin interacting with the blockchain is
connecting to it using a [[Provider]].
_heading: MetaMask (and other injected providers)
The quickest and easiest way to experiment and begin developing
on Ethereum is to use [[link-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 address,
MetaMask will show a pop-up to the user asking for permission.
_code: @lang<script>
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 they 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();
}
_heading: Custom RPC Backend
If you are running your own Ethereum node (e.g. [[link-geth]])
or using a custom third-party service (e.g. [[link-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 to the accounts with
[[JsonRpcProvider-getSigner]].
_code: connecting to a JSON-RPC URL @lang<script>
// 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()
_subsection: User Interaction @<starting-display>
All units in Ethereum tend to be integer values, since dealing with
decimals and floating points can lead to imprecise 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
easily 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.
_code: @lang<javascript>
// Convert user-provided strings in ether to wei for a value
eth = parseEther("1.0")
//_result:
// Convert user-provided strings in gwei to wei for max base fee
feePerGas = parseUnits("4.5", "gwei")
//_result:
// Convert a value in wei to a string in ether to display in a UI
formatEther(eth)
//_result:
// Convert a value in wei to a string in gwei to display in a UI
formatUnits(feePerGas, "gwei")
//_result:
_subsection: Interacting with the Blockchain @<starting-blockchain>
_heading: 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.
_code: @lang<javascript>
//_hide: provider = new InfuraProvider();
// Look up the current block number (i.e. height)
await provider.getBlockNumber()
//_result:
// Get the current balance of an account (by address or ENS name)
balance = await provider.getBalance("ethers.eth")
//_result:
// Since the balance is in wei, you may wish to display it
// in ether instead.
formatEther(balance)
//_result:
// Get the next nonce required to send a transaction
await provider.getTransactionCount("ethers.eth")
//_result:
_heading: 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 [[link-metamask]]) which provides strictly
gated access and requires feedback to the user to approve or
reject operations.
_code: @lang<script>
//_hide: provider = new JsonRpcProvider("http:/\/localhost:8545")
//_hide: provider.resolveName = () => "0x643aA0A61eADCC9Cc202D1915D942d35D005400C";
//_hide: signer = new Wallet(id("test"), provider);
// 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")
});
//_result:
// Often you may wish to wait until the transaction is mined
receipt = await tx.wait();
//_result:
_subsection: Contracts @<starting-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 methods and properties are available on it.
_heading: 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 signature.
_code: simplified ERC-20 ABI @lang<script>
abi = [
"function decimals() view returns (string)",
"function symbol() view returns (string)",
"function balanceOf(address addr) view returns (uint)"
]
// Create a contract
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
_heading: 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.
_code: reading the DAI ERC-20 contract @lang<javascript>
// 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()
//_result:
// The number of decimals the token uses
decimals = await contract.decimals()
//_result:
// Read the token balance for an account
balance = await contract.balanceOf("ethers.eth")
//_result:
// Format the balance for humans, such as in a UI
formatUnits(balance, decimals)
//_result:
_heading: State-changing Methods
_code: change state on an ERC-20 contract @lang<script>
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)
//_result: @TODO
// 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()
//_result: @TODO
_code: forcing a call (simulation) of a state-changing method @lang<javascript>
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)
//_result:
// 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)
//_result:
_heading: 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.
_code: listen for ERC-20 events @lang<script>
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, 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
});
_heading: 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.
_code: query historic ERC-20 events @lang<javascript>
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
//_result:
// The first matching event
events[0]
//_result:
// 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]
//_result:
_subsection: Signing Messages @<starting-signing>
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 authenticate a user and log them in.
_code: @lang<javascript>
// Our signer; Signing messages does not require a Provider
signer = new Wallet(id("test"))
//_result:
message = "sign into ethers.org?"
// Signing the message
sig = await signer.signMessage(message);
//_result:
// Validating a message; notice the address matches the signer
verifyMessage(message, sig)
//_result:
_null:
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.