chia_sdk_driver/primitives/nft/
nft_info.rs

1use chia_protocol::Bytes32;
2use chia_puzzle_types::{
3    nft::{NftOwnershipLayerArgs, NftStateLayerArgs},
4    singleton::SingletonArgs,
5};
6use chia_puzzles::NFT_STATE_LAYER_HASH;
7use chia_sdk_types::{
8    conditions::{CreateCoin, NewMetadataOutput},
9    run_puzzle, Condition, Mod,
10};
11use clvm_traits::{clvm_list, FromClvm, ToClvm};
12use clvm_utils::{ToTreeHash, TreeHash};
13use clvmr::{Allocator, NodePtr};
14
15use crate::{
16    DriverError, Layer, NftOwnershipLayer, NftStateLayer, Puzzle, RoyaltyTransferLayer,
17    SingletonLayer, Spend,
18};
19
20pub type StandardNftLayers<M, I> =
21    SingletonLayer<NftStateLayer<M, NftOwnershipLayer<RoyaltyTransferLayer, I>>>;
22
23/// Information needed to construct the outer puzzle of an NFT.
24/// It does not include the inner puzzle, which must be stored separately.
25///
26/// This type can be used on its own for parsing, or as part of the [`Nft`](crate::Nft) primitive.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct NftInfo<M> {
29    /// The coin id of the launcher coin that created this NFT's singleton.
30    pub launcher_id: Bytes32,
31
32    /// The metadata stored in the [`NftStateLayer`]. This can only be updated by
33    /// going through the [`metadata_updater_puzzle_hash`](NftInfo::metadata_updater_puzzle_hash).
34    pub metadata: M,
35
36    /// The puzzle hash of the metadata updater. This is used to update the metadata of the NFT.
37    /// This is typically [`NFT_METADATA_UPDATER_DEFAULT_HASH`](chia_puzzles::NFT_METADATA_UPDATER_DEFAULT_HASH),
38    /// which ensures the [`NftMetadata`](chia_puzzle_types::nft::NftMetadata) object remains immutable
39    /// except for prepending additional URIs.
40    ///
41    /// A custom metadata updater can be used, however support in existing wallets and display
42    /// services may be limited.
43    pub metadata_updater_puzzle_hash: Bytes32,
44
45    /// The current assigned owner of the NFT, if any. This is managed by the [`NftOwnershipLayer`].
46    ///
47    /// Historically this was always a DID, although it's possible to assign any singleton including a vault.
48    ///
49    /// It's intended to unassign the owner after transferring to an external wallet or creating an offer.
50    pub current_owner: Option<Bytes32>,
51
52    /// The puzzle hash to which royalties will be paid out to in offers involving this NFT.
53    /// This is required even if the royalty is 0. Currently, all NFTs must use the default [`RoyaltyTransferLayer`],
54    /// however this may change in the future.
55    pub royalty_puzzle_hash: Bytes32,
56
57    /// The royalty percentage to be paid out to the owner in offers involving this NFT.
58    /// This is represented as hundredths of a percent, so 300 is 3%.
59    pub royalty_basis_points: u16,
60
61    /// The hash of the inner puzzle to this NFT.
62    /// If you encode this puzzle hash as bech32m, it's the same as the current owner's address.
63    pub p2_puzzle_hash: Bytes32,
64}
65
66impl<M> NftInfo<M> {
67    pub fn new(
68        launcher_id: Bytes32,
69        metadata: M,
70        metadata_updater_puzzle_hash: Bytes32,
71        current_owner: Option<Bytes32>,
72        royalty_puzzle_hash: Bytes32,
73        royalty_basis_points: u16,
74        p2_puzzle_hash: Bytes32,
75    ) -> Self {
76        Self {
77            launcher_id,
78            metadata,
79            metadata_updater_puzzle_hash,
80            current_owner,
81            royalty_puzzle_hash,
82            royalty_basis_points,
83            p2_puzzle_hash,
84        }
85    }
86
87    pub fn with_metadata<N>(self, metadata: N) -> NftInfo<N> {
88        NftInfo {
89            launcher_id: self.launcher_id,
90            metadata,
91            metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash,
92            current_owner: self.current_owner,
93            royalty_puzzle_hash: self.royalty_puzzle_hash,
94            royalty_basis_points: self.royalty_basis_points,
95            p2_puzzle_hash: self.p2_puzzle_hash,
96        }
97    }
98
99    /// Parses an [`NftInfo`] from a [`Puzzle`] by extracting the [`NftStateLayer`] and [`NftOwnershipLayer`].
100    ///
101    /// This will return a tuple of the [`NftInfo`] and its p2 puzzle.
102    ///
103    /// If the puzzle is not an NFT, this will return [`None`] instead of an error.
104    /// However, if the puzzle should have been an NFT but had a parsing error, this will return an error.
105    pub fn parse(
106        allocator: &Allocator,
107        puzzle: Puzzle,
108    ) -> Result<Option<(Self, Puzzle)>, DriverError>
109    where
110        M: ToClvm<Allocator> + FromClvm<Allocator>,
111    {
112        let Some(layers) = StandardNftLayers::<M, Puzzle>::parse_puzzle(allocator, puzzle)? else {
113            return Ok(None);
114        };
115
116        let p2_puzzle = layers.inner_puzzle.inner_puzzle.inner_puzzle;
117
118        Ok(Some((Self::from_layers(layers), p2_puzzle)))
119    }
120
121    pub fn from_layers<I>(layers: StandardNftLayers<M, I>) -> Self
122    where
123        I: ToTreeHash,
124    {
125        Self {
126            launcher_id: layers.launcher_id,
127            metadata: layers.inner_puzzle.metadata,
128            metadata_updater_puzzle_hash: layers.inner_puzzle.metadata_updater_puzzle_hash,
129            current_owner: layers.inner_puzzle.inner_puzzle.current_owner,
130            royalty_puzzle_hash: layers
131                .inner_puzzle
132                .inner_puzzle
133                .transfer_layer
134                .royalty_puzzle_hash,
135            royalty_basis_points: layers
136                .inner_puzzle
137                .inner_puzzle
138                .transfer_layer
139                .royalty_basis_points,
140            p2_puzzle_hash: layers
141                .inner_puzzle
142                .inner_puzzle
143                .inner_puzzle
144                .tree_hash()
145                .into(),
146        }
147    }
148
149    #[must_use]
150    pub fn into_layers<I>(self, p2_puzzle: I) -> StandardNftLayers<M, I> {
151        SingletonLayer::new(
152            self.launcher_id,
153            NftStateLayer::new(
154                self.metadata,
155                self.metadata_updater_puzzle_hash,
156                NftOwnershipLayer::new(
157                    self.current_owner,
158                    RoyaltyTransferLayer::new(
159                        self.launcher_id,
160                        self.royalty_puzzle_hash,
161                        self.royalty_basis_points,
162                    ),
163                    p2_puzzle,
164                ),
165            ),
166        )
167    }
168
169    /// Calculates the inner puzzle hash of the NFT singleton.
170    ///
171    /// This includes both the [`NftStateLayer`] and [`NftOwnershipLayer`], but not the [`SingletonLayer`].
172    pub fn inner_puzzle_hash(&self) -> TreeHash
173    where
174        M: ToTreeHash,
175    {
176        NftStateLayerArgs {
177            mod_hash: NFT_STATE_LAYER_HASH.into(),
178            metadata: self.metadata.tree_hash(),
179            metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash,
180            inner_puzzle: NftOwnershipLayerArgs::curry_tree_hash(
181                self.current_owner,
182                RoyaltyTransferLayer::new(
183                    self.launcher_id,
184                    self.royalty_puzzle_hash,
185                    self.royalty_basis_points,
186                )
187                .tree_hash(),
188                self.p2_puzzle_hash.into(),
189            ),
190        }
191        .curry_tree_hash()
192    }
193
194    /// Calculates the full puzzle hash of the NFT, which is the hash of the outer [`SingletonLayer`].
195    pub fn puzzle_hash(&self) -> TreeHash
196    where
197        M: ToTreeHash,
198    {
199        SingletonArgs::new(self.launcher_id, self.inner_puzzle_hash()).curry_tree_hash()
200    }
201
202    /// Parses the child of an [`NftInfo`] from the p2 spend.
203    ///
204    /// This will automatically run the transfer program or metadata updater, if
205    /// they are revealed in the p2 spend's output conditions. This way the returned
206    /// [`NftInfo`] will have the correct owner (if present) and metadata.
207    pub fn child_from_p2_spend(
208        &self,
209        allocator: &mut Allocator,
210        spend: Spend,
211    ) -> Result<(Self, CreateCoin<NodePtr>), DriverError>
212    where
213        M: Clone + ToClvm<Allocator> + FromClvm<Allocator>,
214    {
215        let output = run_puzzle(allocator, spend.puzzle, spend.solution)?;
216        let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
217
218        let mut create_coin = None;
219        let mut new_owner = None;
220        let mut new_metadata = None;
221
222        for condition in conditions {
223            match condition {
224                Condition::CreateCoin(condition) if condition.amount % 2 == 1 => {
225                    create_coin = Some(condition);
226                }
227                Condition::TransferNft(condition) => {
228                    new_owner = Some(condition);
229                }
230                Condition::UpdateNftMetadata(condition) => {
231                    new_metadata = Some(condition);
232                }
233                _ => {}
234            }
235        }
236
237        let Some(create_coin) = create_coin else {
238            return Err(DriverError::MissingChild);
239        };
240
241        let mut info = self.clone();
242
243        if let Some(new_owner) = new_owner {
244            info.current_owner = new_owner.launcher_id;
245        }
246
247        if let Some(new_metadata) = new_metadata {
248            let metadata_updater_solution = clvm_list!(
249                &self.metadata,
250                self.metadata_updater_puzzle_hash,
251                new_metadata.updater_solution
252            )
253            .to_clvm(allocator)?;
254
255            let output = run_puzzle(
256                allocator,
257                new_metadata.updater_puzzle_reveal,
258                metadata_updater_solution,
259            )?;
260
261            let output =
262                NewMetadataOutput::<M, NodePtr>::from_clvm(allocator, output)?.metadata_info;
263            info.metadata = output.new_metadata;
264            info.metadata_updater_puzzle_hash = output.new_updater_puzzle_hash;
265        }
266
267        info.p2_puzzle_hash = create_coin.puzzle_hash;
268
269        Ok((info, create_coin))
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use chia_puzzle_types::nft::NftMetadata;
276    use chia_sdk_test::Simulator;
277    use chia_sdk_types::{conditions::TransferNft, Conditions};
278
279    use crate::{IntermediateLauncher, Launcher, NftMint, SpendContext, StandardLayer};
280
281    use super::*;
282
283    #[test]
284    fn test_parse_nft_info() -> anyhow::Result<()> {
285        let mut sim = Simulator::new();
286        let ctx = &mut SpendContext::new();
287
288        let alice = sim.bls(2);
289        let alice_p2 = StandardLayer::new(alice.pk);
290
291        let (create_did, did) =
292            Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
293        alice_p2.spend(ctx, alice.coin, create_did)?;
294
295        let mut metadata = NftMetadata::default();
296        metadata.data_uris.push("example.com".to_string());
297
298        let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
299            .create(ctx)?
300            .mint_nft(
301                ctx,
302                NftMint::new(
303                    metadata,
304                    alice.puzzle_hash,
305                    300,
306                    Some(TransferNft::new(
307                        Some(did.info.launcher_id),
308                        Vec::new(),
309                        Some(did.info.inner_puzzle_hash().into()),
310                    )),
311                ),
312            )?;
313
314        let _did = did.update(ctx, &alice_p2, mint_nft)?;
315        let original_nft = nft.clone();
316        let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
317
318        sim.spend_coins(ctx.take(), &[alice.sk])?;
319
320        let puzzle_reveal = sim
321            .puzzle_reveal(original_nft.coin.coin_id())
322            .expect("missing nft puzzle");
323
324        let mut allocator = Allocator::new();
325        let ptr = puzzle_reveal.to_clvm(&mut allocator)?;
326        let puzzle = Puzzle::parse(&allocator, ptr);
327        let (nft_info, p2_puzzle) =
328            NftInfo::<NftMetadata>::parse(&allocator, puzzle)?.expect("not an nft");
329
330        assert_eq!(nft_info, original_nft.info);
331        assert_eq!(p2_puzzle.curried_puzzle_hash(), alice.puzzle_hash.into());
332
333        Ok(())
334    }
335}