From 3186673a2e5d9df0d7b39dee0fd75057975da2d0 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 27 May 2020 23:43:02 +0300 Subject: [PATCH] make ENS a first class citizen --- crates/ethers-abi/src/tokens.rs | 2 +- crates/ethers-contract/src/contract.rs | 4 +- crates/ethers-providers/src/ens.rs | 6 +-- crates/ethers-providers/src/provider.rs | 14 +++++- crates/ethers-signers/src/client.rs | 12 ++++- crates/ethers-types/src/ens.rs | 60 +++++++++++++++++++++++++ crates/ethers-types/src/keys.rs | 23 ++++++++-- crates/ethers-types/src/lib.rs | 3 ++ crates/ethers-types/src/log.rs | 6 +-- crates/ethers-types/src/transaction.rs | 12 ++--- crates/ethers/examples/ens.rs | 31 +++++++++++++ 11 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 crates/ethers-types/src/ens.rs create mode 100644 crates/ethers/examples/ens.rs diff --git a/crates/ethers-abi/src/tokens.rs b/crates/ethers-abi/src/tokens.rs index 66358e2c..16fb42fa 100644 --- a/crates/ethers-abi/src/tokens.rs +++ b/crates/ethers-abi/src/tokens.rs @@ -1,6 +1,6 @@ //! Contract Functions Output types. //! Adapted from: https://github.com/tomusdrw/rust-web3/blob/master/src/contract/tokens.rs -#[allow(clippy::all)] +#![allow(clippy::all)] use crate::Token; use arrayvec::ArrayVec; diff --git a/crates/ethers-contract/src/contract.rs b/crates/ethers-contract/src/contract.rs index 5c9ba69e..72edf1e0 100644 --- a/crates/ethers-contract/src/contract.rs +++ b/crates/ethers-contract/src/contract.rs @@ -3,7 +3,7 @@ use crate::{ContractCall, Event}; use ethers_abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}; use ethers_providers::{networks::Network, JsonRpcClient}; use ethers_signers::{Client, Signer}; -use ethers_types::{Address, Filter, Selector, TransactionRequest}; +use ethers_types::{Address, Filter, NameOrAddress, Selector, TransactionRequest}; use rustc_hex::ToHex; use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; @@ -93,7 +93,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> { // create the tx object let tx = TransactionRequest { - to: Some(self.address), + to: Some(NameOrAddress::Address(self.address)), data: Some(data.into()), ..Default::default() }; diff --git a/crates/ethers-providers/src/ens.rs b/crates/ethers-providers/src/ens.rs index 48a5254d..40e712ce 100644 --- a/crates/ethers-providers/src/ens.rs +++ b/crates/ethers-providers/src/ens.rs @@ -1,5 +1,5 @@ // Adapted from https://github.com/hhatto/rust-ens/blob/master/src/lib.rs -use ethers_types::{Address, Selector, TransactionRequest, H256}; +use ethers_types::{Address, NameOrAddress, Selector, TransactionRequest, H256}; use ethers_utils::keccak256; // Selectors @@ -21,7 +21,7 @@ pub fn get_resolver>(ens_address: T, name: &str) -> Transaction let data = [&RESOLVER[..], &namehash(name).0].concat(); TransactionRequest { data: Some(data.into()), - to: Some(ens_address.into()), + to: Some(NameOrAddress::Address(ens_address.into())), ..Default::default() } } @@ -34,7 +34,7 @@ pub fn resolve>( let data = [&selector[..], &namehash(name).0].concat(); TransactionRequest { data: Some(data.into()), - to: Some(resolver_address.into()), + to: Some(NameOrAddress::Address(resolver_address.into())), ..Default::default() } } diff --git a/crates/ethers-providers/src/provider.rs b/crates/ethers-providers/src/provider.rs index 654f4da2..2aa6639e 100644 --- a/crates/ethers-providers/src/provider.rs +++ b/crates/ethers-providers/src/provider.rs @@ -2,7 +2,7 @@ use crate::{ens, http::Provider as HttpProvider, networks::Network, JsonRpcClien use ethers_abi::{Detokenize, ParamType}; use ethers_types::{ - Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, Selector, Transaction, + Address, Block, BlockId, BlockNumber, Bytes, Filter, Log, NameOrAddress, Selector, Transaction, TransactionReceipt, TransactionRequest, TxHash, U256, }; use ethers_utils as utils; @@ -155,7 +155,17 @@ impl Provider { /// 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 { + pub async fn send_transaction(&self, mut tx: TransactionRequest) -> Result { + if let Some(ref to) = tx.to { + if let NameOrAddress::Name(ens_name) = to { + let addr = self + .resolve_name(&ens_name) + .await? + .expect("TODO: Handle ENS name not found"); + tx.to = Some(addr.into()) + } + } + self.0.request("eth_sendTransaction", Some(tx)).await } diff --git a/crates/ethers-signers/src/client.rs b/crates/ethers-signers/src/client.rs index 08a9addd..2b42eeed 100644 --- a/crates/ethers-signers/src/client.rs +++ b/crates/ethers-signers/src/client.rs @@ -1,7 +1,7 @@ use crate::Signer; use ethers_providers::{networks::Network, JsonRpcClient, Provider}; -use ethers_types::{Address, BlockNumber, TransactionRequest, TxHash}; +use ethers_types::{Address, BlockNumber, NameOrAddress, TransactionRequest, TxHash}; use std::ops::Deref; @@ -33,6 +33,16 @@ where mut tx: TransactionRequest, block: Option, ) -> Result { + if let Some(ref to) = tx.to { + if let NameOrAddress::Name(ens_name) = to { + let addr = self + .resolve_name(&ens_name) + .await? + .expect("TODO: Handle ENS name not found"); + tx.to = Some(addr.into()) + } + } + // if there is no local signer, then the transaction should use the // node's signer which should already be unlocked let signer = if let Some(ref signer) = self.signer { diff --git a/crates/ethers-types/src/ens.rs b/crates/ethers-types/src/ens.rs new file mode 100644 index 00000000..1a337582 --- /dev/null +++ b/crates/ethers-types/src/ens.rs @@ -0,0 +1,60 @@ +use crate::Address; +use rlp::{Encodable, RlpStream}; +use serde::{ser::Error as SerializationError, Deserialize, Deserializer, Serialize, Serializer}; + +/// ENS name or Ethereum Address. Not RLP encoded/serialized if it's a name +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NameOrAddress { + Name(String), + Address(Address), +} + +// Only RLP encode the Address variant since it doesn't make sense to ever RLP encode +// an ENS name +impl Encodable for &NameOrAddress { + fn rlp_append(&self, s: &mut RlpStream) { + if let NameOrAddress::Address(inner) = self { + inner.rlp_append(s); + } + } +} + +impl From<&str> for NameOrAddress { + fn from(s: &str) -> Self { + NameOrAddress::Name(s.to_owned()) + } +} + +impl From
for NameOrAddress { + fn from(s: Address) -> Self { + NameOrAddress::Address(s) + } +} + +// Only serialize the Address variant since it doesn't make sense to ever serialize +// an ENS name +impl Serialize for NameOrAddress { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + NameOrAddress::Address(addr) => addr.serialize(serializer), + NameOrAddress::Name(name) => Err(SerializationError::custom(format!( + "cannot serialize ENS name {}, must be address", + name + ))), + } + } +} + +impl<'de> Deserialize<'de> for NameOrAddress { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let inner = Address::deserialize(deserializer)?; + + Ok(NameOrAddress::Address(inner)) + } +} diff --git a/crates/ethers-types/src/keys.rs b/crates/ethers-types/src/keys.rs index 6c088699..f80e1989 100644 --- a/crates/ethers-types/src/keys.rs +++ b/crates/ethers-types/src/keys.rs @@ -1,4 +1,4 @@ -use crate::{Address, Signature, Transaction, TransactionRequest, H256, U256, U64}; +use crate::{Address, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256, U64}; use ethers_utils::{hash_message, keccak256}; use rand::Rng; @@ -63,6 +63,11 @@ impl PrivateKey { /// /// This will return an error if called if any of the `nonce`, `gas_price` or `gas` /// fields are not populated. + /// + /// # Panics + /// + /// If `tx.to` is an ENS name. The caller MUST take care of naem resolution before + /// calling this function. pub fn sign_transaction( &self, tx: TransactionRequest, @@ -82,11 +87,18 @@ impl PrivateKey { let rlp = tx.rlp_signed(&signature); let hash = keccak256(&rlp.0); + let to = tx.to.map(|to| match to { + NameOrAddress::Address(inner) => inner, + NameOrAddress::Name(_) => { + panic!("Expected `to` to be an Ethereum Address, not an ENS name") + } + }); + Ok(Transaction { hash: hash.into(), nonce, from: self.into(), - to: tx.to, + to, value: tx.value.unwrap_or_default(), gas_price, gas, @@ -217,7 +229,12 @@ mod tests { // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction let tx = TransactionRequest { from: None, - to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), + to: Some( + "F0109fC8DF283027b6285cc889F5aA624EaC1F55" + .parse::
() + .unwrap() + .into(), + ), value: Some(1_000_000_000.into()), gas: Some(2_000_000.into()), nonce: Some(0.into()), diff --git a/crates/ethers-types/src/lib.rs b/crates/ethers-types/src/lib.rs index dc81dbb5..c133bd57 100644 --- a/crates/ethers-types/src/lib.rs +++ b/crates/ethers-types/src/lib.rs @@ -24,6 +24,9 @@ pub use block::{Block, BlockId, BlockNumber}; mod log; pub use log::{Filter, Log, ValueOrArray}; +mod ens; +pub use ens::NameOrAddress; + // re-export the non-standard rand version so that other crates don't use the // wrong one by accident pub use rand; diff --git a/crates/ethers-types/src/log.rs b/crates/ethers-types/src/log.rs index e0fd9127..4e3b573b 100644 --- a/crates/ethers-types/src/log.rs +++ b/crates/ethers-types/src/log.rs @@ -89,16 +89,16 @@ pub struct Filter { impl Filter { pub fn new() -> Self { - let filter = Self::default(); - // filter.topics = vec![H256::zero().into(); 4]; - filter + Self::default() } + #[allow(clippy::wrong_self_convention)] pub fn from_block>(mut self, block: T) -> Self { self.from_block = Some(block.into()); self } + #[allow(clippy::wrong_self_convention)] pub fn to_block>(mut self, block: T) -> Self { self.to_block = Some(block.into()); self diff --git a/crates/ethers-types/src/transaction.rs b/crates/ethers-types/src/transaction.rs index 0e5c862e..63acbf9c 100644 --- a/crates/ethers-types/src/transaction.rs +++ b/crates/ethers-types/src/transaction.rs @@ -1,5 +1,5 @@ //! Transaction types -use crate::{Address, Bloom, Bytes, Log, Signature, H256, U256, U64}; +use crate::{Address, Bloom, Bytes, Log, NameOrAddress, Signature, H256, U256, U64}; use ethers_utils::keccak256; use rlp::RlpStream; @@ -40,7 +40,7 @@ pub struct TransactionRequest { /// Recipient address (None for contract creation) #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option
, + pub to: Option, /// Supplied gas (None for sensible default) #[serde(skip_serializing_if = "Option::is_none")] @@ -73,7 +73,7 @@ impl TransactionRequest { /// Convenience function for sending a new payment transaction to the receiver. The /// `gas`, `gas_price` and `nonce` fields are left empty, to be populated - pub fn pay, V: Into>(to: T, value: V) -> Self { + pub fn pay, V: Into>(to: T, value: V) -> Self { TransactionRequest { from: None, to: Some(to.into()), @@ -95,12 +95,12 @@ impl TransactionRequest { /// Sets the `to` field in the transaction to the provided value pub fn send_to_str(mut self, to: &str) -> Result { - self.to = Some(Address::from_str(to)?); + self.to = Some(Address::from_str(to)?.into()); Ok(self) } /// Sets the `to` field in the transaction to the provided value - pub fn to>(mut self, to: T) -> Self { + pub fn to>(mut self, to: T) -> Self { self.to = Some(to.into()); self } @@ -165,7 +165,7 @@ impl TransactionRequest { rlp_opt(rlp, self.nonce); rlp_opt(rlp, self.gas_price); rlp_opt(rlp, self.gas); - rlp_opt(rlp, self.to); + rlp_opt(rlp, self.to.as_ref()); rlp_opt(rlp, self.value); rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..])); } diff --git a/crates/ethers/examples/ens.rs b/crates/ethers/examples/ens.rs new file mode 100644 index 00000000..fc1d72b9 --- /dev/null +++ b/crates/ethers/examples/ens.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use ethers::{providers::HttpProvider, signers::MainnetWallet, types::TransactionRequest}; +use std::convert::TryFrom; + +#[tokio::main] +async fn main() -> Result<()> { + // connect to the network + let provider = + HttpProvider::try_from("https://mainnet.infura.io/v3/9408f47dedf04716a03ef994182cf150")?; + + // create a wallet and connect it to the provider + let client = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" + .parse::()? + .connect(&provider); + + // craft the transaction + let tx = TransactionRequest::new().to("vitalik.eth").value(100_000); + + // send it! + let hash = client.send_transaction(tx, None).await?; + + // get the mined tx + let tx = client.get_transaction(hash).await?; + + let receipt = client.get_transaction_receipt(tx.hash).await?; + + println!("{}", serde_json::to_string(&tx)?); + println!("{}", serde_json::to_string(&receipt)?); + + Ok(()) +}