odra_modules/cep78/
metadata.rs

1use odra::named_keys::single_value_storage;
2use odra::{args::Maybe, prelude::*};
3use serde::{Deserialize, Serialize};
4
5use super::{
6    constants::{
7        IDENTIFIER_MODE, JSON_SCHEMA, METADATA_MUTABILITY, NFT_METADATA_KIND, NFT_METADATA_KINDS
8    },
9    constants::{METADATA_CEP78, METADATA_CUSTOM_VALIDATED, METADATA_NFT721, METADATA_RAW},
10    error::CEP78Error,
11    modalities::{
12        MetadataMutability, MetadataRequirement, NFTIdentifierMode, NFTMetadataKind, Requirement,
13        TokenIdentifier
14    }
15};
16
17single_value_storage!(
18    Cep78MetadataRequirement,
19    MetadataRequirement,
20    NFT_METADATA_KINDS,
21    CEP78Error::MissingNFTMetadataKind
22);
23single_value_storage!(
24    Cep78NFTMetadataKind,
25    NFTMetadataKind,
26    NFT_METADATA_KIND,
27    CEP78Error::MissingNFTMetadataKind
28);
29single_value_storage!(
30    Cep78IdentifierMode,
31    NFTIdentifierMode,
32    IDENTIFIER_MODE,
33    CEP78Error::MissingIdentifierMode
34);
35single_value_storage!(
36    Cep78MetadataMutability,
37    MetadataMutability,
38    METADATA_MUTABILITY,
39    CEP78Error::MissingMetadataMutability
40);
41single_value_storage!(
42    Cep78JsonSchema,
43    String,
44    JSON_SCHEMA,
45    CEP78Error::MissingJsonSchema
46);
47
48#[odra::module]
49pub struct Cep78ValidatedMetadata;
50
51#[odra::module]
52impl Cep78ValidatedMetadata {
53    #[allow(clippy::ptr_arg)]
54    pub fn set(&self, kind: &NFTMetadataKind, token_id: &String, value: String) {
55        let dictionary_name = get_metadata_key(kind);
56        self.env()
57            .set_dictionary_value(dictionary_name, token_id.as_bytes(), value);
58    }
59    #[allow(clippy::ptr_arg)]
60    pub fn get(&self, kind: &NFTMetadataKind, token_id: &String) -> String {
61        let dictionary_name = get_metadata_key(kind);
62        let env = self.env();
63        env.get_dictionary_value(dictionary_name, token_id.as_bytes())
64            .unwrap_or_revert_with(&env, CEP78Error::InvalidTokenIdentifier)
65    }
66}
67
68#[odra::module]
69pub struct Metadata {
70    requirements: SubModule<Cep78MetadataRequirement>,
71    identifier_mode: SubModule<Cep78IdentifierMode>,
72    mutability: SubModule<Cep78MetadataMutability>,
73    json_schema: SubModule<Cep78JsonSchema>,
74    validated_metadata: SubModule<Cep78ValidatedMetadata>,
75    nft_metadata_kind: SubModule<Cep78NFTMetadataKind>
76}
77
78impl Metadata {
79    pub fn init(
80        &mut self,
81        base_metadata_kind: NFTMetadataKind,
82        additional_required_metadata: Maybe<Vec<NFTMetadataKind>>,
83        optional_metadata: Maybe<Vec<NFTMetadataKind>>,
84        metadata_mutability: MetadataMutability,
85        identifier_mode: NFTIdentifierMode,
86        json_schema: String
87    ) {
88        let mut requirements = MetadataRequirement::new();
89        for optional in optional_metadata.unwrap_or_default() {
90            requirements.insert(optional, Requirement::Optional);
91        }
92        for required in additional_required_metadata.unwrap_or_default() {
93            requirements.insert(required, Requirement::Required);
94        }
95        requirements.insert(base_metadata_kind.clone(), Requirement::Required);
96
97        // Attempt to parse the provided schema if the `CustomValidated` metadata kind is required or
98        // optional and fail installation if the schema cannot be parsed.
99        if let Some(req) = requirements.get(&NFTMetadataKind::CustomValidated) {
100            if req == &Requirement::Required || req == &Requirement::Optional {
101                serde_json_wasm::from_str::<CustomMetadataSchema>(&json_schema)
102                    .map_err(|_| CEP78Error::InvalidJsonSchema)
103                    .unwrap_or_revert(self);
104            }
105        }
106        self.nft_metadata_kind.set(base_metadata_kind);
107        self.requirements.set(requirements);
108        self.identifier_mode.set(identifier_mode);
109        self.mutability.set(metadata_mutability);
110        self.json_schema.set(json_schema);
111    }
112
113    pub fn get_requirements(&self) -> MetadataRequirement {
114        self.requirements.get()
115    }
116
117    pub fn get_identifier_mode(&self) -> NFTIdentifierMode {
118        self.identifier_mode.get()
119    }
120
121    pub fn get_or_revert(&self, token_identifier: &TokenIdentifier) -> String {
122        let env = self.env();
123        let metadata_kind_list = self.get_requirements();
124
125        for (metadata_kind, required) in metadata_kind_list {
126            match required {
127                Requirement::Required => {
128                    let id = token_identifier.to_string();
129                    let metadata = self.validated_metadata.get(&metadata_kind, &id);
130                    return metadata;
131                }
132                _ => continue
133            }
134        }
135        env.revert(CEP78Error::MissingTokenMetaData)
136    }
137
138    // test only
139    pub fn get_metadata_by_kind(&self, token_identifier: String, kind: &NFTMetadataKind) -> String {
140        self.validated_metadata.get(kind, &token_identifier)
141    }
142
143    pub fn get_metadata_kind(&self) -> NFTMetadataKind {
144        self.nft_metadata_kind.get()
145    }
146
147    pub fn ensure_mutability(&self, error: CEP78Error) {
148        let current_mutability = self.mutability.get();
149        if current_mutability != MetadataMutability::Mutable {
150            self.env().revert(error)
151        }
152    }
153
154    pub fn update_or_revert(&mut self, token_metadata: &str, token_id: &String) {
155        let requirements = self.get_requirements();
156        for (metadata_kind, required) in requirements {
157            if required == Requirement::Unneeded {
158                continue;
159            }
160            let token_metadata_validation = self.validate(&metadata_kind, token_metadata);
161            match token_metadata_validation {
162                Ok(validated_token_metadata) => {
163                    self.validated_metadata
164                        .set(&metadata_kind, token_id, validated_token_metadata);
165                }
166                Err(err) => {
167                    self.env().revert(err);
168                }
169            }
170        }
171    }
172
173    fn validate(&self, kind: &NFTMetadataKind, metadata: &str) -> Result<String, CEP78Error> {
174        let token_schema = self.get_metadata_schema(kind);
175        match kind {
176            NFTMetadataKind::CEP78 => {
177                let metadata = serde_json_wasm::from_str::<MetadataCEP78>(metadata)
178                    .map_err(|_| CEP78Error::FailedToParseCep78Metadata)?;
179
180                if let Some(name_property) = token_schema.properties.get("name") {
181                    if name_property.required && metadata.name.is_empty() {
182                        self.env().revert(CEP78Error::InvalidCEP78Metadata)
183                    }
184                }
185                if let Some(token_uri_property) = token_schema.properties.get("token_uri") {
186                    if token_uri_property.required && metadata.token_uri.is_empty() {
187                        self.env().revert(CEP78Error::InvalidCEP78Metadata)
188                    }
189                }
190                if let Some(checksum_property) = token_schema.properties.get("checksum") {
191                    if checksum_property.required && metadata.checksum.is_empty() {
192                        self.env().revert(CEP78Error::InvalidCEP78Metadata)
193                    }
194                }
195                serde_json::to_string_pretty(&metadata)
196                    .map_err(|_| CEP78Error::FailedToJsonifyCEP78Metadata)
197            }
198            NFTMetadataKind::NFT721 => {
199                let metadata = serde_json_wasm::from_str::<MetadataNFT721>(metadata)
200                    .map_err(|_| CEP78Error::FailedToParse721Metadata)?;
201
202                if let Some(name_property) = token_schema.properties.get("name") {
203                    if name_property.required && metadata.name.is_empty() {
204                        self.env().revert(CEP78Error::InvalidNFT721Metadata)
205                    }
206                }
207                if let Some(token_uri_property) = token_schema.properties.get("token_uri") {
208                    if token_uri_property.required && metadata.token_uri.is_empty() {
209                        self.env().revert(CEP78Error::InvalidNFT721Metadata)
210                    }
211                }
212                if let Some(symbol_property) = token_schema.properties.get("symbol") {
213                    if symbol_property.required && metadata.symbol.is_empty() {
214                        self.env().revert(CEP78Error::InvalidNFT721Metadata)
215                    }
216                }
217                serde_json::to_string_pretty(&metadata)
218                    .map_err(|_| CEP78Error::FailedToJsonifyNFT721Metadata)
219            }
220            NFTMetadataKind::Raw => Ok(metadata.to_owned()),
221            NFTMetadataKind::CustomValidated => {
222                let custom_metadata =
223                    serde_json_wasm::from_str::<BTreeMap<String, String>>(metadata)
224                        .map(|attributes| CustomMetadata { attributes })
225                        .map_err(|_| CEP78Error::FailedToParseCustomMetadata)?;
226
227                for (property_name, property_type) in token_schema.properties.iter() {
228                    if property_type.required
229                        && !custom_metadata.attributes.contains_key(property_name)
230                    {
231                        self.env().revert(CEP78Error::InvalidCustomMetadata)
232                    }
233                }
234                serde_json::to_string_pretty(&custom_metadata.attributes)
235                    .map_err(|_| CEP78Error::FailedToJsonifyCustomMetadata)
236            }
237        }
238    }
239
240    fn get_metadata_schema(&self, kind: &NFTMetadataKind) -> CustomMetadataSchema {
241        match kind {
242            NFTMetadataKind::Raw => CustomMetadataSchema {
243                properties: BTreeMap::new()
244            },
245            NFTMetadataKind::NFT721 => {
246                let mut properties = BTreeMap::new();
247                properties.insert(
248                    "name".to_string(),
249                    MetadataSchemaProperty {
250                        name: "name".to_string(),
251                        description: "The name of the NFT".to_string(),
252                        required: true
253                    }
254                );
255                properties.insert(
256                    "symbol".to_string(),
257                    MetadataSchemaProperty {
258                        name: "symbol".to_string(),
259                        description: "The symbol of the NFT collection".to_string(),
260                        required: true
261                    }
262                );
263                properties.insert(
264                    "token_uri".to_string(),
265                    MetadataSchemaProperty {
266                        name: "token_uri".to_string(),
267                        description: "The URI pointing to an off chain resource".to_string(),
268                        required: true
269                    }
270                );
271                CustomMetadataSchema { properties }
272            }
273            NFTMetadataKind::CEP78 => {
274                let mut properties = BTreeMap::new();
275                properties.insert(
276                    "name".to_string(),
277                    MetadataSchemaProperty {
278                        name: "name".to_string(),
279                        description: "The name of the NFT".to_string(),
280                        required: true
281                    }
282                );
283                properties.insert(
284                    "token_uri".to_string(),
285                    MetadataSchemaProperty {
286                        name: "token_uri".to_string(),
287                        description: "The URI pointing to an off chain resource".to_string(),
288                        required: true
289                    }
290                );
291                properties.insert(
292                    "checksum".to_string(),
293                    MetadataSchemaProperty {
294                        name: "checksum".to_string(),
295                        description: "A SHA256 hash of the content at the token_uri".to_string(),
296                        required: true
297                    }
298                );
299                CustomMetadataSchema { properties }
300            }
301            NFTMetadataKind::CustomValidated => {
302                serde_json_wasm::from_str::<CustomMetadataSchema>(&self.json_schema.get())
303                    .map_err(|_| CEP78Error::InvalidJsonSchema)
304                    .unwrap_or_revert(self)
305            }
306        }
307    }
308}
309
310#[derive(Serialize, Deserialize)]
311#[odra::odra_type]
312pub(crate) struct MetadataSchemaProperty {
313    pub name: String,
314    pub description: String,
315    pub required: bool
316}
317
318#[derive(Serialize, Deserialize, Clone)]
319pub(crate) struct CustomMetadataSchema {
320    pub properties: BTreeMap<String, MetadataSchemaProperty>
321}
322
323// Using a structure for the purposes of serialization formatting.
324#[derive(Serialize, Deserialize)]
325pub(crate) struct MetadataNFT721 {
326    name: String,
327    symbol: String,
328    token_uri: String
329}
330
331#[derive(Serialize, Deserialize)]
332pub(crate) struct MetadataCEP78 {
333    name: String,
334    token_uri: String,
335    checksum: String
336}
337
338// Using a structure for the purposes of serialization formatting.
339#[derive(Serialize, Deserialize)]
340pub(crate) struct CustomMetadata {
341    attributes: BTreeMap<String, String>
342}
343
344pub(crate) fn get_metadata_key(metadata_kind: &NFTMetadataKind) -> String {
345    match metadata_kind {
346        NFTMetadataKind::CEP78 => METADATA_CEP78,
347        NFTMetadataKind::NFT721 => METADATA_NFT721,
348        NFTMetadataKind::Raw => METADATA_RAW,
349        NFTMetadataKind::CustomValidated => METADATA_CUSTOM_VALIDATED
350    }
351    .to_string()
352}