iota_types/stardust/output/
nft.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use move_core_types::{ident_str, identifier::IdentStr, language_storage::StructTag};
5use serde::{Deserialize, Serialize};
6use serde_with::serde_as;
7
8use super::unlock_conditions::{
9    ExpirationUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition,
10};
11use crate::{
12    STARDUST_ADDRESS, TypeTag,
13    balance::Balance,
14    base_types::IotaAddress,
15    collection_types::{Bag, VecMap},
16    error::IotaError,
17    id::UID,
18    object::{Data, Object},
19};
20
21pub const IRC27_MODULE_NAME: &IdentStr = ident_str!("irc27");
22pub const NFT_MODULE_NAME: &IdentStr = ident_str!("nft");
23pub const NFT_OUTPUT_MODULE_NAME: &IdentStr = ident_str!("nft_output");
24pub const NFT_OUTPUT_STRUCT_NAME: &IdentStr = ident_str!("NftOutput");
25pub const NFT_STRUCT_NAME: &IdentStr = ident_str!("Nft");
26pub const IRC27_STRUCT_NAME: &IdentStr = ident_str!("Irc27Metadata");
27pub const NFT_DYNAMIC_OBJECT_FIELD_KEY: &[u8] = b"nft";
28pub const NFT_DYNAMIC_OBJECT_FIELD_KEY_TYPE: &str = "vector<u8>";
29
30/// Rust version of the Move std::fixed_point32::FixedPoint32 type.
31#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
32pub struct FixedPoint32 {
33    pub value: u64,
34}
35
36/// Rust version of the Move iota::url::Url type.
37#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
38pub struct Url {
39    /// The underlying URL as a string.
40    ///
41    /// # SAFETY
42    ///
43    /// Note that this String is UTF-8 encoded while the URL type in Move is
44    /// ascii-encoded. Setting this field requires ensuring that the string
45    /// consists of only ASCII characters.
46    url: String,
47}
48
49impl Url {
50    pub fn url(&self) -> &str {
51        &self.url
52    }
53}
54
55impl TryFrom<String> for Url {
56    type Error = anyhow::Error;
57
58    /// Creates a new `Url` ensuring that it only consists of ascii characters.
59    fn try_from(url: String) -> Result<Self, Self::Error> {
60        if !url.is_ascii() {
61            anyhow::bail!("url `{url}` does not consist of only ascii characters")
62        }
63        Ok(Self { url })
64    }
65}
66
67#[serde_as]
68#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
69pub struct Irc27Metadata {
70    /// Version of the metadata standard.
71    pub version: String,
72
73    /// The media type (MIME) of the asset.
74    ///
75    /// ## Examples
76    /// - Image files: `image/jpeg`, `image/png`, `image/gif`, etc.
77    /// - Video files: `video/x-msvideo` (avi), `video/mp4`, `video/mpeg`, etc.
78    /// - Audio files: `audio/mpeg`, `audio/wav`, etc.
79    /// - 3D Assets: `model/obj`, `model/u3d`, etc.
80    /// - Documents: `application/pdf`, `text/plain`, etc.
81    pub media_type: String,
82
83    /// URL pointing to the NFT file location.
84    pub uri: Url,
85
86    /// Alphanumeric text string defining the human identifiable name for the
87    /// NFT.
88    pub name: String,
89
90    /// The human-readable collection name of the NFT.
91    pub collection_name: Option<String>,
92
93    /// Royalty payment addresses mapped to the payout percentage.
94    /// Contains a hash of the 32 bytes parsed from the BECH32 encoded IOTA
95    /// address in the metadata, it is a legacy address. Royalties are not
96    /// supported by the protocol and needed to be processed by an integrator.
97    pub royalties: VecMap<IotaAddress, FixedPoint32>,
98
99    /// The human-readable name of the NFT creator.
100    pub issuer_name: Option<String>,
101
102    /// The human-readable description of the NFT.
103    pub description: Option<String>,
104
105    /// Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards).
106    pub attributes: VecMap<String, String>,
107
108    /// Legacy non-standard metadata fields.
109    pub non_standard_fields: VecMap<String, String>,
110}
111
112#[serde_as]
113#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
114pub struct Nft {
115    /// The ID of the Nft = hash of the Output ID that created the Nft Output in
116    /// Stardust. This is the NftID from Stardust.
117    pub id: UID,
118
119    /// The sender feature holds the last sender address assigned before the
120    /// migration and is not supported by the protocol after it.
121    pub legacy_sender: Option<IotaAddress>,
122    /// The metadata feature.
123    pub metadata: Option<Vec<u8>>,
124    /// The tag feature.
125    pub tag: Option<Vec<u8>>,
126
127    /// The immutable issuer feature.
128    pub immutable_issuer: Option<IotaAddress>,
129    /// The immutable metadata feature.
130    pub immutable_metadata: Irc27Metadata,
131}
132
133impl Nft {
134    /// Returns the struct tag that represents the fully qualified path of an
135    /// [`Nft`] in its move package.
136    pub fn tag() -> StructTag {
137        StructTag {
138            address: STARDUST_ADDRESS,
139            module: NFT_MODULE_NAME.to_owned(),
140            name: NFT_STRUCT_NAME.to_owned(),
141            type_params: Vec::new(),
142        }
143    }
144}
145
146#[serde_as]
147#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
148pub struct NftOutput {
149    /// This is a "random" UID, not the NftID from Stardust.
150    pub id: UID,
151
152    /// The amount of IOTA coins held by the output.
153    pub balance: Balance,
154    /// The `Bag` holds native tokens, key-ed by the stringified type of the
155    /// asset. Example: key: "0xabcded::soon::SOON", value:
156    /// Balance<0xabcded::soon::SOON>.
157    pub native_tokens: Bag,
158
159    /// The storage deposit return unlock condition.
160    pub storage_deposit_return: Option<StorageDepositReturnUnlockCondition>,
161    /// The timelock unlock condition.
162    pub timelock: Option<TimelockUnlockCondition>,
163    /// The expiration unlock condition.
164    pub expiration: Option<ExpirationUnlockCondition>,
165}
166
167impl NftOutput {
168    /// Returns the struct tag that represents the fully qualified path of an
169    /// [`NftOutput`] in its move package.
170    pub fn tag(type_param: TypeTag) -> StructTag {
171        StructTag {
172            address: STARDUST_ADDRESS,
173            module: NFT_OUTPUT_MODULE_NAME.to_owned(),
174            name: NFT_OUTPUT_STRUCT_NAME.to_owned(),
175            type_params: vec![type_param],
176        }
177    }
178
179    /// Create an `NftOutput` from BCS bytes.
180    pub fn from_bcs_bytes(content: &[u8]) -> Result<Self, IotaError> {
181        bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization {
182            error: format!("Unable to deserialize NftOutput object: {err:?}"),
183        })
184    }
185
186    pub fn is_nft_output(s: &StructTag) -> bool {
187        s.address == STARDUST_ADDRESS
188            && s.module.as_ident_str() == NFT_OUTPUT_MODULE_NAME
189            && s.name.as_ident_str() == NFT_OUTPUT_STRUCT_NAME
190    }
191}
192
193impl TryFrom<&Object> for NftOutput {
194    type Error = IotaError;
195    fn try_from(object: &Object) -> Result<Self, Self::Error> {
196        match &object.data {
197            Data::Move(o) => {
198                if o.type_().is_nft_output() {
199                    return NftOutput::from_bcs_bytes(o.contents());
200                }
201            }
202            Data::Package(_) => {}
203        }
204
205        Err(IotaError::Type {
206            error: format!("Object type is not a NftOutput: {object:?}"),
207        })
208    }
209}