chia_sdk_driver/primitives/
nft.rs

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