|
| 1 | +//! This module presents traits according to [multi-token metadata extension](https://github.com/near/NEPs/blob/master/specs/Standards/Tokens/MultiToken/Metadata.md) |
| 2 | +
|
| 3 | +use crate::TokenId; |
| 4 | +use crate::enumeration::MultiTokenEnumeration; |
| 5 | +use crate::metadata::adapters::As; |
| 6 | +use borsh::schema::{Declaration, Definition}; |
| 7 | +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; |
| 8 | +use chrono::{DateTime, Utc}; |
| 9 | +use defuse_borsh_utils::adapters; |
| 10 | +use near_sdk::near; |
| 11 | +use near_sdk::serde::{Deserialize, Serialize}; |
| 12 | +use schemars::JsonSchema; |
| 13 | +use schemars::r#gen::SchemaGenerator; |
| 14 | +use schemars::schema::Schema; |
| 15 | +use serde_with::serde_as; |
| 16 | +use serde_with::skip_serializing_none; |
| 17 | +use std::collections::BTreeMap; |
| 18 | + |
| 19 | +pub type MetadataId = String; |
| 20 | + |
| 21 | +#[derive(Debug, Clone)] |
| 22 | +#[near(serializers = [json, borsh])] |
| 23 | +pub struct MTContractMetadata { |
| 24 | + pub spec: String, // "a string that MUST be formatted mt-1.0.0" or whatever the spec version is used. |
| 25 | + pub name: String, |
| 26 | +} |
| 27 | + |
| 28 | +#[derive(Debug, Clone)] |
| 29 | +#[skip_serializing_none] |
| 30 | +#[near(serializers = [json, borsh])] |
| 31 | +pub struct MTBaseTokenMetadata { |
| 32 | + /// Human‐readable name of the base (e.g., "Silver Swords" or "Metaverse 3") |
| 33 | + pub name: String, |
| 34 | + |
| 35 | + /// Unique identifier for this metadata entry |
| 36 | + pub id: MetadataId, |
| 37 | + |
| 38 | + /// Abbreviated symbol for the token (e.g., "MOCHI"), or `None` if unset |
| 39 | + pub symbol: Option<String>, |
| 40 | + |
| 41 | + /// Data URL for a small icon image, or `None` |
| 42 | + pub icon: Option<String>, |
| 43 | + |
| 44 | + /// Number of decimals (useful if this base represents an FT‐style token), or `None` |
| 45 | + pub decimals: Option<u8>, |
| 46 | + |
| 47 | + /// Centralized gateway URL for reliably accessing decentralized storage assets referenced by `reference` or `media`, or `None` |
| 48 | + pub base_uri: Option<String>, |
| 49 | + |
| 50 | + /// URL pointing to a JSON file with additional info, or `None` |
| 51 | + pub reference: Option<String>, |
| 52 | + |
| 53 | + /// Number of copies of this set of metadata that existed when the token was minted, or `None` |
| 54 | + pub copies: Option<u64>, |
| 55 | + |
| 56 | + /// Base64‐encoded SHA-256 hash of the JSON from `reference`; required if `reference` is set, or `None` |
| 57 | + pub reference_hash: Option<String>, |
| 58 | +} |
| 59 | + |
| 60 | +#[derive(Debug, Clone)] |
| 61 | +#[skip_serializing_none] |
| 62 | +#[near(serializers = [json, borsh])] |
| 63 | +pub struct MTTokenMetadata { |
| 64 | + /// Title of this specific token (e.g., "Arch Nemesis: Mail Carrier" or "Parcel #5055"), or `None` |
| 65 | + pub title: Option<String>, |
| 66 | + |
| 67 | + /// Free-form description of this token, or `None` |
| 68 | + pub description: Option<String>, |
| 69 | + |
| 70 | + /// URL to associated media (ideally decentralized, content-addressed storage), or `None` |
| 71 | + pub media: Option<String>, |
| 72 | + |
| 73 | + /// Base64‐encoded SHA-256 hash of the content referenced by `media`; required if `media` is set, or `None` |
| 74 | + pub media_hash: Option<String>, |
| 75 | + |
| 76 | + /// Unix epoch in milliseconds or RFC3339 when this token was issued or minted, or `None` |
| 77 | + pub issued_at: Option<DatetimeUtcWrapper>, |
| 78 | + |
| 79 | + /// Unix epoch in milliseconds or RFC3339 when this token expires, or `None` |
| 80 | + pub expires_at: Option<DatetimeUtcWrapper>, |
| 81 | + |
| 82 | + /// Unix epoch in milliseconds or RFC3339 when this token starts being valid, or `None` |
| 83 | + pub starts_at: Option<DatetimeUtcWrapper>, |
| 84 | + |
| 85 | + /// Unix epoch in milliseconds or RFC3339 when this token metadata was last updated, or `None` |
| 86 | + pub updated_at: Option<DatetimeUtcWrapper>, |
| 87 | + |
| 88 | + /// Anything extra the MT wants to store on-chain (can be stringified JSON), or `None` |
| 89 | + pub extra: Option<String>, |
| 90 | + |
| 91 | + /// URL to an off-chain JSON file with more info, or `None` |
| 92 | + pub reference: Option<String>, |
| 93 | + |
| 94 | + /// Base64‐encoded SHA-256 hash of the JSON from `reference`; required if `reference` is set, or `None` |
| 95 | + pub reference_hash: Option<String>, |
| 96 | +} |
| 97 | + |
| 98 | +#[derive(Debug, Clone)] |
| 99 | +#[near(serializers = [json, borsh])] |
| 100 | +pub struct MTTokenMetadataAll { |
| 101 | + pub base: MTBaseTokenMetadata, |
| 102 | + pub token: MTTokenMetadata, |
| 103 | +} |
| 104 | + |
| 105 | +pub trait MultiTokenMetadata { |
| 106 | + /// Returns the contract‐level metadata (spec + name). |
| 107 | + fn mt_metadata_contract(&self) -> MTContractMetadata; |
| 108 | + |
| 109 | + /// For a list of `token_ids`, returns a vector of combined `(base, token)` metadata. |
| 110 | + fn mt_metadata_token_all(&self, token_ids: Vec<TokenId>) -> Vec<Option<MTTokenMetadataAll>>; |
| 111 | + |
| 112 | + /// Given `token_ids`, returns each token’s `MTTokenMetadata` or `None` if absent. |
| 113 | + fn mt_metadata_token_by_token_id( |
| 114 | + &self, |
| 115 | + token_ids: Vec<TokenId>, |
| 116 | + ) -> Vec<Option<MTTokenMetadata>>; |
| 117 | + |
| 118 | + /// Given `token_ids`, returns each token’s `MTBaseTokenMetadata` or `None` if absent. |
| 119 | + fn mt_metadata_base_by_token_id( |
| 120 | + &self, |
| 121 | + token_ids: Vec<TokenId>, |
| 122 | + ) -> Vec<Option<MTBaseTokenMetadata>>; |
| 123 | + |
| 124 | + /// Given a list of `base_metadata_ids`, returns each `MTBaseTokenMetadata` or `None` if absent. |
| 125 | + fn mt_metadata_base_by_metadata_id( |
| 126 | + &self, |
| 127 | + base_metadata_ids: Vec<MetadataId>, |
| 128 | + ) -> Vec<Option<MTBaseTokenMetadata>>; |
| 129 | +} |
| 130 | + |
| 131 | +/// The contract must implement the following view method if using [multi-token enumeration standard](https://nomicon.io/Standards/Tokens/MultiToken/Enumeration#interface). |
| 132 | +pub trait MultiTokenMetadataEnumeration: MultiTokenMetadata + MultiTokenEnumeration { |
| 133 | + /// Get list of all base metadata for the contract, with pagination. |
| 134 | + /// |
| 135 | + /// # Arguments |
| 136 | + /// * `from_index`: an optional string representing an unsigned 128-bit integer, |
| 137 | + /// indicating the starting index |
| 138 | + /// * `limit`: an optional u64 indicating the maximum number of entries to return |
| 139 | + /// |
| 140 | + /// # Returns |
| 141 | + /// A vector of `MTBaseTokenMetadata` objects, or an empty vector if none. |
| 142 | + fn mt_tokens_base_metadata_all( |
| 143 | + &self, |
| 144 | + from_index: Option<String>, |
| 145 | + limit: Option<u64>, |
| 146 | + ) -> Vec<MTBaseTokenMetadata>; |
| 147 | +} |
| 148 | + |
| 149 | +/// A wrapper that implements Borsh de-/serialization for `Datetime<Utc>` |
| 150 | +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] |
| 151 | +#[serde(crate = "::near_sdk::serde")] |
| 152 | +#[serde_as] |
| 153 | +pub struct DatetimeUtcWrapper( |
| 154 | + #[serde_as(as = "PickFirst<(_, serde_with::TimestampMilliSeconds)>")] |
| 155 | + #[borsh( |
| 156 | + deserialize_with = "As::<adapters::TimestampMilliSeconds>::deserialize", |
| 157 | + serialize_with = "As::<adapters::TimestampMilliSeconds>::serialize" |
| 158 | + )] |
| 159 | + pub DateTime<Utc>, |
| 160 | +); |
| 161 | + |
| 162 | +impl JsonSchema for DatetimeUtcWrapper { |
| 163 | + fn schema_name() -> String { |
| 164 | + "DatetimeUtcWrapper".to_owned() |
| 165 | + } |
| 166 | + |
| 167 | + fn json_schema(generator: &mut SchemaGenerator) -> Schema { |
| 168 | + generator.subschema_for::<u64>() |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +impl BorshSchema for DatetimeUtcWrapper { |
| 173 | + fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) { |
| 174 | + <u64 as BorshSchema>::add_definitions_recursively(definitions); |
| 175 | + } |
| 176 | + |
| 177 | + fn declaration() -> Declaration { |
| 178 | + <u64 as BorshSchema>::declaration() |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +#[cfg(test)] |
| 183 | +mod tests { |
| 184 | + use crate::metadata::DatetimeUtcWrapper; |
| 185 | + use chrono::DateTime; |
| 186 | + use hex::FromHex; |
| 187 | + use near_sdk::borsh; |
| 188 | + |
| 189 | + #[test] |
| 190 | + fn test_datetime_utc_wrapper_borsh() { |
| 191 | + let timestamp = DateTime::from_timestamp(1747772412, 0).unwrap(); |
| 192 | + let wrapped = DatetimeUtcWrapper(timestamp); |
| 193 | + let encoded = borsh::to_vec(&wrapped).unwrap(); |
| 194 | + assert_eq!(encoded, Vec::from_hex("60905aef96010000").unwrap()); |
| 195 | + let actual_wrapped: DatetimeUtcWrapper = borsh::from_slice(encoded.as_slice()).unwrap(); |
| 196 | + assert_eq!(actual_wrapped.0, wrapped.0); |
| 197 | + } |
| 198 | +} |
0 commit comments