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 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 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#[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#[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}