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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use ethers::types::Address;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::NftImage;

/// OpenSea display types for [`OpenSeaAttributeValue`] types.
///
/// See [OpenSea documentation](https://docs.opensea.io/docs/metadata-standards)
/// for examples
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OpenSeaDisplayType {
    /// Date
    Date,
    /// Number
    Number,
    /// BoostPercentage
    BoostPercentage,
    /// BoostNumber
    BoostNumber,
}

/// OpenSea attribute value types.
///
/// See [OpenSea documentation](https://docs.opensea.io/docs/metadata-standards)
/// for examples
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OpenSeaAttributeValue {
    /// A `String` attribute value
    String {
        /// The value
        value: String,
    },
    /// An `Integer` with display type and max value
    Integer {
        /// The value
        value: i64,
        /// The display type on OpenSea
        #[serde(default, skip_serializing_if = "Option::is_none")]
        display_type: Option<OpenSeaDisplayType>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        /// The maximum value for display on OpenSea
        max_value: Option<i64>,
    },
    /// A `Float` with display type and max value
    Float {
        /// The value
        value: f64,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        /// The display type on OpenSea
        display_type: Option<OpenSeaDisplayType>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        /// The maximum value for display on OpenSea
        max_value: Option<f64>,
    },
}

impl<T> From<T> for OpenSeaAttributeValue
where
    T: AsRef<str>,
{
    fn from(t: T) -> Self {
        OpenSeaAttributeValue::String {
            value: t.as_ref().to_string(),
        }
    }
}

/// An OpenSea-style NFT attribute. Optionally includes an attribute name (the
/// `trait_type`), as well as the [`OpenSeaAttributeValue`]/
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OpenSeaAttribute {
    /// The attribute name (if any)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trait_type: Option<String>,
    /// The attribute value
    #[serde(flatten)]
    pub value: OpenSeaAttributeValue,
}

impl OpenSeaAttribute {
    /// Shortcut to instantiate a string attribute
    pub fn string<S, T>(trait_type: Option<S>, value: T) -> Self
    where
        S: AsRef<str>,
        T: AsRef<str>,
    {
        Self {
            trait_type: trait_type.map(|s| s.as_ref().to_string()),
            value: value.into(),
        }
    }

    /// Shortcut to instantiate a float attribute
    pub fn float<S>(trait_type: Option<S>, value: f64, max_value: Option<f64>) -> Self
    where
        S: AsRef<str>,
    {
        Self {
            trait_type: trait_type.map(|s| s.as_ref().to_string()),
            value: OpenSeaAttributeValue::Float {
                value,
                display_type: None,
                max_value,
            },
        }
    }

    /// Shortcut to instantiate an integer attribute
    pub fn integer<S>(trait_type: Option<S>, value: i64, max_value: Option<i64>) -> Self
    where
        S: AsRef<str>,
    {
        Self {
            trait_type: trait_type.map(|s| s.as_ref().to_string()),
            value: OpenSeaAttributeValue::Integer {
                value,
                display_type: None,
                max_value,
            },
        }
    }
}

/// OpenSea-style contract-level metadata
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContractMetadata {
    /// The collection name
    pub name: String,
    /// The collection description
    pub description: String,
    /// The collection image
    #[serde(flatten)]
    pub image: NftImage,
    /// An external link for the NFT collection
    pub external_link: Url,
    /// The seller fee, in bps
    pub seller_fee_basis_points: usize,
    /// The recipient of the seller fee
    pub fee_recipient: Address,
}