eth-avatars 0.0.2

A library for fetching Ethereum avatars.
Documentation
use std::sync::Arc;

use crate::{AnyFetcher, FetchError, Fetcher, Resource, resource::Dyncoder};

pub struct Client {
    fetchers: Vec<Arc<dyn AnyFetcher>>,
    max_hops: usize,
}

const DEFAULT_MAX_HOPS: usize = 5;

impl Default for Client {
    fn default() -> Self {
        Self {
            fetchers: Vec::new(),
            max_hops: DEFAULT_MAX_HOPS,
        }
    }
}

impl Client {
    pub fn with_fetcher(mut self, fetcher: impl Fetcher + 'static) -> Self {
        self.fetchers.push(Arc::new(fetcher));
        self
    }

    pub fn with_max_hops(mut self, max_hops: usize) -> Self {
        self.max_hops = max_hops;
        self
    }

    pub async fn fetch(&self, resource: Resource) -> Result<Vec<u8>, FetchError> {
        let mut current = resource;
        let mut decoders: Vec<Dyncoder> = Vec::new();
        let mut hops = 0;

        loop {
            current = match current {
                Resource::Decode { source, decoder } => {
                    decoders.push(decoder);
                    *source
                }
                Resource::Raw(bytes) => match decoders.pop() {
                    None => return Ok(bytes),
                    Some(decoder) => decoder.decode(bytes)?,
                },
                pending if hops < self.max_hops => {
                    hops += 1;
                    self.step(&pending).await?
                }
                _ => {
                    return Err(FetchError::TooManyHops {
                        hops: self.max_hops,
                    });
                }
            };
        }
    }

    async fn step(&self, resource: &Resource) -> Result<Resource, FetchError> {
        let mut failure = None;

        for fetcher in &self.fetchers {
            match fetcher.fetch_any(resource).await {
                None => continue,
                Some(Ok(fetched)) => return Ok(fetched),
                Some(Err(error)) => {
                    tracing::warn!(%error, "fetcher failed");
                    failure = Some(error);
                }
            }
        }

        Err(failure.unwrap_or(FetchError::Unsupported))
    }
}

#[cfg(test)]
mod tests {
    use crate::{Client, modules::ipfs::IpfsGateway, resource::Resource};

    #[cfg(feature = "reqwest")]
    #[tokio::test]
    async fn client_eip155_to_bytes() {
        use crate::{
            modules::{ethereum::resolver::EthereumResolver, http::HttpFetcher},
            utils::test::get_test_provider,
        };

        let mainnet_provider = get_test_provider().await;

        let client = Client::default()
            .with_fetcher(HttpFetcher::default())
            .with_fetcher(IpfsGateway::new("https://ipfs.io/"))
            .with_fetcher(EthereumResolver::new(1, mainnet_provider));

        let input: Resource = "eip155:1/erc1155:0x495f947276749ce646f68ac8c248420045cb7b5e/109791375735522898048150917964456965919994596086232976516654423066184641413121"
            .parse()
            .unwrap();

        let result = client.fetch(input).await.unwrap();

        assert_eq!(result.len(), 559490);
    }

    #[tokio::test]
    async fn client_ipfs_to_bytes() {
        use crate::modules::http::HttpFetcher;

        let client = Client::default()
            .with_fetcher(HttpFetcher::default())
            .with_fetcher(IpfsGateway::new("https://ipfs.io/"));

        let input: Resource = "ipfs://bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu"
            .parse()
            .unwrap();

        let result = client.fetch(input).await.unwrap();

        assert_eq!(result.len(), 26914);
    }
}