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