1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//! 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 {
    /// Type of the NFT
    pub type_: ERCNFTType,
    /// Address of the NFT contract
    pub contract: Address,
    /// NFT ID in that contract
    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: {} {e}", token_split[1]))?;
            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: {} {e}", token_split[0]))?,
                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, Eq)]
pub enum ERCNFTType {
    /// ERC721
    ERC721,
    /// ERC1155
    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 {
    /// Get the method selector
    pub const 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 {
    /// The URL of the image for the NFT
    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())
}