docs: added more docs for getting started

This commit is contained in:
Richard Moore 2023-01-15 07:49:46 -05:00
parent fb035d8416
commit 4de185b5c7
2 changed files with 480 additions and 41 deletions

@ -1,29 +1,29 @@
_section: Getting Started @<getting-started> @priority<100>
This
The v6 getting started is coming soon...
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: Installing
_code: Install via NPM @lang<shell>
# Create a new folder for your project
/home/ricmoo> mkdir test-ethers
/home/ricmoo> cd test-ethers
_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@beta-exports
/home/ricmoo/test-ethers> npm install ethers@beta
_heading: Importing
_null:
Everything in Ethers is exported at the root as well as on the ``ethers``
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
sub-packages.
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.
_code: Node.js @lang<script>
_code: importing in Node.js @lang<script>
// Import everything
import { ethers } from "ethers";
@ -33,28 +33,80 @@ _code: Node.js @lang<script>
// Import from a specific export
import { HDNodeWallet } from "ethers/wallet";
_code: Web Browsers (ESM) @lang<script>
_code: importing ESM in a browser @lang<script>
<script type="module">
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.min.js";
// Your code here...
</script>
_subsection: Some Common Terminology
_subsection: Some Common Terminology @<starting-glossary>
_heading: PROVIDER
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: SIGNER
_heading: Provider
_heading: TRANSACTION
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**.
_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 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
_heading: CONTRACT
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>
_subsection: Connecting to Ethereum
This very first thing needed to begin interacting with the blockchain is
connecting to it using a [[Provider]].
_heading: MetaMask (and other injected providers)
@ -107,7 +159,7 @@ 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]].
_code: @lang<script>
_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.
@ -117,9 +169,51 @@ _code: @lang<script>
signer = await provider.getSigner()
_subsection: Interacting with the Blockchain
_subsection: User Interaction @<starting-display>
_heading: Querying State (reading)
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.
_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
@ -145,7 +239,7 @@ _code: @lang<javascript>
await provider.getTransactionCount("ethers.eth")
//_result:
_heading: Sending Transactions (writing)
_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
@ -169,39 +263,239 @@ _code: @lang<script>
receipt = await tx.wait();
//_result:
_heading: User Interaction
explain ether vs gwei... format vs parse. getAddress, resolveAddress
_subsection: Contracts @<starting-contracts>
_code: @lang<javascript>
// Convert user-provided strings in ether to wei for a value
eth = parseEther("1.0")
//_result:
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.
// Convert user-provided strings in gwei to wei for max base fee
feePerGas = parseUnits("4.5", "gwei")
//_result:
_heading: Application Binary Interface (ABI)
// Convert a value in wei to a string in ether to display in a UI
formatEther(eth)
//_result:
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.
// Convert a value in wei to a string in gwei to display in a UI
formatUnits(feePerGas, "gwei")
//_result:
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.
_subsection: Contracts
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.
_code: simplified ERC-20 ABI @lang<script>
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)
_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
_heading: Estimating Gas and fees
_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, 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
});
_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.
_subsection: Signing Messages
_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 authenicate 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.

145
docs.wrm/migrating.wrm Normal file

@ -0,0 +1,145 @@
_section: Migrating from v5 @<migrating> @priority<-10>
This guide aims to capture some of the high-level differences
between v5 and v6 to help those migrating an existing app and
those already familiar with v5 that just need a quick primer.
The biggest differnce in v6 is the use of modern ES6 features,
so a lot of changes are largely internal.
- [Contracts](migrate-contracts)
- [Importing](migrate-importing)
_subsection: Contracts @<migrate-contracts>
The [[Contract]] is an ES6 Proxy, which means it can resolve
method names at run-time.
_heading: Ambiguous Methods
In v5, in the case of an ambiguous method, it was necessary to
look up a method by its canonical normalized signature. In v6
the signature does not need to be normalized and the Typed API
provides a cleaner way to access the desired method.
In v5, duplicate definitions also injected warnings into the
console, since there was no way to detect them at run-time.
_code: contracts in v5 @lang<script>
abi = [
"function foo(address bar)",
"function foo(uint160 bar)",
]
contract = new Contract(address, abi, provider)
// In v5 it was necessary to specify the fully-qualified normalized
// signature to access the desired method. For example:
contract["foo(address)"](addr)
// These would fail, since there signature is not normalized:
contract["foo(address )"](addr)
contract["foo(address addr)"](addr)
// This would fail, since the method is ambiguous:
contract.foo(addr)
_code: contracts in v6 @lang<script>
abi = [
"function foo(address bar)",
"function foo(uint160 bar)",
]
contract = new Contract(address, abi, provider)
// Any of these work fine:
contract["foo(address)"](addr)
contract["foo(address )"](addr)
contract["foo(address addr)"](addr)
// This still fails, since there is no way to know which
// method was intended
contract.foo(addr)
// However, the Typed API makes things a bit easier, since it
// allows providing typing information to the Contract:
contract.foo(Typed.address(addr))
_heading: Other Method Operations
In v5, contracts contained a series of method buckets, which
then in turn had all signatures and non-ambiguous names
attached to them to perform less-common operations.
In v6, the methods each have their own less-common operations
attached directly to them.
_code: other operations in v5 @lang<script>
// The default action chooses send or call base on method
// type (pure, view, constant, non-payable or payable)
contract.foo(addr)
// This would perform the default action, but return a Result
// object, instead of destructing the value
contract.functions.foo(addr)
// Forces using call
contract.staticCall.foo(addr)
// Estimate the gas
contract.estimateGas.foo(addr)
// Populate a transaction
contract.populateTransaction.foo(addr)
_code: other operations in v6 @lang<script>
// Still behaves the same
contract.foo(addr)
// Perform a call, returning a Result object directly
contract.foo.staticCallResult(addr)
// Forces using call (even for payable and non-payable)
contract.foo.staticCall(addr)
// Forces sending a transaction (even for pure and view)
contract.foo.send(addr)
// Estimate the gas
contract.foo.estimateGas(addr)
// Populate a transaction
contract.foo.populateTransaction(addr)
_subsection: Importing @<migrate-importing>
In v5, the project was maintained as a large set of sub-packages
managed as a monorepo.
In v6 all imports are available in the root package, and for those
who wish to have finer-grained control, the ``pkg.exports`` makes
certain folders avilable directly.
_code: importing in v5 @lang<script>
// Many things (but not all) we available on the root package
import { ethers } from "ethers"
// But some packages were grouped behind an additional property
import { providers } from "ethers"
const { InfuraProvider } = providers
// For granular control, importing from the sub-package
// was necessary
import { InfuraProvider } from "@ethersproject/providers"
_code: importing in v6 @lang<script>
// Everything is available on the root package
import { ethers } from "ethers"
import { InfuraProvider } from "ethers"
// The pkg.exports provides granular access
import { InfuraProvider } from "ethers/providers"