Add ENS avatar and TXT records resolution (#889)
* 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 <me@gakonst.com>
This commit is contained in:
parent
78161f07e7
commit
cd8a9b5a97
|
@ -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)
|
||||
|
|
|
@ -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(())
|
||||
# }
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
@ -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)')
|
||||
|
@ -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())),
|
||||
|
@ -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::*;
|
||||
|
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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};
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
ens, maybe,
|
||||
ens, erc, maybe,
|
||||
pubsub::{PubsubClient, SubscriptionStream},
|
||||
stream::{FilterWatcher, DEFAULT_POLL_INTERVAL},
|
||||
FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, MockProvider,
|
||||
|
@ -17,8 +17,8 @@ use ethers_core::{
|
|||
transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
|
||||
Address, Block, BlockId, BlockNumber, BlockTrace, Bytes, EIP1186ProofResponse, FeeHistory,
|
||||
Filter, Log, NameOrAddress, Selector, Signature, Trace, TraceFilter, TraceType,
|
||||
Transaction, TransactionReceipt, TxHash, TxpoolContent, TxpoolInspect, TxpoolStatus, H256,
|
||||
U256, U64,
|
||||
Transaction, TransactionReceipt, TransactionRequest, TxHash, TxpoolContent, TxpoolInspect,
|
||||
TxpoolStatus, H256, U256, U64,
|
||||
},
|
||||
utils,
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|||
use thiserror::Error;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use futures_util::lock::Mutex;
|
||||
use futures_util::{lock::Mutex, try_join};
|
||||
use std::{convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration};
|
||||
use tracing::trace;
|
||||
use tracing_futures::Instrument;
|
||||
|
@ -118,6 +118,9 @@ pub enum ProviderError {
|
|||
#[error(transparent)]
|
||||
HexError(#[from] hex::FromHexError),
|
||||
|
||||
#[error(transparent)]
|
||||
HTTPError(#[from] reqwest::Error),
|
||||
|
||||
#[error("custom error: {0}")]
|
||||
CustomError(String),
|
||||
|
||||
|
@ -786,6 +789,143 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
|
|||
self.query_resolver(ParamType::String, &ens_name, ens::NAME_SELECTOR).await
|
||||
}
|
||||
|
||||
/// Returns the avatar HTTP link of the avatar that the `ens_name` resolves to (or None
|
||||
/// if not configured)
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use ethers_providers::{Provider, Http as HttpProvider, Middleware};
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() {
|
||||
/// # let provider = Provider::<HttpProvider>::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap();
|
||||
/// let avatar = provider.resolve_avatar("parishilton.eth").await.unwrap();
|
||||
/// assert_eq!(avatar.to_string(), "https://i.imgur.com/YW3Hzph.jpg");
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
||||
/// a string. This should theoretically never happen.
|
||||
async fn resolve_avatar(&self, ens_name: &str) -> Result<Url, ProviderError> {
|
||||
let (field, owner) =
|
||||
try_join!(self.resolve_field(ens_name, "avatar"), self.resolve_name(ens_name))?;
|
||||
let url = Url::from_str(&field).map_err(|e| ProviderError::CustomError(e.to_string()))?;
|
||||
match url.scheme() {
|
||||
"https" | "data" => Ok(url),
|
||||
"ipfs" => erc::http_link_ipfs(url).map_err(ProviderError::CustomError),
|
||||
"eip155" => {
|
||||
let token =
|
||||
erc::ERCNFT::from_str(url.path()).map_err(ProviderError::CustomError)?;
|
||||
match token.type_ {
|
||||
erc::ERCNFTType::ERC721 => {
|
||||
let tx = TransactionRequest {
|
||||
data: Some(
|
||||
[&erc::ERC721_OWNER_SELECTOR[..], &token.id].concat().into(),
|
||||
),
|
||||
to: Some(NameOrAddress::Address(token.contract)),
|
||||
..Default::default()
|
||||
};
|
||||
let data = self.call(&tx.into(), None).await?;
|
||||
if decode_bytes::<Address>(ParamType::Address, data) != owner {
|
||||
return Err(ProviderError::CustomError("Incorrect owner.".to_string()))
|
||||
}
|
||||
}
|
||||
erc::ERCNFTType::ERC1155 => {
|
||||
let tx = TransactionRequest {
|
||||
data: Some(
|
||||
[
|
||||
&erc::ERC1155_BALANCE_SELECTOR[..],
|
||||
&[0x0; 12],
|
||||
&owner.0,
|
||||
&token.id,
|
||||
]
|
||||
.concat()
|
||||
.into(),
|
||||
),
|
||||
to: Some(NameOrAddress::Address(token.contract)),
|
||||
..Default::default()
|
||||
};
|
||||
let data = self.call(&tx.into(), None).await?;
|
||||
if decode_bytes::<u64>(ParamType::Uint(64), data) == 0 {
|
||||
return Err(ProviderError::CustomError("Incorrect balance.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let image_url = self.resolve_nft(token).await?;
|
||||
match image_url.scheme() {
|
||||
"https" | "data" => Ok(image_url),
|
||||
"ipfs" => erc::http_link_ipfs(image_url).map_err(ProviderError::CustomError),
|
||||
_ => Err(ProviderError::CustomError(
|
||||
"Unsupported scheme for the image".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => Err(ProviderError::CustomError("Unsupported scheme".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the URL (not necesserily HTTP) of the image behind a token.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # use ethers_providers::{Provider, Http as HttpProvider, Middleware};
|
||||
/// # use std::{str::FromStr, convert::TryFrom};
|
||||
/// # #[tokio::main(flavor = "current_thread")]
|
||||
/// # async fn main() {
|
||||
/// # let provider = Provider::<HttpProvider>::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap();
|
||||
/// let token = ethers_providers::erc::ERCNFT::from_str("erc721:0xc92ceddfb8dd984a89fb494c376f9a48b999aafc/9018").unwrap();
|
||||
/// let token_image = provider.resolve_nft(token).await.unwrap();
|
||||
/// assert_eq!(token_image.to_string(), "https://creature.mypinata.cloud/ipfs/QmNwj3aUzXfG4twV3no7hJRYxLLAWNPk6RrfQaqJ6nVJFa/9018.jpg");
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
||||
/// a string. This should theoretically never happen.
|
||||
async fn resolve_nft(&self, token: erc::ERCNFT) -> Result<Url, ProviderError> {
|
||||
let selector = token.type_.resolution_selector();
|
||||
let tx = TransactionRequest {
|
||||
data: Some([&selector[..], &token.id].concat().into()),
|
||||
to: Some(NameOrAddress::Address(token.contract)),
|
||||
..Default::default()
|
||||
};
|
||||
let data = self.call(&tx.into(), None).await?;
|
||||
let mut metadata_url = Url::parse(&decode_bytes::<String>(ParamType::String, data))
|
||||
.map_err(|e| ProviderError::CustomError(format!("Invalid metadata url: {}", e)))?;
|
||||
|
||||
if token.type_ == erc::ERCNFTType::ERC1155 {
|
||||
metadata_url
|
||||
.set_path(&metadata_url.path().replace("%7Bid%7D", &hex::encode(&token.id)));
|
||||
}
|
||||
if metadata_url.scheme() == "ipfs" {
|
||||
metadata_url = erc::http_link_ipfs(metadata_url).map_err(ProviderError::CustomError)?;
|
||||
}
|
||||
let metadata: erc::Metadata = reqwest::get(metadata_url).await?.json().await?;
|
||||
Url::parse(&metadata.image).map_err(|e| ProviderError::CustomError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Fetch a field for the `ens_name` (no None if not configured).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If the bytes returned from the ENS registrar/resolver cannot be interpreted as
|
||||
/// a string. This should theoretically never happen.
|
||||
async fn resolve_field(&self, ens_name: &str, field: &str) -> Result<String, ProviderError> {
|
||||
let field: String = self
|
||||
.query_resolver_parameters(
|
||||
ParamType::String,
|
||||
ens_name,
|
||||
ens::FIELD_SELECTOR,
|
||||
Some(&ens::parameterhash(field)),
|
||||
)
|
||||
.await?;
|
||||
Ok(field)
|
||||
}
|
||||
|
||||
/// Returns the details of all transactions currently pending for inclusion in the next
|
||||
/// block(s), as well as the ones that are being scheduled for future execution only.
|
||||
/// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content)
|
||||
|
@ -981,6 +1121,16 @@ impl<P: JsonRpcClient> Provider<P> {
|
|||
param: ParamType,
|
||||
ens_name: &str,
|
||||
selector: Selector,
|
||||
) -> Result<T, ProviderError> {
|
||||
self.query_resolver_parameters(param, ens_name, selector, None).await
|
||||
}
|
||||
|
||||
async fn query_resolver_parameters<T: Detokenize>(
|
||||
&self,
|
||||
param: ParamType,
|
||||
ens_name: &str,
|
||||
selector: Selector,
|
||||
parameters: Option<&[u8]>,
|
||||
) -> Result<T, ProviderError> {
|
||||
// Get the ENS address, prioritize the local override variable
|
||||
let ens_addr = self.ens.unwrap_or(ens::ENS_ADDRESS);
|
||||
|
@ -995,8 +1145,9 @@ impl<P: JsonRpcClient> Provider<P> {
|
|||
}
|
||||
|
||||
// resolve
|
||||
let data =
|
||||
self.call(&ens::resolve(resolver_address, selector, ens_name).into(), None).await?;
|
||||
let data = self
|
||||
.call(&ens::resolve(resolver_address, selector, ens_name, parameters).into(), None)
|
||||
.await?;
|
||||
|
||||
Ok(decode_bytes(param, data))
|
||||
}
|
||||
|
@ -1346,6 +1497,30 @@ mod tests {
|
|||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mainnet_resolve_avatar() {
|
||||
let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap();
|
||||
|
||||
for (ens_name, res) in &[
|
||||
// HTTPS
|
||||
("alisha.eth", "https://ipfs.io/ipfs/QmeQm91kAdPGnUKsE74WvkqYKUeHvc2oHd2FW11V3TrqkQ"),
|
||||
// ERC-1155
|
||||
("nick.eth", "https://lh3.googleusercontent.com/hKHZTZSTmcznonu8I6xcVZio1IF76fq0XmcxnvUykC-FGuVJ75UPdLDlKJsfgVXH9wOSmkyHw0C39VAYtsGyxT7WNybjQ6s3fM3macE"),
|
||||
// HTTPS
|
||||
("parishilton.eth", "https://i.imgur.com/YW3Hzph.jpg"),
|
||||
// ERC-721 with IPFS link
|
||||
("ikehaya-nft.eth", "https://ipfs.io/ipfs/QmdKkwCE8uVhgYd7tWBfhtHdQZDnbNukWJ8bvQmR6nZKsk"),
|
||||
// ERC-1155 with IPFS link
|
||||
("vitalik.eth", "https://ipfs.io/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif"),
|
||||
// IPFS
|
||||
("cdixon.eth", "https://ipfs.io/ipfs/QmYA6ZpEARgHvRHZQdFPynMMX8NtdL2JCadvyuyG2oA88u"),
|
||||
("0age.eth", "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOmJsYWNrIiB2aWV3Qm94PSIwIDAgNTAwIDUwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNTUiIHk9IjYwIiB3aWR0aD0iMTkwIiBoZWlnaHQ9IjM5MCIgZmlsbD0iIzY5ZmYzNyIvPjwvc3ZnPg==")
|
||||
] {
|
||||
println!("Resolving: {}", ens_name);
|
||||
assert_eq!(provider.resolve_avatar(ens_name).await.unwrap(), Url::parse(res).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(feature = "celo", ignore)]
|
||||
async fn test_new_block_filter() {
|
||||
|
|
Loading…
Reference in New Issue