chia_sdk_driver/primitives/
nft.rs

1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::{
3    LineageProof, Proof,
4    nft::{NftOwnershipLayerSolution, NftStateLayerSolution},
5    offer::{NotarizedPayment, SettlementPaymentsSolution},
6    singleton::SingletonSolution,
7};
8use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
9use chia_sdk_types::{
10    Conditions,
11    conditions::{TradePrice, TransferNft},
12};
13use chia_sha2::Sha256;
14use clvm_traits::{ToClvm, clvm_list};
15use clvm_utils::tree_hash;
16use clvmr::{Allocator, NodePtr};
17
18use crate::{
19    DriverError, HashedPtr, Layer, Puzzle, SettlementLayer, Singleton, SingletonInfo, Spend,
20    SpendContext, SpendWithConditions,
21};
22
23mod metadata_update;
24mod nft_info;
25mod nft_launcher;
26mod nft_mint;
27
28pub use metadata_update::*;
29pub use nft_info::*;
30pub use nft_mint::*;
31
32/// Contains all information needed to spend the outer puzzles of NFT coins.
33/// The [`NftInfo`] is used to construct the puzzle, but the [`Proof`] is needed for the solution.
34///
35/// The only thing missing to create a valid coin spend is the inner puzzle and solution.
36/// However, this is handled separately to provide as much flexibility as possible.
37///
38/// This type should contain all of the information you need to store in a database for later.
39/// As long as you can figure out what puzzle the p2 puzzle hash corresponds to and spend it,
40/// you have enough information to spend the NFT coin.
41pub type Nft = Singleton<NftInfo>;
42
43impl Nft {
44    /// Creates a new [`Nft`] that represents a child of this one.
45    pub fn child(
46        &self,
47        p2_puzzle_hash: Bytes32,
48        current_owner: Option<Bytes32>,
49        metadata: HashedPtr,
50        amount: u64,
51    ) -> Nft {
52        self.child_with(
53            NftInfo {
54                metadata,
55                current_owner,
56                p2_puzzle_hash,
57                ..self.info
58            },
59            amount,
60        )
61    }
62
63    /// Spends this NFT coin with the provided inner spend.
64    /// The spend is added to the [`SpendContext`] for convenience.
65    pub fn spend(&self, ctx: &mut SpendContext, inner_spend: Spend) -> Result<Self, DriverError> {
66        let layers = self.info.into_layers(inner_spend.puzzle);
67
68        let spend = layers.construct_spend(
69            ctx,
70            SingletonSolution {
71                lineage_proof: self.proof,
72                amount: self.coin.amount,
73                inner_solution: NftStateLayerSolution {
74                    inner_solution: NftOwnershipLayerSolution {
75                        inner_solution: inner_spend.solution,
76                    },
77                },
78            },
79        )?;
80
81        ctx.spend(self.coin, spend)?;
82
83        let (info, create_coin) = self.info.child_from_p2_spend(ctx, inner_spend)?;
84
85        Ok(self.child_with(info, create_coin.amount))
86    }
87
88    /// Spends this NFT coin with a [`Layer`] that supports [`SpendWithConditions`].
89    /// This is a building block for built in spend methods, but can also be used to spend
90    /// NFTs with conditions more easily.
91    ///
92    /// However, if you need full flexibility of the inner spend, you can use [`Nft::spend`] instead.
93    pub fn spend_with<I>(
94        &self,
95        ctx: &mut SpendContext,
96        inner: &I,
97        conditions: Conditions,
98    ) -> Result<Self, DriverError>
99    where
100        I: SpendWithConditions,
101    {
102        let inner_spend = inner.spend_with_conditions(ctx, conditions)?;
103        self.spend(ctx, inner_spend)
104    }
105
106    /// Transfers this NFT coin to a new p2 puzzle hash and runs the metadata updater with the
107    /// provided spend.
108    ///
109    /// This spend requires a [`Layer`] that supports [`SpendWithConditions`]. If it doesn't, you can
110    /// use [`Nft::spend_with`] instead.
111    pub fn transfer_with_metadata<I>(
112        self,
113        ctx: &mut SpendContext,
114        inner: &I,
115        p2_puzzle_hash: Bytes32,
116        metadata_update: Spend,
117        extra_conditions: Conditions,
118    ) -> Result<Nft, DriverError>
119    where
120        I: SpendWithConditions,
121    {
122        let memos = ctx.hint(p2_puzzle_hash)?;
123
124        self.spend_with(
125            ctx,
126            inner,
127            extra_conditions
128                .create_coin(p2_puzzle_hash, self.coin.amount, memos)
129                .update_nft_metadata(metadata_update.puzzle, metadata_update.solution),
130        )
131    }
132
133    /// Transfers this NFT coin to a new p2 puzzle hash.
134    ///
135    /// This spend requires a [`Layer`] that supports [`SpendWithConditions`]. If it doesn't, you can
136    /// use [`Nft::spend_with`] instead.
137    pub fn transfer<I>(
138        self,
139        ctx: &mut SpendContext,
140        inner: &I,
141        p2_puzzle_hash: Bytes32,
142        extra_conditions: Conditions,
143    ) -> Result<Nft, DriverError>
144    where
145        I: SpendWithConditions,
146    {
147        let memos = ctx.hint(p2_puzzle_hash)?;
148
149        self.spend_with(
150            ctx,
151            inner,
152            extra_conditions.create_coin(p2_puzzle_hash, self.coin.amount, memos),
153        )
154    }
155
156    /// Transfers this NFT coin to the settlement puzzle hash and runs the transfer program to
157    /// remove the assigned owner and reveal the trade prices for the offer.
158    ///
159    /// This spend requires a [`Layer`] that supports [`SpendWithConditions`]. If it doesn't, you can
160    /// use [`Nft::spend_with`] instead.
161    pub fn lock_settlement<I>(
162        self,
163        ctx: &mut SpendContext,
164        inner: &I,
165        trade_prices: Vec<TradePrice>,
166        extra_conditions: Conditions,
167    ) -> Result<Nft, DriverError>
168    where
169        I: SpendWithConditions,
170    {
171        let transfer_condition = TransferNft::new(None, trade_prices, None);
172
173        let (conditions, nft) = self.assign_owner(
174            ctx,
175            inner,
176            SETTLEMENT_PAYMENT_HASH.into(),
177            transfer_condition,
178            extra_conditions,
179        )?;
180
181        assert_eq!(conditions.len(), 0);
182
183        Ok(nft)
184    }
185
186    /// Spends this NFT with the settlement puzzle as its inner puzzle, with the provided notarized
187    /// payments. This only works if the NFT has been locked in an offer already.
188    pub fn unlock_settlement(
189        self,
190        ctx: &mut SpendContext,
191        notarized_payments: Vec<NotarizedPayment>,
192    ) -> Result<Nft, DriverError> {
193        let inner_spend = SettlementLayer
194            .construct_spend(ctx, SettlementPaymentsSolution { notarized_payments })?;
195
196        self.spend(ctx, inner_spend)
197    }
198
199    /// Transfers this NFT coin to a new p2 puzzle hash and runs the transfer program.
200    ///
201    /// This will return the conditions that must be emitted by the singleton you're assigning the NFT to.
202    /// The singleton must be spent in the same spend bundle as the NFT spend and emit these conditions.
203    ///
204    /// However, if the NFT is being unassigned, there is no singleton spend and the conditions are empty.
205    ///
206    /// This spend requires a [`Layer`] that supports [`SpendWithConditions`]. If it doesn't, you can
207    /// use [`Nft::spend_with`] instead.
208    pub fn assign_owner<I>(
209        self,
210        ctx: &mut SpendContext,
211        inner: &I,
212        p2_puzzle_hash: Bytes32,
213        transfer_condition: TransferNft,
214        extra_conditions: Conditions,
215    ) -> Result<(Conditions, Nft), DriverError>
216    where
217        I: SpendWithConditions,
218    {
219        let launcher_id = transfer_condition.launcher_id;
220
221        let assignment_conditions = if launcher_id.is_some() {
222            Conditions::new()
223                .assert_puzzle_announcement(assignment_puzzle_announcement_id(
224                    self.coin.puzzle_hash,
225                    &transfer_condition,
226                ))
227                .create_puzzle_announcement(self.info.launcher_id.into())
228        } else {
229            Conditions::new()
230        };
231
232        let memos = ctx.hint(p2_puzzle_hash)?;
233
234        let child = self.spend_with(
235            ctx,
236            inner,
237            extra_conditions
238                .create_coin(p2_puzzle_hash, self.coin.amount, memos)
239                .with(transfer_condition),
240        )?;
241
242        Ok((assignment_conditions, child))
243    }
244
245    /// Parses the child of an [`Nft`] from the parent coin spend.
246    ///
247    /// This can be used to construct a valid spendable [`Nft`] for a hinted coin.
248    /// You simply need to look up the parent coin's spend, parse the child, and
249    /// ensure it matches the hinted coin.
250    ///
251    /// This will automatically run the transfer program or metadata updater, if
252    /// they are revealed in the p2 spend's output conditions. This way the returned
253    /// [`Nft`] will have the correct owner (if present) and metadata.
254    pub fn parse_child(
255        allocator: &mut Allocator,
256        parent_coin: Coin,
257        parent_puzzle: Puzzle,
258        parent_solution: NodePtr,
259    ) -> Result<Option<Self>, DriverError> {
260        let Some((parent_info, p2_puzzle)) = NftInfo::parse(allocator, parent_puzzle)? else {
261            return Ok(None);
262        };
263
264        let p2_solution =
265            StandardNftLayers::<HashedPtr, Puzzle>::parse_solution(allocator, parent_solution)?
266                .inner_solution
267                .inner_solution
268                .inner_solution;
269
270        let (info, create_coin) =
271            parent_info.child_from_p2_spend(allocator, Spend::new(p2_puzzle.ptr(), p2_solution))?;
272
273        Ok(Some(Self {
274            coin: Coin::new(
275                parent_coin.coin_id(),
276                info.puzzle_hash().into(),
277                create_coin.amount,
278            ),
279            proof: Proof::Lineage(LineageProof {
280                parent_parent_coin_info: parent_coin.parent_coin_info,
281                parent_inner_puzzle_hash: parent_info.inner_puzzle_hash().into(),
282                parent_amount: parent_coin.amount,
283            }),
284            info,
285        }))
286    }
287
288    /// Parses an [`Nft`] and its p2 spend from a coin spend.
289    ///
290    /// If the puzzle is not an NFT, this will return [`None`] instead of an error.
291    /// However, if the puzzle should have been an NFT but had a parsing error, this will return an error.
292    pub fn parse(
293        allocator: &Allocator,
294        coin: Coin,
295        puzzle: Puzzle,
296        solution: NodePtr,
297    ) -> Result<Option<(Self, Puzzle, NodePtr)>, DriverError> {
298        let Some((nft_info, p2_puzzle)) = NftInfo::parse(allocator, puzzle)? else {
299            return Ok(None);
300        };
301
302        let solution = StandardNftLayers::<HashedPtr, Puzzle>::parse_solution(allocator, solution)?;
303
304        let p2_solution = solution.inner_solution.inner_solution.inner_solution;
305
306        Ok(Some((
307            Self::new(coin, solution.lineage_proof, nft_info),
308            p2_puzzle,
309            p2_solution,
310        )))
311    }
312}
313
314pub fn assignment_puzzle_announcement_id(
315    nft_full_puzzle_hash: Bytes32,
316    new_nft_owner: &TransferNft,
317) -> Bytes32 {
318    let mut allocator = Allocator::new();
319
320    let new_nft_owner_args = clvm_list!(
321        new_nft_owner.launcher_id,
322        &new_nft_owner.trade_prices,
323        new_nft_owner.singleton_inner_puzzle_hash
324    )
325    .to_clvm(&mut allocator)
326    .unwrap();
327
328    let mut hasher = Sha256::new();
329    hasher.update(nft_full_puzzle_hash);
330    hasher.update([0xad, 0x4c]);
331    hasher.update(tree_hash(&allocator, new_nft_owner_args));
332
333    Bytes32::new(hasher.finalize())
334}
335
336#[cfg(test)]
337mod tests {
338    use std::slice;
339
340    use crate::{IntermediateLauncher, Launcher, NftMint, SingletonInfo, StandardLayer};
341
342    use super::*;
343
344    use chia_puzzle_types::nft::NftMetadata;
345    use chia_sdk_test::Simulator;
346    use clvm_utils::ToTreeHash;
347
348    #[test]
349    fn test_nft_transfer() -> anyhow::Result<()> {
350        let mut sim = Simulator::new();
351        let ctx = &mut SpendContext::new();
352
353        let alice = sim.bls(2);
354        let alice_p2 = StandardLayer::new(alice.pk);
355
356        let (create_did, did) =
357            Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
358        alice_p2.spend(ctx, alice.coin, create_did)?;
359
360        let metadata = ctx.alloc_hashed(&NftMetadata::default())?;
361
362        let mint = NftMint::new(
363            metadata,
364            alice.puzzle_hash,
365            300,
366            Some(TransferNft::new(
367                Some(did.info.launcher_id),
368                Vec::new(),
369                Some(did.info.inner_puzzle_hash().into()),
370            )),
371        );
372
373        let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
374            .create(ctx)?
375            .mint_nft(ctx, &mint)?;
376        let _did = did.update(ctx, &alice_p2, mint_nft)?;
377        let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
378
379        sim.spend_coins(ctx.take(), &[alice.sk])?;
380
381        Ok(())
382    }
383
384    #[test]
385    fn test_nft_lineage() -> anyhow::Result<()> {
386        let mut sim = Simulator::new();
387        let ctx = &mut SpendContext::new();
388
389        let alice = sim.bls(2);
390        let alice_p2 = StandardLayer::new(alice.pk);
391
392        let (create_did, did) =
393            Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
394        alice_p2.spend(ctx, alice.coin, create_did)?;
395
396        let metadata = ctx.alloc_hashed(&NftMetadata::default())?;
397
398        let mint = NftMint::new(
399            metadata,
400            alice.puzzle_hash,
401            300,
402            Some(TransferNft::new(
403                Some(did.info.launcher_id),
404                Vec::new(),
405                Some(did.info.inner_puzzle_hash().into()),
406            )),
407        );
408
409        let (mint_nft, mut nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
410            .create(ctx)?
411            .mint_nft(ctx, &mint)?;
412
413        let mut did = did.update(ctx, &alice_p2, mint_nft)?;
414
415        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
416
417        for i in 0..5 {
418            let transfer_condition = TransferNft::new(
419                Some(did.info.launcher_id),
420                Vec::new(),
421                Some(did.info.inner_puzzle_hash().into()),
422            );
423
424            let (spend_nft, new_nft) = nft.assign_owner(
425                ctx,
426                &alice_p2,
427                alice.puzzle_hash,
428                if i % 2 == 0 {
429                    transfer_condition
430                } else {
431                    TransferNft::new(None, Vec::new(), None)
432                },
433                Conditions::new(),
434            )?;
435
436            nft = new_nft;
437            did = did.update(ctx, &alice_p2, spend_nft)?;
438        }
439
440        sim.spend_coins(ctx.take(), &[alice.sk])?;
441
442        Ok(())
443    }
444
445    #[test]
446    fn test_nft_metadata_update() -> anyhow::Result<()> {
447        let mut sim = Simulator::new();
448        let ctx = &mut SpendContext::new();
449
450        let alice = sim.bls(2);
451        let alice_p2 = StandardLayer::new(alice.pk);
452
453        let (create_did, did) =
454            Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
455        alice_p2.spend(ctx, alice.coin, create_did)?;
456
457        let metadata = ctx.alloc_hashed(&NftMetadata {
458            data_uris: vec!["example.com".to_string()],
459            data_hash: Some(Bytes32::default()),
460            ..Default::default()
461        })?;
462
463        let mint = NftMint::new(
464            metadata,
465            alice.puzzle_hash,
466            300,
467            Some(TransferNft::new(
468                Some(did.info.launcher_id),
469                Vec::new(),
470                Some(did.info.inner_puzzle_hash().into()),
471            )),
472        );
473
474        let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
475            .create(ctx)?
476            .mint_nft(ctx, &mint)?;
477        let _did = did.update(ctx, &alice_p2, mint_nft)?;
478
479        let metadata_update = MetadataUpdate {
480            kind: UriKind::Data,
481            uri: "another.com".to_string(),
482        }
483        .spend(ctx)?;
484        let parent_nft = nft;
485        let nft = nft.transfer_with_metadata(
486            ctx,
487            &alice_p2,
488            alice.puzzle_hash,
489            metadata_update,
490            Conditions::new(),
491        )?;
492
493        assert_eq!(
494            nft.info.metadata.tree_hash(),
495            NftMetadata {
496                data_uris: vec!["another.com".to_string(), "example.com".to_string()],
497                data_hash: Some(Bytes32::default()),
498                ..Default::default()
499            }
500            .tree_hash()
501        );
502
503        let child_nft = nft;
504        let _nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
505
506        sim.spend_coins(ctx.take(), &[alice.sk])?;
507
508        // Ensure that the metadata update can be parsed.
509        let parent_puzzle = sim
510            .puzzle_reveal(parent_nft.coin.coin_id())
511            .expect("missing puzzle");
512
513        let parent_solution = sim
514            .solution(parent_nft.coin.coin_id())
515            .expect("missing solution");
516
517        let parent_puzzle = parent_puzzle.to_clvm(ctx)?;
518        let parent_puzzle = Puzzle::parse(ctx, parent_puzzle);
519        let parent_solution = parent_solution.to_clvm(ctx)?;
520
521        let new_child_nft = Nft::parse_child(ctx, parent_nft.coin, parent_puzzle, parent_solution)?
522            .expect("child is not an NFT");
523
524        assert_eq!(new_child_nft, child_nft);
525
526        Ok(())
527    }
528
529    #[test]
530    fn test_parse_nft() -> anyhow::Result<()> {
531        let mut sim = Simulator::new();
532        let ctx = &mut SpendContext::new();
533
534        let alice = sim.bls(2);
535        let alice_p2 = StandardLayer::new(alice.pk);
536
537        let (create_did, did) =
538            Launcher::new(alice.coin.coin_id(), 1).create_simple_did(ctx, &alice_p2)?;
539        alice_p2.spend(ctx, alice.coin, create_did)?;
540
541        let mut metadata = NftMetadata::default();
542        metadata.data_uris.push("example.com".to_string());
543
544        let metadata = ctx.alloc_hashed(&metadata)?;
545
546        let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1)
547            .create(ctx)?
548            .mint_nft(
549                ctx,
550                &NftMint::new(
551                    metadata,
552                    alice.puzzle_hash,
553                    300,
554                    Some(TransferNft::new(
555                        Some(did.info.launcher_id),
556                        Vec::new(),
557                        Some(did.info.inner_puzzle_hash().into()),
558                    )),
559                ),
560            )?;
561        let _did = did.update(ctx, &alice_p2, mint_nft)?;
562
563        let parent_coin = nft.coin;
564        let expected_nft = nft.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
565
566        sim.spend_coins(ctx.take(), &[alice.sk])?;
567
568        let mut allocator = Allocator::new();
569
570        let puzzle_reveal = sim
571            .puzzle_reveal(parent_coin.coin_id())
572            .expect("missing puzzle")
573            .to_clvm(&mut allocator)?;
574
575        let solution = sim
576            .solution(parent_coin.coin_id())
577            .expect("missing solution")
578            .to_clvm(&mut allocator)?;
579
580        let puzzle = Puzzle::parse(&allocator, puzzle_reveal);
581
582        let nft = Nft::parse_child(&mut allocator, parent_coin, puzzle, solution)?
583            .expect("could not parse nft");
584
585        assert_eq!(nft, expected_nft);
586
587        Ok(())
588    }
589}