feat: add ens support

This commit is contained in:
Georgios Konstantopoulos 2020-05-27 14:32:44 +03:00
parent f42aaf2588
commit bd6fee59cb
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
5 changed files with 342 additions and 73 deletions

2
Cargo.lock generated
View File

@ -299,9 +299,11 @@ dependencies = [
"ethers-types 0.1.0", "ethers-types 0.1.0",
"ethers-utils 0.1.0", "ethers-utils 0.1.0",
"reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)", "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 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)", "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)", "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)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -15,3 +15,7 @@ serde = { version = "1.0.110", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.53", default-features = false } serde_json = { version = "1.0.53", default-features = false }
thiserror = { version = "1.0.19", default-features = false } thiserror = { version = "1.0.19", default-features = false }
url = { version = "2.1.1", 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"] }

View File

@ -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<T: Into<Address>>(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<T: Into<Address>>(
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::<Vec<u8>>().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);
}
}
}

View File

@ -7,6 +7,9 @@
mod http; mod http;
mod provider; mod provider;
/// ENS support
pub mod ens;
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{error::Error, fmt::Debug}; use std::{error::Error, fmt::Debug};

View File

@ -1,10 +1,12 @@
use crate::{ens, http::Provider as HttpProvider, JsonRpcClient};
use ethers_abi::{Detokenize, ParamType};
use ethers_types::{ use ethers_types::{
Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Transaction, TransactionReceipt, Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Selector, Transaction,
TransactionRequest, TxHash, U256, TransactionReceipt, TransactionRequest, TxHash, U256,
}; };
use ethers_utils as utils; use ethers_utils as utils;
use crate::{http::Provider as HttpProvider, JsonRpcClient};
use serde::Deserialize; use serde::Deserialize;
use url::{ParseError, Url}; use url::{ParseError, Url};
@ -13,55 +15,33 @@ use std::{convert::TryFrom, fmt::Debug};
/// An abstract provider for interacting with the [Ethereum JSON RPC /// An abstract provider for interacting with the [Ethereum JSON RPC
/// API](https://github.com/ethereum/wiki/wiki/JSON-RPC) /// API](https://github.com/ethereum/wiki/wiki/JSON-RPC)
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Provider<P>(P); pub struct Provider<P>(P, Option<Address>);
// JSON RPC bindings // JSON RPC bindings
impl<P: JsonRpcClient> Provider<P> { impl<P: JsonRpcClient> Provider<P> {
/// Gets the current gas price as estimated by the node ////// Blockchain Status
pub async fn get_gas_price(&self) -> Result<U256, P::Error> { //
self.0.request("eth_gasPrice", None::<()>).await // Functions for querying the state of the blockchain
}
/// Tries to estimate the gas for the transaction
pub async fn estimate_gas(
&self,
tx: &TransactionRequest,
block: Option<BlockNumber>,
) -> Result<U256, P::Error> {
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<Vec<Log>, P::Error> {
self.0.request("eth_getLogs", Some(filter)).await
}
/// Gets the accounts on the node
pub async fn get_accounts(&self) -> Result<Vec<Address>, P::Error> {
self.0.request("eth_accounts", None::<()>).await
}
/// Gets the latest block number via the `eth_BlockNumber` API /// Gets the latest block number via the `eth_BlockNumber` API
pub async fn get_block_number(&self) -> Result<U256, P::Error> { pub async fn get_block_number(&self) -> Result<U256, P::Error> {
self.0.request("eth_blockNumber", None::<()>).await self.0.request("eth_blockNumber", None::<()>).await
} }
pub async fn get_block(&self, id: impl Into<BlockId>) -> Result<Block<TxHash>, P::Error> { /// Gets the block at `block_hash_or_number` (transaction hashes only)
self.get_block_gen(id.into(), false).await pub async fn get_block(
&self,
block_hash_or_number: impl Into<BlockId>,
) -> Result<Block<TxHash>, 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( pub async fn get_block_with_txs(
&self, &self,
id: impl Into<BlockId>, block_hash_or_number: impl Into<BlockId>,
) -> Result<Block<Transaction>, P::Error> { ) -> Result<Block<Transaction>, 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<Tx: for<'a> Deserialize<'a>>( async fn get_block_gen<Tx: for<'a> Deserialize<'a>>(
@ -85,52 +65,37 @@ impl<P: JsonRpcClient> Provider<P> {
} }
} }
/// Gets the transaction receipt for tx hash /// Gets the transaction with `transaction_hash`
pub async fn get_transaction<T: Send + Sync + Into<TxHash>>(
&self,
transaction_hash: T,
) -> Result<Transaction, P::Error> {
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<T: Send + Sync + Into<TxHash>>( pub async fn get_transaction_receipt<T: Send + Sync + Into<TxHash>>(
&self, &self,
hash: T, transaction_hash: T,
) -> Result<TransactionReceipt, P::Error> { ) -> Result<TransactionReceipt, P::Error> {
let hash = hash.into(); let hash = transaction_hash.into();
self.0 self.0
.request("eth_getTransactionReceipt", Some(hash)) .request("eth_getTransactionReceipt", Some(hash))
.await .await
} }
/// Gets the transaction which matches the provided hash via the `eth_getTransactionByHash` API /// Gets the current gas price as estimated by the node
pub async fn get_transaction<T: Send + Sync + Into<TxHash>>( pub async fn get_gas_price(&self) -> Result<U256, P::Error> {
&self, self.0.request("eth_gasPrice", None::<()>).await
hash: T,
) -> Result<Transaction, P::Error> {
let hash = hash.into();
self.0.request("eth_getTransactionByHash", Some(hash)).await
} }
// State mutations /// Gets the accounts on the node
pub async fn get_accounts(&self) -> Result<Vec<Address>, P::Error> {
/// Broadcasts the transaction request via the `eth_sendTransaction` API self.0.request("eth_accounts", None::<()>).await
pub async fn call(
&self,
tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<Bytes, P::Error> {
let tx = utils::serialize(&tx);
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
self.0.request("eth_call", Some(vec![tx, block])).await
} }
/// Broadcasts the transaction request via the `eth_sendTransaction` API /// Returns the nonce of the address
pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> {
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<TxHash, P::Error> {
let rlp = utils::serialize(&tx.rlp());
self.0.request("eth_sendRawTransaction", Some(rlp)).await
}
// Account state
pub async fn get_transaction_count( pub async fn get_transaction_count(
&self, &self,
from: Address, from: Address,
@ -143,6 +108,7 @@ impl<P: JsonRpcClient> Provider<P> {
.await .await
} }
/// Returns the account's balance
pub async fn get_balance( pub async fn get_balance(
&self, &self,
from: Address, from: Address,
@ -152,12 +118,207 @@ impl<P: JsonRpcClient> Provider<P> {
let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest)); let block = utils::serialize(&block.unwrap_or(BlockNumber::Latest));
self.0.request("eth_getBalance", Some(&[from, block])).await 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<BlockNumber>,
) -> Result<Bytes, P::Error> {
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<BlockNumber>,
) -> Result<U256, P::Error> {
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<TxHash, P::Error> {
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<TxHash, P::Error> {
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<Vec<Log>, 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<Option<Address>, 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<Option<String>, P::Error> {
let ens_name = ens::reverse_address(address);
self.query_resolver(ParamType::String, &ens_name, ens::NAME_SELECTOR)
.await
}
async fn query_resolver<T: Detokenize>(
&self,
param: ParamType,
ens_name: &str,
selector: Selector,
) -> Result<Option<T>, 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<T: Into<Address>>(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<T: Detokenize>(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<HttpProvider> { impl TryFrom<&str> for Provider<HttpProvider> {
type Error = ParseError; type Error = ParseError;
fn try_from(src: &str) -> Result<Self, Self::Error> { fn try_from(src: &str) -> Result<Self, Self::Error> {
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::<Address>()
.unwrap();
let provider = Provider::<HttpProvider>::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::<Address>()
.unwrap();
let provider = Provider::<HttpProvider>::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());
} }
} }