make ENS a first class citizen

This commit is contained in:
Georgios Konstantopoulos 2020-05-27 23:43:02 +03:00
parent 6109003559
commit 3186673a2e
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
11 changed files with 152 additions and 21 deletions

View File

@ -1,6 +1,6 @@
//! Contract Functions Output types. //! Contract Functions Output types.
//! Adapted from: https://github.com/tomusdrw/rust-web3/blob/master/src/contract/tokens.rs //! Adapted from: https://github.com/tomusdrw/rust-web3/blob/master/src/contract/tokens.rs
#[allow(clippy::all)] #![allow(clippy::all)]
use crate::Token; use crate::Token;
use arrayvec::ArrayVec; use arrayvec::ArrayVec;

View File

@ -3,7 +3,7 @@ use crate::{ContractCall, Event};
use ethers_abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}; use ethers_abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize};
use ethers_providers::{networks::Network, JsonRpcClient}; use ethers_providers::{networks::Network, JsonRpcClient};
use ethers_signers::{Client, Signer}; 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 rustc_hex::ToHex;
use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; 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 // create the tx object
let tx = TransactionRequest { let tx = TransactionRequest {
to: Some(self.address), to: Some(NameOrAddress::Address(self.address)),
data: Some(data.into()), data: Some(data.into()),
..Default::default() ..Default::default()
}; };

View File

@ -1,5 +1,5 @@
// Adapted from https://github.com/hhatto/rust-ens/blob/master/src/lib.rs // 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; use ethers_utils::keccak256;
// Selectors // Selectors
@ -21,7 +21,7 @@ pub fn get_resolver<T: Into<Address>>(ens_address: T, name: &str) -> Transaction
let data = [&RESOLVER[..], &namehash(name).0].concat(); let data = [&RESOLVER[..], &namehash(name).0].concat();
TransactionRequest { TransactionRequest {
data: Some(data.into()), data: Some(data.into()),
to: Some(ens_address.into()), to: Some(NameOrAddress::Address(ens_address.into())),
..Default::default() ..Default::default()
} }
} }
@ -34,7 +34,7 @@ pub fn resolve<T: Into<Address>>(
let data = [&selector[..], &namehash(name).0].concat(); let data = [&selector[..], &namehash(name).0].concat();
TransactionRequest { TransactionRequest {
data: Some(data.into()), data: Some(data.into()),
to: Some(resolver_address.into()), to: Some(NameOrAddress::Address(resolver_address.into())),
..Default::default() ..Default::default()
} }
} }

View File

@ -2,7 +2,7 @@ use crate::{ens, http::Provider as HttpProvider, networks::Network, JsonRpcClien
use ethers_abi::{Detokenize, ParamType}; use ethers_abi::{Detokenize, ParamType};
use ethers_types::{ 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, TransactionReceipt, TransactionRequest, TxHash, U256,
}; };
use ethers_utils as utils; use ethers_utils as utils;
@ -155,7 +155,17 @@ impl<P: JsonRpcClient, N: Network> Provider<P, N> {
/// Send the transaction to the entire Ethereum network and returns the transaction's hash /// 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. /// This will consume gas from the account that signed the transaction.
pub async fn send_transaction(&self, tx: TransactionRequest) -> Result<TxHash, P::Error> { pub async fn send_transaction(&self, mut tx: TransactionRequest) -> Result<TxHash, P::Error> {
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 self.0.request("eth_sendTransaction", Some(tx)).await
} }

View File

@ -1,7 +1,7 @@
use crate::Signer; use crate::Signer;
use ethers_providers::{networks::Network, JsonRpcClient, Provider}; 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; use std::ops::Deref;
@ -33,6 +33,16 @@ where
mut tx: TransactionRequest, mut tx: TransactionRequest,
block: Option<BlockNumber>, block: Option<BlockNumber>,
) -> Result<TxHash, P::Error> { ) -> Result<TxHash, P::Error> {
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 // if there is no local signer, then the transaction should use the
// node's signer which should already be unlocked // node's signer which should already be unlocked
let signer = if let Some(ref signer) = self.signer { let signer = if let Some(ref signer) = self.signer {

View File

@ -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<Address> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let inner = Address::deserialize(deserializer)?;
Ok(NameOrAddress::Address(inner))
}
}

View File

@ -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 ethers_utils::{hash_message, keccak256};
use rand::Rng; 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` /// This will return an error if called if any of the `nonce`, `gas_price` or `gas`
/// fields are not populated. /// 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( pub fn sign_transaction(
&self, &self,
tx: TransactionRequest, tx: TransactionRequest,
@ -82,11 +87,18 @@ impl PrivateKey {
let rlp = tx.rlp_signed(&signature); let rlp = tx.rlp_signed(&signature);
let hash = keccak256(&rlp.0); 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 { Ok(Transaction {
hash: hash.into(), hash: hash.into(),
nonce, nonce,
from: self.into(), from: self.into(),
to: tx.to, to,
value: tx.value.unwrap_or_default(), value: tx.value.unwrap_or_default(),
gas_price, gas_price,
gas, gas,
@ -217,7 +229,12 @@ mod tests {
// https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction
let tx = TransactionRequest { let tx = TransactionRequest {
from: None, from: None,
to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse().unwrap()), to: Some(
"F0109fC8DF283027b6285cc889F5aA624EaC1F55"
.parse::<Address>()
.unwrap()
.into(),
),
value: Some(1_000_000_000.into()), value: Some(1_000_000_000.into()),
gas: Some(2_000_000.into()), gas: Some(2_000_000.into()),
nonce: Some(0.into()), nonce: Some(0.into()),

View File

@ -24,6 +24,9 @@ pub use block::{Block, BlockId, BlockNumber};
mod log; mod log;
pub use log::{Filter, Log, ValueOrArray}; 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 // re-export the non-standard rand version so that other crates don't use the
// wrong one by accident // wrong one by accident
pub use rand; pub use rand;

View File

@ -89,16 +89,16 @@ pub struct Filter {
impl Filter { impl Filter {
pub fn new() -> Self { pub fn new() -> Self {
let filter = Self::default(); Self::default()
// filter.topics = vec![H256::zero().into(); 4];
filter
} }
#[allow(clippy::wrong_self_convention)]
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self { pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.from_block = Some(block.into()); self.from_block = Some(block.into());
self self
} }
#[allow(clippy::wrong_self_convention)]
pub fn to_block<T: Into<BlockNumber>>(mut self, block: T) -> Self { pub fn to_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.to_block = Some(block.into()); self.to_block = Some(block.into());
self self

View File

@ -1,5 +1,5 @@
//! Transaction types //! 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 ethers_utils::keccak256;
use rlp::RlpStream; use rlp::RlpStream;
@ -40,7 +40,7 @@ pub struct TransactionRequest {
/// Recipient address (None for contract creation) /// Recipient address (None for contract creation)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<Address>, pub to: Option<NameOrAddress>,
/// Supplied gas (None for sensible default) /// Supplied gas (None for sensible default)
#[serde(skip_serializing_if = "Option::is_none")] #[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 /// Convenience function for sending a new payment transaction to the receiver. The
/// `gas`, `gas_price` and `nonce` fields are left empty, to be populated /// `gas`, `gas_price` and `nonce` fields are left empty, to be populated
pub fn pay<T: Into<Address>, V: Into<U256>>(to: T, value: V) -> Self { pub fn pay<T: Into<NameOrAddress>, V: Into<U256>>(to: T, value: V) -> Self {
TransactionRequest { TransactionRequest {
from: None, from: None,
to: Some(to.into()), to: Some(to.into()),
@ -95,12 +95,12 @@ impl TransactionRequest {
/// Sets the `to` field in the transaction to the provided value /// Sets the `to` field in the transaction to the provided value
pub fn send_to_str(mut self, to: &str) -> Result<Self, rustc_hex::FromHexError> { pub fn send_to_str(mut self, to: &str) -> Result<Self, rustc_hex::FromHexError> {
self.to = Some(Address::from_str(to)?); self.to = Some(Address::from_str(to)?.into());
Ok(self) Ok(self)
} }
/// Sets the `to` field in the transaction to the provided value /// Sets the `to` field in the transaction to the provided value
pub fn to<T: Into<Address>>(mut self, to: T) -> Self { pub fn to<T: Into<NameOrAddress>>(mut self, to: T) -> Self {
self.to = Some(to.into()); self.to = Some(to.into());
self self
} }
@ -165,7 +165,7 @@ impl TransactionRequest {
rlp_opt(rlp, self.nonce); rlp_opt(rlp, self.nonce);
rlp_opt(rlp, self.gas_price); rlp_opt(rlp, self.gas_price);
rlp_opt(rlp, self.gas); 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.value);
rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..])); rlp_opt(rlp, self.data.as_ref().map(|d| &d.0[..]));
} }

View File

@ -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::<MainnetWallet>()?
.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(())
}