chia_sdk_driver/primitives/nft/
nft_info.rs

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