iota_types/stardust/output/
nft.rs

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