Skip to main content

iota_types/stardust/output/
nft.rs

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