Skip to content

Commit

Permalink
Modularize ENSLookup (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucemans authored Sep 8, 2023
1 parent 803b6b3 commit 7c47f74
Show file tree
Hide file tree
Showing 23 changed files with 774 additions and 460 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ thiserror = "1.0.48"
regex = "1.9.5"
rustls = "0.21.7"
bs58 = "0.5.0"
hex-literal = "0.4.1"
axum-macros = "0.3.8"
lazy_static = "1.4.0"


[build-dependencies]
Expand Down
57 changes: 57 additions & 0 deletions src/models/lookup/addr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use ethers_core::{
abi::{ParamType, Token},
types::H256,
};
use hex_literal::hex;

use super::{ENSLookup, ENSLookupError};

pub struct Addr {}

impl ENSLookup for Addr {
fn calldata(&self, namehash: &H256) -> Vec<u8> {
let fn_selector = hex!("3b3b57de").to_vec();

let data =
ethers_core::abi::encode(&[Token::FixedBytes(namehash.as_fixed_bytes().to_vec())]);

[fn_selector, data].concat()
}

fn decode(&self, data: &[u8]) -> Result<String, ENSLookupError> {
let decoded_abi = ethers_core::abi::decode(&[ParamType::Address], data)
.map_err(|_| ENSLookupError::AbiError)?;
let address = decoded_abi
.get(0)
.ok_or(ENSLookupError::AbiError)?
.clone()
.into_address()
.ok_or(ENSLookupError::InvalidPayload("yup".to_string()))?;

Ok(format!("{address:?}"))
}

fn name(&self) -> String {
"addr".to_string()
}
}

#[cfg(test)]
mod tests {
use super::*;
use ethers::providers::namehash;
use hex_literal::hex;

#[tokio::test]
async fn test_calldata_address() {
assert_eq!(
Addr {}.calldata(&namehash("eth")),
hex!("3b3b57de93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae")
);

assert_eq!(
Addr {}.calldata(&namehash("foo.eth")),
hex!("3b3b57dede9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f")
);
}
}
92 changes: 92 additions & 0 deletions src/models/lookup/avatar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use super::{ENSLookup, ENSLookupError};

use ethers_core::{
abi::{ParamType, Token},
types::H256,
};
use hex_literal::hex;
use tracing::info;

pub struct Avatar {
pub ipfs_gateway: String,
pub name: String,
}

impl Avatar {}

impl ENSLookup for Avatar {
fn calldata(&self, namehash: &H256) -> Vec<u8> {
let fn_selector = hex!("59d1d43c").to_vec();

let data = ethers_core::abi::encode(&[
Token::FixedBytes(namehash.as_fixed_bytes().to_vec()),
Token::String("avatar".to_string()),
]);

[fn_selector, data].concat()
}

fn decode(&self, data: &[u8]) -> Result<String, ENSLookupError> {
let decoded_abi = ethers_core::abi::decode(&[ParamType::String], data)
.map_err(|_| ENSLookupError::AbiError)?;
let value = decoded_abi.get(0).ok_or(ENSLookupError::AbiError)?;
let value = value.to_string();

// If IPFS
let ipfs = regex::Regex::new(r"ipfs://([0-9a-zA-Z]+)").unwrap();
if let Some(captures) = ipfs.captures(&value) {
let hash = captures.get(1).unwrap().as_str();

return Ok(format!("{}{hash}", self.ipfs_gateway));
}

// If the raw value is eip155 url
let eip155 =
regex::Regex::new(r"eip155:([0-9]+)/(erc1155|erc712):0x([0-9a-fA-F]{40})/([0-9]+)")
.unwrap();

if let Some(captures) = eip155.captures(&value) {
let chain_id = captures.get(1).unwrap().as_str();
let contract_type = captures.get(2).unwrap().as_str();
let contract_address = captures.get(3).unwrap().as_str();
let token_id = captures.get(4).unwrap().as_str();

info!(
"Encountered Avatar: {chain_id} {contract_type} {contract_address} {token_id}",
chain_id = chain_id,
contract_type = contract_type,
contract_address = contract_address,
token_id = token_id
);

// TODO: Remove naive approach
return Ok(format!(
"https://metadata.ens.domains/mainnet/avatar/{}",
self.name
));
}

Ok(value)
}

fn name(&self) -> String {
"avatar".to_string()
}
}

#[cfg(test)]
mod tests {
use super::*;
use ethers::providers::namehash;

#[tokio::test]
async fn test_calldata_avatar() {
assert_eq!(
Avatar{
ipfs_gateway: "https://ipfs.io/ipfs/".to_string(),
name: "luc.eth".to_string(),
}.calldata(&namehash("luc.eth")),
hex_literal::hex!("59d1d43ce1e7bcf2ca33c28a806ee265cfedf02fedf1b124ca73b2203ca80cc7c91a02ad000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000066176617461720000000000000000000000000000000000000000000000000000")
);
}
}
28 changes: 28 additions & 0 deletions src/models/lookup/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use ethers_core::types::H256;
use thiserror::Error;

pub mod addr;
pub mod avatar;
pub mod multicoin;
pub mod text;

#[derive(Error, Debug)]
pub enum ENSLookupError {
#[error("ABI error")]
AbiError,

#[error("Invalid payload: {0}")]
InvalidPayload(String),

#[error("Unsupported: {0}")]
Unsupported(String),

#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

pub trait ENSLookup {
fn calldata(&self, namehash: &H256) -> Vec<u8>;
fn decode(&self, data: &[u8]) -> Result<String, ENSLookupError>;
fn name(&self) -> String;
}
101 changes: 101 additions & 0 deletions src/models/lookup/multicoin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use anyhow::anyhow;
use ethers_core::{
abi::{ParamType, Token},
k256::U256,
types::{H160, H256},
};
use hex_literal::hex;
use tracing::info;

use crate::models::multicoin::cointype::{coins::CoinType, evm::ChainId, slip44::SLIP44};

use super::{ENSLookup, ENSLookupError};

pub struct Multicoin {
pub coin_type: CoinType,
}

impl ENSLookup for Multicoin {
fn calldata(&self, namehash: &H256) -> Vec<u8> {
let fn_selector = hex!("f1cb7e06").to_vec();

let data = ethers_core::abi::encode(&[
Token::FixedBytes(namehash.as_fixed_bytes().to_vec()),
Token::Uint(self.coin_type.clone().into()),
]);

[fn_selector, data].concat()
}

fn decode(&self, data: &[u8]) -> Result<String, ENSLookupError> {
info!("Decoding: {:?}", data);

let decoded_abi = ethers_core::abi::decode(&[ParamType::Bytes], data)
.map_err(|_| ENSLookupError::AbiError)?;
let value = decoded_abi
.get(0)
.ok_or(ENSLookupError::AbiError)?
.clone()
.into_bytes();

let value = value.unwrap();

// TODO: If value is empty

match &self.coin_type {
// SLIP-044 Chain Address Decoding (see ensip-9)
CoinType::Slip44(slip44) => match slip44 {
// Bitcoin Decoding
SLIP44::Bitcoin => Ok(format!("btc:{}", bs58::encode(value).into_string())),
// Lightcoin Decoding
SLIP44::Litecoin => {
Err(ENSLookupError::Unknown(anyhow!(
"Litecoin Decoding Not Implemented"
)))
// Ok(format!("ltc:{}", bs58::encode(value).into_string()))
}

// Unsupported SLIP44 Chain
_ => {
// Raw Dump
// Ok(format!("SLIP-{:?}", value))

// Unsupported
Err(ENSLookupError::Unsupported("Chain Not Supported".to_string()))
}
},
// Implement EVM Chain Address Decoding (mostly ChecksummedHex, sometimes ChecksummedHex(chainId)) (see ensip-11)
CoinType::Evm(evm) => match evm {
// TODO: EVM Exceptions go here
// ChainId::Ethereum => {
// // Verify length is 20 bytes
// if value.len() != 20 {
// // TODO: throw invalid length
// return Ok("Invalid Length".to_string());
// }

// let address = hex::encode(value);

// Ok(format!("0x{address}"))
// },

// Every EVM Chain
_ => {
// Verify length is 20 bytes
if value.len() != 20 {
// TODO: throw invalid length
return Ok("Invalid Length".to_string());
}

let address = hex::encode(value);

Ok(format!("0x{address}"))
}
},
}
}

fn name(&self) -> String {
format!("chains.{:?}", self.coin_type)
}
}
42 changes: 42 additions & 0 deletions src/models/lookup/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use ethers_core::{
abi::{ParamType, Token},
types::H256,
};
use hex_literal::hex;

use super::{ENSLookup, ENSLookupError};
pub struct Text {
key: String,
}

impl Text {
pub const fn new(key: String) -> Self {
Self { key }
}
}

impl ENSLookup for Text {
fn calldata(&self, namehash: &H256) -> Vec<u8> {
let fn_selector = hex!("59d1d43c").to_vec();

let data = ethers_core::abi::encode(&[
Token::FixedBytes(namehash.as_fixed_bytes().to_vec()),
Token::String(self.key.to_string()),
]);

[fn_selector, data].concat()
}

fn decode(&self, data: &[u8]) -> Result<String, ENSLookupError> {
let decoded_abi = ethers_core::abi::decode(&[ParamType::String], data)
.map_err(|_| ENSLookupError::AbiError)?;
let value = decoded_abi.get(0).ok_or(ENSLookupError::AbiError)?;
let value = value.to_string();

Ok(value)
}

fn name(&self) -> String {
format!("records.{}", self.key)
}
}
4 changes: 4 additions & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
pub mod lookup;
pub mod multicoin;
pub mod profile;
pub mod records;
pub mod universal_resolver;
Loading

0 comments on commit 7c47f74

Please sign in to comment.