これは Ethers に関する非常に短い紹介ですが、開発者が通常実行する必要がある多くの操作をカバーしています。
Ethers の取得#
まず Ethers パッケージをインストールします。
npm を使用する場合:
npm i ethers
yarn を使用する場合:
yarn add ethers
pnpm を使用する場合:
pnpm i ethers
Ethers のすべては、そのルートディレクトリと ethers オブジェクトからエクスポートされます。 package.json には、より細かいインポート設定も含まれています。
通常、この文書はコード例が ethers からのすべてのエクスポートオブジェクトをインポートしていることを前提としていますが、必要なオブジェクトを任意の方法でインポートすることができます。
Node.js でのインポート:
// すべてをインポート
import { ethers } from "ethers";
// 一部のオブジェクトのみをインポート
import { BrowserProvider, parseUnits } from "ethers";
// 特定のモジュールからオブジェクトをインポート
import { HDNodeWallet } from "ethers/wallet";
ブラウザで ESM をインポート:
<script type="module">
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.min.js";
// ここにあなたのコードを書く...
</script>
一部の一般的な用語#
この分野に入るためには、利用可能なオブジェクトのタイプとその責任を理解することが有用です。
Provider#
Provider はブロックチェーンとの読み取り専用接続を確立し、アカウント、ブロック、またはトランザクションの詳細を照会したり、イベントログを照会したり、call
を使用して読み取り専用コードを評価したりすることを可能にします。
Web3.js に慣れている場合、Provider が読み書きアクセスを提供することに慣れているはずです。しかし、Ethers では、すべての書き込み操作は別のオブジェクトである Signer にさらに抽象化されています。
Signer#
Signer はアカウントとのインタラクションに関するすべての操作をカプセル化します。アカウントは通常、特定の場所にプライベートキーを持ち、さまざまなタイプのペイロードに署名するために使用されます。
プライベートキーはメモリ内にある場合もあれば、MetaMaskのような IPC 層を介して保護される場合もあります。これにより、ウェブサイトがプライベートキーを取得することはなく、ユーザーの承認を要求する際にのみインタラクションが許可されます。
Transaction#
ブロックチェーンの状態を変更するには、トランザクションを実行する必要があり、トランザクションを実行するたびに手数料を支払う必要があります。ここでの「手数料」は、トランザクションを実行するために必要な関連コスト(ディスクの読み取りや数学的演算の実行など)と、更新情報を保存するコストを含みます。
トランザクションがロールバックされた場合でも、手数料は支払う必要があります。なぜなら、検証者はそのトランザクションがロールバックされたかどうかを判断するためにリソースを費やさなければならず、その失敗の詳細は記録されるからです。
トランザクションには、他のユーザーに Ether を送信すること、コントラクトをデプロイすること、またはコントラクトの状態変更操作が含まれます。
Contract#
コントラクトは、ブロックチェーンにデプロイされたプログラムであり、読み書き操作が可能なストレージユニットを割り当てるいくつかのコードを含んでいます。
Provider に接続すると、そこから読み取ることができます。Signer に接続すると、状態変更操作を呼び出すことができます。
Receipt#
トランザクションがブロックチェーンに提出されると、それはメモリプール(mempool)に置かれ、検証者がそれを含めることを決定するまで待機します。
トランザクションの変更は、それがブロックチェーンに含まれ、レシートが生成されたときにのみ有効になります。この時点で、トランザクションの詳細を含むレシートが提供されます。たとえば、レシートが属するブロック、実際に支払った金額、使用した燃料の総量、トランザクションによって引き起こされたすべてのイベント、およびそれが成功したかロールバックされたかなどです。
Ethereum への接続#
Ethereum とインタラクションする最初のステップは、Provider を使用して接続を確立することです。
MetaMask(および他のインジェクター)#
Ethereum 上での開発を試みる最も迅速かつ簡単な方法は、MetaMaskを使用することです。これは、オブジェクトをウィンドウに注入し、次のことを提供するブラウザ拡張機能です:
- Ethereum ネットワークへの読み取り専用アクセス(Provider)
- プライベートキーに基づく書き込みアクセスの承認(Signer)
トランザクションを送信するなどの承認メソッドを要求する際、MetaMask はユーザーに承認を求めるポップアップウィンドウを表示します。
let signer = null;
let provider;
if (window.ethereum == null) {
// MetaMaskがインストールされていない場合、デフォルトのProviderを使用します。
// これは、プライベートキーをインストールしていない複数のサードパーティサービスによってサポートされています。
// したがって、読み取り専用アクセスのみです。
console.log("MetaMask not installed; using read-only defaults")
provider = ethers.getDefaultProvider()
} else {
// MetaMaskのEIP-1193オブジェクトに接続します。これは標準の
// プロトコルであり、EthersがMetaMaskを介してすべての読み取りリクエストを行うことを可能にします。
provider = new ethers.BrowserProvider(window.ethereum)
// これにより、書き込み操作のアクセスを要求する機会も提供されます。
// これらの操作は、MetaMaskがユーザーのプライベートキーを管理します。
signer = await provider.getSigner();
}
カスタム RPC バックエンド#
独自の Ethereum ノード(たとえば、Geth)を実行している場合や、カスタムサードパーティサービス(たとえば、INFURA)を使用している場合は、JsonRpcProvider を介して直接通信できます。彼らは link-jsonrpc プロトコルを使用します。
独自の Ethereum ノードを使用する場合や、HardhatやGanacheのようなブロックチェーンプロジェクトを開発している場合は、JsonRpcProvider.getSigner
メソッドを使用してアクセスを取得できます。
JSON-RPC URL に接続
// urlを提供しない場合、ほとんどのデフォルトノードに接続します。
// http://localhost:8545。
provider = new ethers.JsonRpcProvider(url)
// signerを取得してアカウントに書き込みアクセスを取得します
signer = await provider.getSigner()
ユーザーインタラクション#
Ethereum では、すべての単位は整数値である傾向があります。なぜなら、小数や浮動小数点数を使用すると、数学的操作を実行する際に不正確で不明瞭な結果が生じるからです。
したがって、内部で使用される単位(たとえば、wei)は、機械可読の目的と数学的計算に適しており、通常非常に大きく、人間が読むには適していません。
たとえば、ドルとセントを処理することを想像してください。"$ 2.56" のような値を表示します。ブロックチェーンの世界では、すべての値をセント(cents)として保持し、内部的には 256 セントとして表現します。
したがって、ユーザーが入力したデータを受け入れる際には、それを 10 進数の文字列表現(たとえば、"2.56")から最低単位の整数表現(たとえば、256)に変換する必要があります。ユーザーに値を表示する際には、逆の操作が必要です。
Ethereum では、1Ether は10 ** 18
wei に等しく、1gwei は10 ** 9
wei に等しいため、値はすぐに非常に大きくなります。そのため、さまざまな表現間で変換を助ける便利な関数が提供されています。
// ユーザーが提供した文字列をwei値に変換
eth = parseEther("1.0")
// 1000000000000000000n
// ユーザーが提供した文字列を最大基準料金に対応するwei値に変換
feePerGas = parseUnits("4.5", "gwei")
// 4500000000n
// wei値をUIに表示するether文字列値に変換
formatEther(eth)
// '1.0'
// wei値をUIに表示するgwei文字列値に変換
formatUnits(feePerGas, "gwei")
// '4.5'
ブロックチェーンとのインタラクション#
状態の照会#
Provider を持っていると、ブロックチェーン上のデータを読み取り、照会することができます。これは、現在のアカウントの状態を照会したり、履歴ログを取得したり、コントラクトコードを見つけたりするために使用できます。
// 現在のブロック数を確認
await provider.getBlockNumber()
// 16988474
// アドレスまたはENS不変ドメインに対応する現在の残高を取得
balance = await provider.getBalance("ethers.eth")
// 182334002436162568n
// 残高はwei単位であるため、ether形式で表示したい場合があります。
formatEther(balance)
// '0.182334002436162568'
// 送信トランザクションに必要な次のnonceを取得
await provider.getTransactionCount("ethers.eth")
// 3
トランザクションの送信#
ブロックチェーンに内容を書き込むには、特定のアカウントのプライベートキーへのアクセスが必要です。ほとんどの場合、これらのプライベートキーには直接アクセスできず、Signer を介してリクエストを発行し、その後、ユーザーに承認または拒否を求める厳格なアクセスを提供するサービス(たとえば、MetaMask)に委任されます。
// トランザクションを送信する際、valueはwei単位であるため、parseEther
// はetherをweiに変換します。
tx = await signer.sendTransaction({
to: "ethers.eth",
value: parseEther("1.0")
});
// 通常、トランザクションが掘削されるのを待ちたいと思うでしょう
receipt = await tx.wait();
コントラクト#
Contract はメタクラスであり、これはその定義が渡された ABI(アプリケーションバイナリインターフェース)に基づいて実行時に派生され、利用可能なメソッドと属性を決定することを意味します。
アプリケーションバイナリインターフェース(ABI)
ブロックチェーン上で発生するすべての操作はバイナリデータとしてエンコードされる必要があるため、一般的なオブジェクト(たとえば、文字列や数字)とそのバイナリ表現との間でどのように変換するか、呼び出しをエンコードし、コントラクトを解釈する方法を定義する簡潔な方法が必要です。
使用するメソッド、イベント、またはエラーのいずれかについては、Ethers がリクエストをエンコードし、結果をデコードする方法を知るための Fragment(フラグメント)が含まれている必要があります。不要なメソッドやイベントは安全に除外できます。
ABI を記述するための一般的な形式がいくつかあります。Solidity コンパイラは通常、JSON 表現をダンプする傾向がありますが、ABI を手動で入力する場合は、通常、人間が読みやすい ABI 表現を使用する方が簡単であり、これは Solidity の署名に過ぎません。
簡略化された ERC-20 ABI の例:
abi = [
"function decimals() returns (string)",
"function symbol() returns (string)",
"function balanceOf(address addr) returns (uint)"
]
// コントラクトを作成
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
読み取り専用メソッド(すなわち:"view" および "pure" メソッド)#
読み取り専用メソッドは、ブロックチェーンの状態を変更できないメソッドですが、通常、コントラクトに関連する重要なデータを取得するための簡単なインターフェースを提供します。
以下は DAI ERC-20 コントラクトを読み取る例です:
// コントラクトABI(私たちが関心のあるフラグメント)
abi = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function balanceOf(address a) view returns (uint)"
]
// コントラクトを作成;Providerに接続するため、読み取り専用メソッド(viewおよびpure)のみアクセスできます
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// シンボル名
sym = await contract.symbol()
// 'DAI'
// トークンの小数点以下の桁数
decimals = await contract.decimals()
// 18n
// アカウントのトークン残高を読み取る
balance = await contract.balanceOf("ethers.eth")
// 201469770000000000000000n
// 残高を人間が読みやすい形式にフォーマットし、UIに表示
formatUnits(balance, decimals)
// '201469.77'
状態変更メソッド#
ERC-20 コントラクトの状態を変更します。
abi = [
"function transfer(address to, uint amount)"
]
// Signerに接続;状態を変更するトランザクションを発行できます。
// これによりアカウントのEtherが消費されます。
contract = new Contract("dai.tokens.ethers.eth", abi, signer)
// 1DAIを送信
amount = parseUnits("1.0", 18);
// トランザクションを送信
tx = await contract.transfer("ethers.eth", amount)
// 現在、このトランザクションはメモリプールに送信されましたが、
// まだ含まれていません。したがって、...
// ...トランザクションが含まれるのを待ちます。
await tx.wait()
強制呼び出し(シミュレーション)による状態変更メソッド
abi = [
"function transfer(address to, uint amount) returns (bool)"
]
// Providerにのみ接続するため、読み取りアクセスのみが必要です。
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
amount = parseUnits("1.0", 18)
// 静的呼び出しには多くの制限がありますが、通常はトランザクションを事前にチェックするために使用できます。
await contract.transfer.staticCall("ethers.eth", amount)
// true
// 他のアカウントによって発起されたトランザクションをシミュレートすることもできます。
other = new VoidSigner("0x643aA0A61eADCC9Cc202D1915D942d35D005400C")
contractAsOther = contract.connect(other.connect(provider))
await contractAsOther.transfer.staticCall("ethers.eth", amount)
// true
イベントのリスニング#
命名されたイベントのリスナーを追加すると、イベントパラメータが分解されます。リスナーには常に追加のパラメータが渡され、これは EventPayload であり、イベントに関する詳細情報(フィルターやそのリスナーを削除するためのメソッドを含む)を含みます。
ERC-20 イベントをリッスン:
abi = [
"event Transfer(address indexed from, address indexed to, uint amount)"
]
// コントラクトを作成;Providerに接続するため、読み取り専用メソッド(viewおよびpure)のみアクセスできます
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// すべての転送イベントをリッスンし始める
contract.on("Transfer", (from, to, _amount, event) => {
const amount = formatEther(_amount, 18)
console.log(`${ from } => ${ to }: ${ amount }`);
// `event.log`には全体のEventLogがあります
// オプションで、リスニングを停止するための便利なメソッド
event.removeListener();
});
// 同様に
contract.on(contract.filters.Transfer, (from, to, amount, event) => {
// 上記を参照
})
// "ethers.eth"への転送イベントをリッスン
filter = contract.filters.Transfer("ethers.eth")
contract.on(filter, (from, to, amount, event) => {
// `to`は常に"ethers.eth"のアドレスと等しくなります。
});
// ABIに存在するかどうかに関係なく、すべてのイベントをリッスンします。未知のイベントがキャッチされる可能性があるため、パラメータは分解されません。
contract.on("*", (event) => {
// `event.log`には全体のEventLogがあります
});
歴史的イベントの照会#
広範なブロック内で照会する際、特定のバックエンドは速度が遅く、エラーを返したり、サーバーのポリシーに応じて結果を提供しない場合があります。
ERC-20 の歴史的イベントを照会:
abi = [
"event Transfer(address indexed from, address indexed to, uint amount)"
]
// コントラクトを作成;Providerに接続するため、読み取り専用メソッド(viewおよびpure)のみアクセスできます
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// 最近の100ブロック内で転送があったかどうかを照会
filter = contract.filters.Transfer
events = await contract.queryFilter(filter, -100)
// イベント配列のサイズを取得
events.length
// 90
// 最初の一致するイベント
events[0]
// EventLog { ... }
// すべての期間にわたる"ethers.eth"への転送イベントを照会
filter = contract.filters.Transfer("ethers.eth")
events = await contract.queryFilter(filter)
// 最初の一致するイベント
events[0]
// EventLog { ... }
メッセージの署名#
プライベートキーは多くのことができます。トランザクションに署名してそれを承認するだけでなく、他の形式のデータに署名するためにも使用でき、そのデータを検証し、他の目的に達することができます。
たとえば、メッセージに署名することでアカウントの所有権を証明でき、ウェブサイトはそれを使用してユーザーを検証し、ログインできます。
// 私たちの署名者;メッセージに署名するためにProviderは必要ありません
signer = new Wallet(id("test"))
// Wallet {
// address: '0xC08B5542D177ac6686946920409741463a15dDdB',
// provider: null
// }
message = "ethers.orgにサインインしますか?"
// メッセージに署名
sig = await signer.signMessage(message);
// '0xefc6e1d2f21bb22b1013d05ecf1f06fd73cdcb34388111e4deec58605f3667061783be1297d8e3bee955d5b583bac7b26789b4a4c12042d59799ca75d98d23a51c'
// メッセージを検証;アドレスが署名者と一致することに注意
verifyMessage(message, sig)
// '0xC08B5542D177ac6686946920409741463a15dDdB'
署名されたメッセージの上に構築された多くの他の高度なプロトコルは、1 つのプライベートキーが他のユーザーにトークンの移転を承認することを可能にし、他の人が移転のトランザクション手数料を支払うことを許可します。