From bd6fee59cbb95ede977445784a9678ed7b8d2964 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 27 May 2020 14:32:44 +0300 Subject: [PATCH] feat: add ens support --- Cargo.lock | 2 + crates/ethers-providers/Cargo.toml | 4 + crates/ethers-providers/src/ens.rs | 99 ++++++++ crates/ethers-providers/src/lib.rs | 3 + crates/ethers-providers/src/provider.rs | 307 ++++++++++++++++++------ 5 files changed, 342 insertions(+), 73 deletions(-) create mode 100644 crates/ethers-providers/src/ens.rs diff --git a/Cargo.lock b/Cargo.lock index 009dcc46..583a1b73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,9 +299,11 @@ dependencies = [ "ethers-types 0.1.0", "ethers-utils 0.1.0", "reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-hex 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/crates/ethers-providers/Cargo.toml b/crates/ethers-providers/Cargo.toml index 8e4ff8a3..9234c3c5 100644 --- a/crates/ethers-providers/Cargo.toml +++ b/crates/ethers-providers/Cargo.toml @@ -15,3 +15,7 @@ serde = { version = "1.0.110", default-features = false, features = ["derive"] } serde_json = { version = "1.0.53", default-features = false } thiserror = { version = "1.0.19", default-features = false } url = { version = "2.1.1", default-features = false } + +[dev-dependencies] +rustc-hex = "2.1.0" +tokio = { version = "0.2.21", features = ["macros"] } diff --git a/crates/ethers-providers/src/ens.rs b/crates/ethers-providers/src/ens.rs new file mode 100644 index 00000000..29df4d83 --- /dev/null +++ b/crates/ethers-providers/src/ens.rs @@ -0,0 +1,99 @@ +// Adapted from https://github.com/hhatto/rust-ens/blob/master/src/lib.rs +use ethers_types::{Address, Selector, TransactionRequest, H256}; +use ethers_utils::keccak256; + +// Selectors + +const ENS_REVERSE_REGISTRAR_DOMAIN: &str = "addr.reverse"; + +// resolver(bytes32) +const RESOLVER: Selector = [1, 120, 184, 191]; + +// addr(bytes32) +pub const ADDR_SELECTOR: Selector = [59, 59, 87, 222]; + +// name(bytes32) +pub const NAME_SELECTOR: Selector = [105, 31, 52, 49]; + +/// Returns a transaction request for calling the `resolver` method on the ENS server +pub fn get_resolver>(ens_address: T, name: &str) -> TransactionRequest { + // keccak256('resolver(bytes32)') + let data = [&RESOLVER[..], &namehash(name).0].concat(); + TransactionRequest { + data: Some(data.into()), + to: Some(ens_address.into()), + ..Default::default() + } +} + +pub fn resolve>( + resolver_address: T, + selector: Selector, + name: &str, +) -> TransactionRequest { + let data = [&selector[..], &namehash(name).0].concat(); + TransactionRequest { + data: Some(data.into()), + to: Some(resolver_address.into()), + ..Default::default() + } +} + +pub fn reverse_address(addr: Address) -> String { + format!("{:?}.{}", addr, ENS_REVERSE_REGISTRAR_DOMAIN)[2..].to_string() +} + +/// Returns the ENS namehash as specified in [EIP-137](https://eips.ethereum.org/EIPS/eip-137) +pub fn namehash(name: &str) -> H256 { + if name.is_empty() { + return H256::zero(); + } + + // iterate in reverse + name.rsplit('.') + .fold([0u8; 32], |node, label| { + keccak256(&[node, keccak256(label.as_bytes())].concat()) + }) + .into() +} + +#[cfg(test)] +mod tests { + use super::*; + use rustc_hex::FromHex; + + fn assert_hex(hash: H256, val: &str) { + let v = if val.starts_with("0x") { + &val[2..] + } else { + val + }; + + assert_eq!(hash.0.to_vec(), v.from_hex::>().unwrap()); + } + + #[test] + fn test_namehash() { + dbg!(ethers_utils::id("name(bytes32)")); + for (name, expected) in &[ + ( + "", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "foo.eth", + "de9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f", + ), + ( + "eth", + "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", + ), + ( + "alice.eth", + "0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec", + ), + ] { + assert_hex(namehash(name), expected); + } + } +} diff --git a/crates/ethers-providers/src/lib.rs b/crates/ethers-providers/src/lib.rs index 474c29b1..5acffe62 100644 --- a/crates/ethers-providers/src/lib.rs +++ b/crates/ethers-providers/src/lib.rs @@ -7,6 +7,9 @@ mod http; mod provider; +/// ENS support +pub mod ens; + use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::{error::Error, fmt::Debug}; diff --git a/crates/ethers-providers/src/provider.rs b/crates/ethers-providers/src/provider.rs index 8b608085..dc5db310 100644 --- a/crates/ethers-providers/src/provider.rs +++ b/crates/ethers-providers/src/provider.rs @@ -1,10 +1,12 @@ +use crate::{ens, http::Provider as HttpProvider, JsonRpcClient}; + +use ethers_abi::{Detokenize, ParamType}; use ethers_types::{ - Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Transaction, TransactionReceipt, - TransactionRequest, TxHash, U256, + Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Selector, Transaction, + TransactionReceipt, TransactionRequest, TxHash, U256, }; use ethers_utils as utils; -use crate::{http::Provider as HttpProvider, JsonRpcClient}; use serde::Deserialize; use url::{ParseError, Url}; @@ -13,55 +15,33 @@ use std::{convert::TryFrom, fmt::Debug}; /// An abstract provider for interacting with the [Ethereum JSON RPC /// API](https://github.com/ethereum/wiki/wiki/JSON-RPC) #[derive(Clone, Debug)] -pub struct Provider

(P); +pub struct Provider

(P, Option

); // JSON RPC bindings impl Provider

{ - /// Gets the current gas price as estimated by the node - pub async fn get_gas_price(&self) -> Result { - self.0.request("eth_gasPrice", None::<()>).await - } - - /// Tries to estimate the gas for the transaction - pub async fn estimate_gas( - &self, - tx: &TransactionRequest, - block: Option, - ) -> Result { - let tx = utils::serialize(tx); - - let args = match block { - Some(block) => vec![tx, utils::serialize(&block)], - None => vec![tx], - }; - - self.0.request("eth_estimateGas", Some(args)).await - } - - /// Gets the logs matching a given filter - pub async fn get_logs(&self, filter: &Filter) -> Result, P::Error> { - self.0.request("eth_getLogs", Some(filter)).await - } - - /// Gets the accounts on the node - pub async fn get_accounts(&self) -> Result, P::Error> { - self.0.request("eth_accounts", None::<()>).await - } + ////// Blockchain Status + // + // Functions for querying the state of the blockchain /// Gets the latest block number via the `eth_BlockNumber` API pub async fn get_block_number(&self) -> Result { self.0.request("eth_blockNumber", None::<()>).await } - pub async fn get_block(&self, id: impl Into) -> Result, P::Error> { - self.get_block_gen(id.into(), false).await + /// Gets the block at `block_hash_or_number` (transaction hashes only) + pub async fn get_block( + &self, + block_hash_or_number: impl Into, + ) -> Result, P::Error> { + self.get_block_gen(block_hash_or_number.into(), false).await } + /// Gets the block at `block_hash_or_number` (full transactions included) pub async fn get_block_with_txs( &self, - id: impl Into, + block_hash_or_number: impl Into, ) -> Result, P::Error> { - self.get_block_gen(id.into(), true).await + self.get_block_gen(block_hash_or_number.into(), true).await } async fn get_block_gen Deserialize<'a>>( @@ -85,52 +65,37 @@ impl Provider

{ } } - /// Gets the transaction receipt for tx hash + /// Gets the transaction with `transaction_hash` + pub async fn get_transaction>( + &self, + transaction_hash: T, + ) -> Result { + let hash = transaction_hash.into(); + self.0.request("eth_getTransactionByHash", Some(hash)).await + } + + /// Gets the transaction receipt with `transaction_hash` pub async fn get_transaction_receipt>( &self, - hash: T, + transaction_hash: T, ) -> Result { - let hash = hash.into(); + let hash = transaction_hash.into(); self.0 .request("eth_getTransactionReceipt", Some(hash)) .await } - /// Gets the transaction which matches the provided hash via the `eth_getTransactionByHash` API - pub async fn get_transaction>( - &self, - hash: T, - ) -> Result { - let hash = hash.into(); - self.0.request("eth_getTransactionByHash", Some(hash)).await + /// Gets the current gas price as estimated by the node + pub async fn get_gas_price(&self) -> Result { + self.0.request("eth_gasPrice", None::<()>).await } - // State mutations - - /// Broadcasts the transaction request via the `eth_sendTransaction` API - pub async fn call( - &self, - tx: TransactionRequest, - block: Option, - ) -> Result { - let tx = utils::serialize(&tx); - let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); - self.0.request("eth_call", Some(vec![tx, block])).await + /// Gets the accounts on the node + pub async fn get_accounts(&self) -> Result, P::Error> { + self.0.request("eth_accounts", None::<()>).await } - /// Broadcasts the transaction request via the `eth_sendTransaction` API - pub async fn send_transaction(&self, tx: TransactionRequest) -> Result { - self.0.request("eth_sendTransaction", Some(tx)).await - } - - /// Broadcasts a raw RLP encoded transaction via the `eth_sendRawTransaction` API - pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result { - let rlp = utils::serialize(&tx.rlp()); - self.0.request("eth_sendRawTransaction", Some(rlp)).await - } - - // Account state - + /// Returns the nonce of the address pub async fn get_transaction_count( &self, from: Address, @@ -143,6 +108,7 @@ impl Provider

{ .await } + /// Returns the account's balance pub async fn get_balance( &self, from: Address, @@ -152,12 +118,207 @@ impl Provider

{ let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); self.0.request("eth_getBalance", Some(&[from, block])).await } + + ////// Contract Execution + // + // These are relatively low-level calls. The Contracts API should usually be used instead. + + /// Send the read-only (constant) transaction to a single Ethereum node and return the result (as bytes) of executing it. + /// This is free, since it does not change any state on the blockchain. + pub async fn call( + &self, + tx: TransactionRequest, + block: Option, + ) -> Result { + let tx = utils::serialize(&tx); + let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); + self.0.request("eth_call", Some(vec![tx, block])).await + } + + /// Send a transaction to a single Ethereum node and return the estimated amount of gas required (as a U256) to send it + /// This is free, but only an estimate. Providing too little gas will result in a transaction being rejected + /// (while still consuming all provided gas). + pub async fn estimate_gas( + &self, + tx: &TransactionRequest, + block: Option, + ) -> Result { + let tx = utils::serialize(tx); + + let args = match block { + Some(block) => vec![tx, utils::serialize(&block)], + None => vec![tx], + }; + + self.0.request("eth_estimateGas", Some(args)).await + } + + /// Send the transaction to the entire Ethereum network and returns the transaction's hash + /// This will consume gas from the account that signed the transaction. + pub async fn send_transaction(&self, tx: TransactionRequest) -> Result { + self.0.request("eth_sendTransaction", Some(tx)).await + } + + /// Send the raw RLP encoded transaction to the entire Ethereum network and returns the transaction's hash + /// This will consume gas from the account that signed the transaction. + pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result { + let rlp = utils::serialize(&tx.rlp()); + self.0.request("eth_sendRawTransaction", Some(rlp)).await + } + + ////// Contract state + + /// Returns an array (possibly empty) of logs that match the filter + pub async fn get_logs(&self, filter: &Filter) -> Result, P::Error> { + self.0.request("eth_getLogs", Some(filter)).await + } + + // TODO: get_code, get_storage_at + + ////// Ethereum Naming Service + // The Ethereum Naming Service (ENS) allows easy to remember and use names to + // be assigned to Ethereum addresses. Any provider operation which takes an address + // may also take an ENS name. + // + // ENS also provides the ability for a reverse lookup, which determines the name for an address if it has been configured. + + /// Returns the address that the `ens_name` resolves to (or None if not configured). + /// + /// # Panics + /// + /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as + /// an address. This should theoretically never happen. + pub async fn resolve_name(&self, ens_name: &str) -> Result, P::Error> { + self.query_resolver(ParamType::Address, ens_name, ens::ADDR_SELECTOR) + .await + } + + /// Returns the ENS name the `address` resolves to (or None if not configured). + pub async fn lookup_address(&self, address: Address) -> Result, P::Error> { + let ens_name = ens::reverse_address(address); + self.query_resolver(ParamType::String, &ens_name, ens::NAME_SELECTOR) + .await + } + + async fn query_resolver( + &self, + param: ParamType, + ens_name: &str, + selector: Selector, + ) -> Result, P::Error> { + let ens_addr = if let Some(ens_addr) = self.1 { + ens_addr + } else { + return Ok(None); + }; + + // first get the resolver responsible for this name + // the call will return a Bytes array which we convert to an address + let data = self + .call(ens::get_resolver(ens_addr, ens_name), None) + .await?; + + let resolver_address: Address = decode_bytes(ParamType::Address, data); + if resolver_address == Address::zero() { + return Ok(None); + } + + // resolve + let data = self + .call(ens::resolve(resolver_address, selector, ens_name), None) + .await?; + + Ok(Some(decode_bytes(param, data))) + } + + pub fn ens>(mut self, ens_addr: T) -> Self { + self.1 = Some(ens_addr.into()); + self + } +} + +/// infallbile conversion of Bytes to Address/String +/// +/// # Panics +/// +/// If the provided bytes were not an interpretation of an address +fn decode_bytes(param: ParamType, bytes: Bytes) -> T { + let tokens = ethers_abi::decode(&[param], &bytes.0) + .expect("could not abi-decode bytes to address tokens"); + T::from_tokens(tokens).expect("could not parse tokens as address") } impl TryFrom<&str> for Provider { type Error = ParseError; fn try_from(src: &str) -> Result { - Ok(Provider(HttpProvider::new(Url::parse(src)?))) + Ok(Provider(HttpProvider::new(Url::parse(src)?), None)) + } +} + +#[cfg(test)] +mod ens_tests { + use super::*; + + #[tokio::test] + // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2 + async fn mainnet_resolve_name() { + let mainnet_ens_addr = "00000000000C2E074eC69A0dFb2997BA6C7d2e1e" + .parse::

() + .unwrap(); + + let provider = Provider::::try_from( + "https://mainnet.infura.io/v3/9408f47dedf04716a03ef994182cf150", + ) + .unwrap() + .ens(mainnet_ens_addr); + + let addr = provider + .resolve_name("registrar.firefly.eth") + .await + .unwrap(); + assert_eq!( + addr.unwrap(), + "6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap() + ); + + // registrar not found + let addr = provider.resolve_name("asdfasdffads").await.unwrap(); + assert!(addr.is_none()); + + // name not found + let addr = provider + .resolve_name("asdfasdf.registrar.firefly.eth") + .await + .unwrap(); + assert!(addr.is_none()); + } + + #[tokio::test] + // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2 + async fn mainnet_lookup_address() { + let mainnet_ens_addr = "00000000000C2E074eC69A0dFb2997BA6C7d2e1e" + .parse::
() + .unwrap(); + + let provider = Provider::::try_from( + "https://mainnet.infura.io/v3/9408f47dedf04716a03ef994182cf150", + ) + .unwrap() + .ens(mainnet_ens_addr); + + let name = provider + .lookup_address("6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap()) + .await + .unwrap(); + + assert_eq!(name.unwrap(), "registrar.firefly.eth"); + + let name = provider + .lookup_address("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".parse().unwrap()) + .await + .unwrap(); + + assert!(name.is_none()); } }