diff --git a/Cargo.lock b/Cargo.lock index 4ad7589d..7f18d3ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,8 +764,14 @@ dependencies = [ name = "defuse-nep245" version = "0.1.0" dependencies = [ + "borsh", + "chrono", + "defuse-borsh-utils", "derive_more 2.0.1", + "hex", "near-sdk", + "schemars", + "serde_with", ] [[package]] diff --git a/nep245/Cargo.toml b/nep245/Cargo.toml index 44cff152..60c777c6 100644 --- a/nep245/Cargo.toml +++ b/nep245/Cargo.toml @@ -6,8 +6,16 @@ rust-version.workspace = true repository.workspace = true [dependencies] -derive_more.workspace = true +derive_more = { workspace = true, features = ["from"] } near-sdk.workspace = true +borsh = { version = "1.5.7", features = ["unstable__schema"] } +chrono = { workspace = true, default-features = false, features = ["serde"] } +serde_with = { workspace = true, features = ["chrono_0_4", "schemars_0_8"] } +defuse-borsh-utils = { workspace = true, features = ["chrono"] } +schemars.workspace = true + +[dev-dependencies] +hex = "0.4.3" [lints] workspace = true diff --git a/nep245/src/lib.rs b/nep245/src/lib.rs index 457d8bdd..ce82d227 100644 --- a/nep245/src/lib.rs +++ b/nep245/src/lib.rs @@ -1,6 +1,7 @@ mod core; pub mod enumeration; mod events; +pub mod metadata; pub mod receiver; pub mod resolver; mod token; diff --git a/nep245/src/metadata.rs b/nep245/src/metadata.rs new file mode 100644 index 00000000..3a4f0476 --- /dev/null +++ b/nep245/src/metadata.rs @@ -0,0 +1,198 @@ +//! This module presents traits according to [multi-token metadata extension](https://github.com/near/NEPs/blob/master/specs/Standards/Tokens/MultiToken/Metadata.md) + +use crate::TokenId; +use crate::enumeration::MultiTokenEnumeration; +use crate::metadata::adapters::As; +use borsh::schema::{Declaration, Definition}; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use chrono::{DateTime, Utc}; +use defuse_borsh_utils::adapters; +use near_sdk::near; +use near_sdk::serde::{Deserialize, Serialize}; +use schemars::JsonSchema; +use schemars::r#gen::SchemaGenerator; +use schemars::schema::Schema; +use serde_with::serde_as; +use serde_with::skip_serializing_none; +use std::collections::BTreeMap; + +pub type MetadataId = String; + +#[derive(Debug, Clone)] +#[near(serializers = [json, borsh])] +pub struct MTContractMetadata { + pub spec: String, // "a string that MUST be formatted mt-1.0.0" or whatever the spec version is used. + pub name: String, +} + +#[derive(Debug, Clone)] +#[skip_serializing_none] +#[near(serializers = [json, borsh])] +pub struct MTBaseTokenMetadata { + /// Human‐readable name of the base (e.g., "Silver Swords" or "Metaverse 3") + pub name: String, + + /// Unique identifier for this metadata entry + pub id: MetadataId, + + /// Abbreviated symbol for the token (e.g., "MOCHI"), or `None` if unset + pub symbol: Option, + + /// Data URL for a small icon image, or `None` + pub icon: Option, + + /// Number of decimals (useful if this base represents an FT‐style token), or `None` + pub decimals: Option, + + /// Centralized gateway URL for reliably accessing decentralized storage assets referenced by `reference` or `media`, or `None` + pub base_uri: Option, + + /// URL pointing to a JSON file with additional info, or `None` + pub reference: Option, + + /// Number of copies of this set of metadata that existed when the token was minted, or `None` + pub copies: Option, + + /// Base64‐encoded SHA-256 hash of the JSON from `reference`; required if `reference` is set, or `None` + pub reference_hash: Option, +} + +#[derive(Debug, Clone)] +#[skip_serializing_none] +#[near(serializers = [json, borsh])] +pub struct MTTokenMetadata { + /// Title of this specific token (e.g., "Arch Nemesis: Mail Carrier" or "Parcel #5055"), or `None` + pub title: Option, + + /// Free-form description of this token, or `None` + pub description: Option, + + /// URL to associated media (ideally decentralized, content-addressed storage), or `None` + pub media: Option, + + /// Base64‐encoded SHA-256 hash of the content referenced by `media`; required if `media` is set, or `None` + pub media_hash: Option, + + /// Unix epoch in milliseconds or RFC3339 when this token was issued or minted, or `None` + pub issued_at: Option, + + /// Unix epoch in milliseconds or RFC3339 when this token expires, or `None` + pub expires_at: Option, + + /// Unix epoch in milliseconds or RFC3339 when this token starts being valid, or `None` + pub starts_at: Option, + + /// Unix epoch in milliseconds or RFC3339 when this token metadata was last updated, or `None` + pub updated_at: Option, + + /// Anything extra the MT wants to store on-chain (can be stringified JSON), or `None` + pub extra: Option, + + /// URL to an off-chain JSON file with more info, or `None` + pub reference: Option, + + /// Base64‐encoded SHA-256 hash of the JSON from `reference`; required if `reference` is set, or `None` + pub reference_hash: Option, +} + +#[derive(Debug, Clone)] +#[near(serializers = [json, borsh])] +pub struct MTTokenMetadataAll { + pub base: MTBaseTokenMetadata, + pub token: MTTokenMetadata, +} + +pub trait MultiTokenMetadata { + /// Returns the contract‐level metadata (spec + name). + fn mt_metadata_contract(&self) -> MTContractMetadata; + + /// For a list of `token_ids`, returns a vector of combined `(base, token)` metadata. + fn mt_metadata_token_all(&self, token_ids: Vec) -> Vec>; + + /// Given `token_ids`, returns each token’s `MTTokenMetadata` or `None` if absent. + fn mt_metadata_token_by_token_id( + &self, + token_ids: Vec, + ) -> Vec>; + + /// Given `token_ids`, returns each token’s `MTBaseTokenMetadata` or `None` if absent. + fn mt_metadata_base_by_token_id( + &self, + token_ids: Vec, + ) -> Vec>; + + /// Given a list of `base_metadata_ids`, returns each `MTBaseTokenMetadata` or `None` if absent. + fn mt_metadata_base_by_metadata_id( + &self, + base_metadata_ids: Vec, + ) -> Vec>; +} + +/// The contract must implement the following view method if using [multi-token enumeration standard](https://nomicon.io/Standards/Tokens/MultiToken/Enumeration#interface). +pub trait MultiTokenMetadataEnumeration: MultiTokenMetadata + MultiTokenEnumeration { + /// Get list of all base metadata for the contract, with pagination. + /// + /// # Arguments + /// * `from_index`: an optional string representing an unsigned 128-bit integer, + /// indicating the starting index + /// * `limit`: an optional u64 indicating the maximum number of entries to return + /// + /// # Returns + /// A vector of `MTBaseTokenMetadata` objects, or an empty vector if none. + fn mt_tokens_base_metadata_all( + &self, + from_index: Option, + limit: Option, + ) -> Vec; +} + +/// A wrapper that implements Borsh de-/serialization for `Datetime` +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "::near_sdk::serde")] +#[serde_as] +pub struct DatetimeUtcWrapper( + #[serde_as(as = "PickFirst<(_, serde_with::TimestampMilliSeconds)>")] + #[borsh( + deserialize_with = "As::::deserialize", + serialize_with = "As::::serialize" + )] + pub DateTime, +); + +impl JsonSchema for DatetimeUtcWrapper { + fn schema_name() -> String { + "DatetimeUtcWrapper".to_owned() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + generator.subschema_for::() + } +} + +impl BorshSchema for DatetimeUtcWrapper { + fn add_definitions_recursively(definitions: &mut BTreeMap) { + ::add_definitions_recursively(definitions); + } + + fn declaration() -> Declaration { + ::declaration() + } +} + +#[cfg(test)] +mod tests { + use crate::metadata::DatetimeUtcWrapper; + use chrono::DateTime; + use hex::FromHex; + use near_sdk::borsh; + + #[test] + fn test_datetime_utc_wrapper_borsh() { + let timestamp = DateTime::from_timestamp(1747772412, 0).unwrap(); + let wrapped = DatetimeUtcWrapper(timestamp); + let encoded = borsh::to_vec(&wrapped).unwrap(); + assert_eq!(encoded, Vec::from_hex("60905aef96010000").unwrap()); + let actual_wrapped: DatetimeUtcWrapper = borsh::from_slice(encoded.as_slice()).unwrap(); + assert_eq!(actual_wrapped.0, wrapped.0); + } +}