Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Add ENS avatar and TXT records resolution (#889)
Browse files Browse the repository at this point in the history
* Add ENS avatar resolution

As well as arbitrary fields.

* Use try_join

* Improve reqwest's error passing

* Split avatar resolution in ERC token parsing and resolution

* no_run examples

* Rename token to NFT

* A bit more documentation

* Nightly cargo fmt

* Use different ERC-721 test

* Update CHANGELOG

Co-authored-by: Georgios Konstantopoulos <[email protected]>
  • Loading branch information
sbihel and gakonst authored Feb 16, 2022
1 parent 78161f0 commit cd8a9b5
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
so that the receipt can be returned to the called when deploying
a contract [#865](https://github.com/gakonst/ethers-rs/pull/865)
- Add Arbitrum mainnet and testnet to the list of known chains
- Add ENS avatar and TXT records resolution
[#889](https://github.com/gakonst/ethers-rs/pull/889)
- Add a getter to `ProjectCompileOutput` that returns a mapping of compiler
versions to a vector of name + contract struct tuples
[#908](https://github.com/gakonst/ethers-rs/pull/908)
Expand Down
10 changes: 10 additions & 0 deletions ethers-providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ let address = provider.resolve_name(name).await?;
// Lookup ENS name given Address
let resolved_name = provider.lookup_address(address).await?;
assert_eq!(name, resolved_name);
/// Lookup ENS field
let url = "https://vitalik.ca".to_string();
let resolved_url = provider.resolve_field(name, "url").await?;
assert_eq!(url, resolved_url);
/// Lookup and resolve ENS avatar
let avatar = "https://ipfs.io/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif".to_string();
let resolved_avatar = provider.resolve_avatar(name).await?;
assert_eq!(avatar, resolved_avatar.to_string());
# Ok(())
# }
```
38 changes: 37 additions & 1 deletion ethers-providers/src/ens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use ethers_core::{
utils::keccak256,
};

use std::convert::TryInto;

/// ENS registry address (`0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e`)
pub const ENS_ADDRESS: Address = H160([
// cannot set type aliases as constructors
Expand All @@ -23,6 +25,9 @@ pub const ADDR_SELECTOR: Selector = [59, 59, 87, 222];
/// name(bytes32)
pub const NAME_SELECTOR: Selector = [105, 31, 52, 49];

/// text(bytes32, string)
pub const FIELD_SELECTOR: Selector = [89, 209, 212, 60];

/// 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)')
Expand All @@ -39,8 +44,9 @@ pub fn resolve<T: Into<Address>>(
resolver_address: T,
selector: Selector,
name: &str,
parameters: Option<&[u8]>,
) -> TransactionRequest {
let data = [&selector[..], &namehash(name).0].concat();
let data = [&selector[..], &namehash(name).0, parameters.unwrap_or_default()].concat();
TransactionRequest {
data: Some(data.into()),
to: Some(NameOrAddress::Address(resolver_address.into())),
Expand All @@ -65,6 +71,23 @@ pub fn namehash(name: &str) -> H256 {
.into()
}

/// Returns a number in bytes form with padding to fit in 32 bytes.
pub fn bytes_32ify(n: u64) -> Vec<u8> {
let b = n.to_be_bytes();
[[0; 32][b.len()..].to_vec(), b.to_vec()].concat()
}

/// Returns the ENS record key hash [EIP-634](https://eips.ethereum.org/EIPS/eip-634)
pub fn parameterhash(name: &str) -> Vec<u8> {
let bytes = name.as_bytes();
let key_bytes =
[&bytes_32ify(64), &bytes_32ify(bytes.len().try_into().unwrap()), bytes].concat();
match key_bytes.len() % 32 {
0 => key_bytes,
n => [key_bytes, [0; 32][n..].to_vec()].concat(),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -86,4 +109,17 @@ mod tests {
assert_hex(namehash(name), expected);
}
}

#[test]
fn test_parametershash() {
assert_eq!(
parameterhash("avatar").to_vec(),
vec![
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 6, 97, 118, 97, 116, 97, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]
);
}
}
97 changes: 97 additions & 0 deletions ethers-providers/src/erc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! ERC related utilities. Only supporting NFTs for now.
use ethers_core::types::{Address, Selector, U256};

use serde::Deserialize;
use std::str::FromStr;
use url::Url;

/// ownerOf(uint256 tokenId)
pub const ERC721_OWNER_SELECTOR: Selector = [0x63, 0x52, 0x21, 0x1e];

/// balanceOf(address owner, uint256 tokenId)
pub const ERC1155_BALANCE_SELECTOR: Selector = [0x00, 0xfd, 0xd5, 0x8e];

const IPFS_GATEWAY: &str = "https://ipfs.io/ipfs/";

/// An ERC 721 or 1155 token
pub struct ERCNFT {
pub type_: ERCNFTType,
pub contract: Address,
pub id: [u8; 32],
}

impl FromStr for ERCNFT {
type Err = String;
fn from_str(input: &str) -> Result<ERCNFT, Self::Err> {
let split: Vec<&str> =
input.trim_start_matches("eip155:").trim_start_matches("1/").split(':').collect();
let (token_type, inner_path) = if split.len() == 2 {
(
ERCNFTType::from_str(split[0])
.map_err(|_| "Unsupported ERC token type".to_string())?,
split[1],
)
} else {
return Err("Unsupported ERC link".to_string())
};

let token_split: Vec<&str> = inner_path.split('/').collect();
let (contract_addr, token_id) = if token_split.len() == 2 {
let token_id = U256::from_dec_str(token_split[1])
.map_err(|e| format!("Unsupported token id type: {} {}", token_split[1], e))?;
let mut token_id_bytes = [0x0; 32];
token_id.to_big_endian(&mut token_id_bytes);
(
Address::from_str(token_split[0].trim_start_matches("0x"))
.map_err(|e| format!("Invalid contract address: {} {}", token_split[0], e))?,
token_id_bytes,
)
} else {
return Err("Unsupported ERC link path".to_string())
};
Ok(ERCNFT { id: token_id, type_: token_type, contract: contract_addr })
}
}

/// Supported ERCs
#[derive(PartialEq)]
pub enum ERCNFTType {
ERC721,
ERC1155,
}

impl FromStr for ERCNFTType {
type Err = ();
fn from_str(input: &str) -> Result<ERCNFTType, Self::Err> {
match input {
"erc721" => Ok(ERCNFTType::ERC721),
"erc1155" => Ok(ERCNFTType::ERC1155),
_ => Err(()),
}
}
}

impl ERCNFTType {
pub fn resolution_selector(&self) -> Selector {
match self {
// tokenURI(uint256)
ERCNFTType::ERC721 => [0xc8, 0x7b, 0x56, 0xdd],
// url(uint256)
ERCNFTType::ERC1155 => [0x0e, 0x89, 0x34, 0x1c],
}
}
}

/// ERC-1155 and ERC-721 metadata document.
#[derive(Deserialize)]
pub struct Metadata {
pub image: String,
}

/// Returns a HTTP url for an IPFS object.
pub fn http_link_ipfs(url: Url) -> Result<Url, String> {
Url::parse(IPFS_GATEWAY)
.unwrap()
.join(url.to_string().trim_start_matches("ipfs://").trim_start_matches("ipfs/"))
.map_err(|e| e.to_string())
}
15 changes: 15 additions & 0 deletions ethers-providers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ pub use stream::{interval, FilterWatcher, TransactionStream, DEFAULT_POLL_INTERV
mod pubsub;
pub use pubsub::{PubsubClient, SubscriptionStream};

pub mod erc;

use async_trait::async_trait;
use auto_impl::auto_impl;
use ethers_core::types::transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed};
use serde::{de::DeserializeOwned, Serialize};
use std::{error::Error, fmt::Debug, future::Future, pin::Pin};
use url::Url;

pub use provider::{FilterKind, Provider, ProviderError};

Expand Down Expand Up @@ -263,6 +266,18 @@ pub trait Middleware: Sync + Send + Debug {
self.inner().lookup_address(address).await.map_err(FromErr::from)
}

async fn resolve_avatar(&self, ens_name: &str) -> Result<Url, Self::Error> {
self.inner().resolve_avatar(ens_name).await.map_err(FromErr::from)
}

async fn resolve_nft(&self, token: erc::ERCNFT) -> Result<Url, Self::Error> {
self.inner().resolve_nft(token).await.map_err(FromErr::from)
}

async fn resolve_field(&self, ens_name: &str, field: &str) -> Result<String, Self::Error> {
self.inner().resolve_field(ens_name, field).await.map_err(FromErr::from)
}

async fn get_block<T: Into<BlockId> + Send + Sync>(
&self,
block_hash_or_number: T,
Expand Down
Loading

0 comments on commit cd8a9b5

Please sign in to comment.