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