Skip to main content

chia_sdk_driver/primitives/
cat.rs

1use chia_bls::PublicKey;
2use chia_protocol::{Bytes32, Coin};
3use chia_puzzle_types::{
4    CoinProof, LineageProof, Memos,
5    cat::{CatSolution, EverythingWithSignatureTailArgs, GenesisByCoinIdTailArgs},
6};
7use chia_sdk_types::{
8    Condition, Conditions,
9    conditions::{CreateCoin, RunCatTail},
10    puzzles::RevocationSolution,
11    run_puzzle,
12};
13use clvm_traits::FromClvm;
14use clvm_utils::ToTreeHash;
15use clvmr::{Allocator, NodePtr};
16
17use crate::{CatLayer, DriverError, Layer, Puzzle, RevocationLayer, Spend, SpendContext};
18
19mod cat_info;
20mod cat_spend;
21mod parsed_cat;
22mod single_cat_spend;
23
24pub use cat_info::*;
25pub use cat_spend::*;
26pub use parsed_cat::*;
27pub use single_cat_spend::*;
28
29/// Contains all information needed to spend the outer puzzles of CAT coins.
30/// The [`CatInfo`] is used to construct the puzzle, but the [`LineageProof`] is needed for the solution.
31///
32/// The only thing missing to create a valid coin spend is the inner puzzle and solution.
33/// However, this is handled separately to provide as much flexibility as possible.
34///
35/// This type should contain all of the information you need to store in a database for later.
36/// As long as you can figure out what puzzle the p2 puzzle hash corresponds to and spend it,
37/// you have enough information to spend the CAT coin.
38#[must_use]
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Cat {
41    /// The coin that this [`Cat`] represents. Its puzzle hash should match the [`CatInfo::puzzle_hash`].
42    pub coin: Coin,
43
44    /// The lineage proof is needed by the CAT puzzle to prove that this coin is a legitimate CAT.
45    /// It's typically obtained by looking up and parsing the parent coin.
46    ///
47    /// This can get a bit tedious, so a helper method [`Cat::parse_children`] is provided to parse
48    /// the child [`Cat`] objects from the parent (once you have looked up its information on-chain).
49    ///
50    /// Note that while the lineage proof is needed for most coins, it is optional if you are
51    /// issuing more of the CAT by running its TAIL program.
52    pub lineage_proof: Option<LineageProof>,
53
54    /// The information needed to construct the outer puzzle of a CAT. See [`CatInfo`] for more details.
55    pub info: CatInfo,
56}
57
58impl Cat {
59    pub fn new(coin: Coin, lineage_proof: Option<LineageProof>, info: CatInfo) -> Self {
60        Self {
61            coin,
62            lineage_proof,
63            info,
64        }
65    }
66
67    pub fn single_issuance(
68        ctx: &mut SpendContext,
69        parent_coin_id: Bytes32,
70        hidden_puzzle_hash: Option<Bytes32>,
71        amount: u64,
72        extra_conditions: Conditions,
73    ) -> Result<(Conditions, Vec<Cat>), DriverError> {
74        let tail = ctx.curry(GenesisByCoinIdTailArgs::new(parent_coin_id))?;
75
76        Self::issue(
77            ctx,
78            parent_coin_id,
79            hidden_puzzle_hash,
80            amount,
81            RunCatTail::new(tail, NodePtr::NIL),
82            extra_conditions,
83        )
84    }
85
86    pub fn multi_issuance(
87        ctx: &mut SpendContext,
88        parent_coin_id: Bytes32,
89        public_key: PublicKey,
90        hidden_puzzle_hash: Option<Bytes32>,
91        amount: u64,
92        extra_conditions: Conditions,
93    ) -> Result<(Conditions, Vec<Cat>), DriverError> {
94        let tail = ctx.curry(EverythingWithSignatureTailArgs::new(public_key))?;
95
96        Self::issue(
97            ctx,
98            parent_coin_id,
99            hidden_puzzle_hash,
100            amount,
101            RunCatTail::new(tail, NodePtr::NIL),
102            extra_conditions,
103        )
104    }
105
106    pub fn issue(
107        ctx: &mut SpendContext,
108        parent_coin_id: Bytes32,
109        hidden_puzzle_hash: Option<Bytes32>,
110        amount: u64,
111        run_tail: RunCatTail<NodePtr, NodePtr>,
112        conditions: Conditions,
113    ) -> Result<(Conditions, Vec<Cat>), DriverError> {
114        let delegated_spend = ctx.delegated_spend(conditions.with(run_tail))?;
115        let eve_info = CatInfo::new(
116            ctx.tree_hash(run_tail.program).into(),
117            hidden_puzzle_hash,
118            ctx.tree_hash(delegated_spend.puzzle).into(),
119        );
120
121        let eve = Cat::new(
122            Coin::new(parent_coin_id, eve_info.puzzle_hash().into(), amount),
123            None,
124            eve_info,
125        );
126
127        let children = Cat::spend_all(ctx, &[CatSpend::new(eve, delegated_spend)])?;
128
129        Ok((
130            Conditions::new().create_coin(eve.coin.puzzle_hash, eve.coin.amount, Memos::None),
131            children,
132        ))
133    }
134
135    /// Constructs a [`CoinSpend`](chia_protocol::CoinSpend) for each [`CatSpend`] in the list.
136    /// The spends are added to the [`SpendContext`] (in order) for convenience.
137    ///
138    /// All of the ring announcements and proofs required by the CAT puzzle are calculated automatically.
139    /// This requires running the inner spends to get the conditions, so any errors will be propagated.
140    ///
141    /// It's important not to spend CATs with different asset IDs at the same time, since they are not
142    /// compatible.
143    ///
144    /// Additionally, you should group all CAT spends done in the same transaction together
145    /// so that the value of one coin can be freely used in the output of another. If you spend them
146    /// separately, there will be multiple announcement rings and a non-zero delta will be calculated.
147    pub fn spend_all(
148        ctx: &mut SpendContext,
149        cat_spends: &[CatSpend],
150    ) -> Result<Vec<Cat>, DriverError> {
151        let len = cat_spends.len();
152
153        let mut total_delta = 0;
154        let mut prev_subtotals = Vec::new();
155        let mut run_tail_index = None;
156        let mut children = Vec::new();
157
158        for (index, &item) in cat_spends.iter().enumerate() {
159            let output = ctx.run(item.spend.puzzle, item.spend.solution)?;
160            let conditions: Vec<Condition> = ctx.extract(output)?;
161
162            // If this is the first TAIL reveal, we're going to keep track of it
163            if run_tail_index.is_none() && conditions.iter().any(Condition::is_run_cat_tail) {
164                run_tail_index = Some(index);
165            }
166
167            let create_coins: Vec<CreateCoin<NodePtr>> = conditions
168                .into_iter()
169                .filter_map(Condition::into_create_coin)
170                .collect();
171
172            // Calculate the delta of inputs and outputs
173            let delta = create_coins
174                .iter()
175                .fold(i128::from(item.cat.coin.amount), |delta, create_coin| {
176                    delta - i128::from(create_coin.amount)
177                });
178
179            // Add the previous subtotal for this coin
180            prev_subtotals.push(total_delta);
181
182            // Add the delta to the total
183            total_delta += delta;
184
185            for create_coin in create_coins {
186                children.push(
187                    item.cat
188                        .child_from_p2_create_coin(ctx, create_coin, item.hidden),
189                );
190            }
191        }
192
193        // If the TAIL was revealed, we need to adjust the subsequent previous subtotals to account for the extra delta
194        if let Some(tail_index) = run_tail_index {
195            let tail_adjustment = -total_delta;
196
197            prev_subtotals
198                .iter_mut()
199                .skip(tail_index + 1)
200                .for_each(|subtotal| {
201                    *subtotal += tail_adjustment;
202                });
203        }
204
205        for (index, item) in cat_spends.iter().enumerate() {
206            // Find information of neighboring coins on the ring.
207            let prev = &cat_spends[if index == 0 { len - 1 } else { index - 1 }];
208            let next = &cat_spends[if index == len - 1 { 0 } else { index + 1 }];
209
210            let next_inner_puzzle_hash = next.cat.info.inner_puzzle_hash();
211
212            item.cat.spend(
213                ctx,
214                SingleCatSpend {
215                    p2_spend: item.spend,
216                    prev_coin_id: prev.cat.coin.coin_id(),
217                    next_coin_proof: CoinProof {
218                        parent_coin_info: next.cat.coin.parent_coin_info,
219                        inner_puzzle_hash: next_inner_puzzle_hash.into(),
220                        amount: next.cat.coin.amount,
221                    },
222                    prev_subtotal: prev_subtotals[index].try_into()?,
223                    // If the TAIL was revealed, we need to add the extra delta needed to net the spend to zero
224                    extra_delta: if run_tail_index.is_some_and(|i| i == index) {
225                        -total_delta.try_into()?
226                    } else {
227                        0
228                    },
229                    revoke: item.hidden,
230                },
231            )?;
232        }
233
234        Ok(children)
235    }
236
237    /// Spends this CAT coin with the provided solution parameters. Other parameters are inferred from
238    /// the [`Cat`] instance.
239    ///
240    /// This is useful if you have already calculated the conditions and want to spend the coin directly.
241    /// However, it's more common to use [`Cat::spend_all`] which handles the details of calculating the
242    /// solution (including ring announcements) for multiple CATs and spending them all at once.
243    pub fn spend(&self, ctx: &mut SpendContext, info: SingleCatSpend) -> Result<(), DriverError> {
244        let mut spend = info.p2_spend;
245
246        if let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash {
247            spend = RevocationLayer::new(hidden_puzzle_hash, self.info.p2_puzzle_hash)
248                .construct_spend(
249                    ctx,
250                    RevocationSolution::new(info.revoke, spend.puzzle, spend.solution),
251                )?;
252        }
253
254        spend = CatLayer::new(self.info.asset_id, spend.puzzle).construct_spend(
255            ctx,
256            CatSolution {
257                lineage_proof: self.lineage_proof,
258                inner_puzzle_solution: spend.solution,
259                prev_coin_id: info.prev_coin_id,
260                this_coin_info: self.coin,
261                next_coin_proof: info.next_coin_proof,
262                extra_delta: info.extra_delta,
263                prev_subtotal: info.prev_subtotal,
264            },
265        )?;
266
267        ctx.spend(self.coin, spend)?;
268
269        Ok(())
270    }
271
272    /// Creates a [`LineageProof`] for which would be valid for any children created by this [`Cat`].
273    pub fn child_lineage_proof(&self) -> LineageProof {
274        LineageProof {
275            parent_parent_coin_info: self.coin.parent_coin_info,
276            parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
277            parent_amount: self.coin.amount,
278        }
279    }
280
281    /// Creates a new [`Cat`] that represents a child of this one.
282    /// The child will have the same revocation layer (or lack thereof) as the current [`Cat`].
283    ///
284    /// If you need to construct a child without the revocation layer, use [`Cat::unrevocable_child`].
285    pub fn child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
286        self.child_with(
287            CatInfo {
288                p2_puzzle_hash,
289                ..self.info
290            },
291            amount,
292        )
293    }
294
295    /// Creates a new [`Cat`] that represents a child of this one.
296    /// The child will not have a revocation layer.
297    ///
298    /// If you need to construct a child with the same revocation layer, use [`Cat::child`].
299    pub fn unrevocable_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
300        self.child_with(
301            CatInfo {
302                p2_puzzle_hash,
303                hidden_puzzle_hash: None,
304                ..self.info
305            },
306            amount,
307        )
308    }
309
310    /// Creates a new [`Cat`] that represents a child of this one.
311    ///
312    /// You can specify the [`CatInfo`] to use for the child manually.
313    /// In most cases, you will want to use [`Cat::child`] or [`Cat::unrevocable_child`] instead.
314    pub fn child_with(&self, info: CatInfo, amount: u64) -> Self {
315        Self {
316            coin: Coin::new(self.coin.coin_id(), info.puzzle_hash().into(), amount),
317            lineage_proof: Some(self.child_lineage_proof()),
318            info,
319        }
320    }
321
322    /// Parses a [`Cat`] and its p2 spend from a coin spend by extracting the [`CatLayer`] and [`RevocationLayer`] if present.
323    ///
324    /// If the puzzle is not a CAT, this will return [`None`] instead of an error.
325    /// However, if the puzzle should have been a CAT but had a parsing error, this will return an error.
326    pub fn parse(
327        allocator: &Allocator,
328        coin: Coin,
329        puzzle: Puzzle,
330        solution: NodePtr,
331    ) -> Result<Option<ParsedCat>, DriverError> {
332        let Some(cat_layer) = CatLayer::<Puzzle>::parse_puzzle(allocator, puzzle)? else {
333            return Ok(None);
334        };
335        let cat_solution = CatLayer::<Puzzle>::parse_solution(allocator, solution)?;
336
337        if let Some(revocation_layer) =
338            RevocationLayer::parse_puzzle(allocator, cat_layer.inner_puzzle)?
339        {
340            let revocation_solution =
341                RevocationLayer::parse_solution(allocator, cat_solution.inner_puzzle_solution)?;
342
343            let cat = Self::new(
344                coin,
345                cat_solution.lineage_proof,
346                CatInfo::new(
347                    cat_layer.asset_id,
348                    Some(revocation_layer.hidden_puzzle_hash),
349                    revocation_layer.inner_puzzle_hash,
350                ),
351            );
352
353            Ok(Some(ParsedCat {
354                cat,
355                p2_puzzle: Puzzle::parse(allocator, revocation_solution.puzzle),
356                p2_solution: revocation_solution.solution,
357                revoked: revocation_solution.hidden,
358            }))
359        } else {
360            let cat = Self::new(
361                coin,
362                cat_solution.lineage_proof,
363                CatInfo::new(
364                    cat_layer.asset_id,
365                    None,
366                    cat_layer.inner_puzzle.curried_puzzle_hash().into(),
367                ),
368            );
369
370            Ok(Some(ParsedCat {
371                cat,
372                p2_puzzle: cat_layer.inner_puzzle,
373                p2_solution: cat_solution.inner_puzzle_solution,
374                revoked: false,
375            }))
376        }
377    }
378
379    /// Parses the children of a [`Cat`] from the parent coin spend.
380    ///
381    /// This can be used to construct a valid spendable [`Cat`] for a hinted coin.
382    /// You simply need to look up the parent coin's spend, parse the children, and
383    /// find the one that matches the hinted coin.
384    ///
385    /// There is special handling for the revocation layer.
386    /// See [`Cat::child_from_p2_create_coin`] for more details.
387    pub fn parse_children(
388        allocator: &mut Allocator,
389        parent_coin: Coin,
390        parent_puzzle: Puzzle,
391        parent_solution: NodePtr,
392    ) -> Result<Option<Vec<Self>>, DriverError> {
393        let Some(parent_layer) = CatLayer::<Puzzle>::parse_puzzle(allocator, parent_puzzle)? else {
394            return Ok(None);
395        };
396        let parent_solution = CatLayer::<Puzzle>::parse_solution(allocator, parent_solution)?;
397
398        let mut hidden_puzzle_hash = None;
399        let mut p2_puzzle_hash = parent_layer.inner_puzzle.curried_puzzle_hash().into();
400        let mut inner_spend = Spend::new(
401            parent_layer.inner_puzzle.ptr(),
402            parent_solution.inner_puzzle_solution,
403        );
404        let mut revoke = false;
405
406        if let Some(revocation_layer) =
407            RevocationLayer::parse_puzzle(allocator, parent_layer.inner_puzzle)?
408        {
409            hidden_puzzle_hash = Some(revocation_layer.hidden_puzzle_hash);
410            p2_puzzle_hash = revocation_layer.inner_puzzle_hash;
411
412            let revocation_solution =
413                RevocationLayer::parse_solution(allocator, parent_solution.inner_puzzle_solution)?;
414
415            inner_spend = Spend::new(revocation_solution.puzzle, revocation_solution.solution);
416            revoke = revocation_solution.hidden;
417        }
418
419        let cat = Cat::new(
420            parent_coin,
421            parent_solution.lineage_proof,
422            CatInfo::new(parent_layer.asset_id, hidden_puzzle_hash, p2_puzzle_hash),
423        );
424
425        let output = run_puzzle(allocator, inner_spend.puzzle, inner_spend.solution)?;
426        let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
427
428        let outputs = conditions
429            .into_iter()
430            .filter_map(Condition::into_create_coin)
431            .map(|create_coin| cat.child_from_p2_create_coin(allocator, create_coin, revoke))
432            .collect();
433
434        Ok(Some(outputs))
435    }
436
437    /// Creates a new [`Cat`] that reflects the create coin condition in the p2 spend's conditions.
438    ///
439    /// There is special handling for the revocation layer:
440    /// 1. If there is no revocation layer for the parent, the child will not have one either.
441    /// 2. If the parent was not revoked, the child will have the same revocation layer.
442    /// 3. If the parent was revoked, the child will not have a revocation layer.
443    /// 4. If the parent was revoked, and the child was hinted (and wrapped with the revocation layer), it will detect it.
444    pub fn child_from_p2_create_coin(
445        &self,
446        allocator: &Allocator,
447        create_coin: CreateCoin<NodePtr>,
448        revoke: bool,
449    ) -> Self {
450        // Child with the same hidden puzzle hash as the parent
451        let child = self.child(create_coin.puzzle_hash, create_coin.amount);
452
453        // If the parent is not revocable, we don't need to add a revocation layer
454        let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash else {
455            return child;
456        };
457
458        // If we're not doing a revocation spend, we know it's wrapped in the same revocation layer
459        if !revoke {
460            return child;
461        }
462
463        // Child without a hidden puzzle hash but with the create coin puzzle hash as the p2 puzzle hash
464        let unrevocable_child = self.unrevocable_child(create_coin.puzzle_hash, create_coin.amount);
465
466        // If the hint is missing, just assume the child doesn't have a hidden puzzle hash
467        let Memos::Some(memos) = create_coin.memos else {
468            return unrevocable_child;
469        };
470
471        let Some((hint, _)) = <(Bytes32, NodePtr)>::from_clvm(allocator, memos).ok() else {
472            return unrevocable_child;
473        };
474
475        // If the hint wrapped in the revocation layer of the parent matches the create coin's puzzle hash,
476        // then we know that the hint is the p2 puzzle hash and the child has the same revocation layer as the parent
477        if create_coin.puzzle_hash
478            == RevocationLayer::new(hidden_puzzle_hash, hint)
479                .tree_hash()
480                .into()
481        {
482            return self.child(hint, create_coin.amount);
483        }
484
485        // Otherwise, we can't determine whether there is a revocation layer or not, so we will just assume it's unrevocable
486        // In practice, this should never happen while parsing a coin which is still spendable (not an ephemeral spend)
487        // If it does, a new hinting mechanism should be introduced in the future to accommodate this, but for now this is the best we can do
488        unrevocable_child
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use std::slice;
495
496    use chia_puzzle_types::cat::EverythingWithSignatureTailArgs;
497    use chia_sdk_test::Simulator;
498    use chia_sdk_types::{Mod, puzzles::RevocationArgs};
499    use rstest::rstest;
500
501    use crate::{SpendWithConditions, StandardLayer};
502
503    use super::*;
504
505    #[test]
506    fn test_single_issuance_cat() -> anyhow::Result<()> {
507        let mut sim = Simulator::new();
508        let ctx = &mut SpendContext::new();
509
510        let alice = sim.bls(1);
511        let alice_p2 = StandardLayer::new(alice.pk);
512
513        let memos = ctx.hint(alice.puzzle_hash)?;
514        let (issue_cat, cats) = Cat::single_issuance(
515            ctx,
516            alice.coin.coin_id(),
517            None,
518            1,
519            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
520        )?;
521        alice_p2.spend(ctx, alice.coin, issue_cat)?;
522
523        sim.spend_coins(ctx.take(), &[alice.sk])?;
524
525        let cat = cats[0];
526        assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
527        assert_eq!(
528            cat.info.asset_id,
529            GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
530        );
531        assert!(sim.coin_state(cat.coin.coin_id()).is_some());
532
533        Ok(())
534    }
535
536    #[test]
537    fn test_multi_issuance_cat() -> anyhow::Result<()> {
538        let mut sim = Simulator::new();
539        let ctx = &mut SpendContext::new();
540
541        let alice = sim.bls(1);
542        let alice_p2 = StandardLayer::new(alice.pk);
543
544        let memos = ctx.hint(alice.puzzle_hash)?;
545        let (issue_cat, cats) = Cat::multi_issuance(
546            ctx,
547            alice.coin.coin_id(),
548            alice.pk,
549            None,
550            1,
551            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
552        )?;
553        alice_p2.spend(ctx, alice.coin, issue_cat)?;
554        sim.spend_coins(ctx.take(), &[alice.sk])?;
555
556        let cat = cats[0];
557        assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
558        assert_eq!(
559            cat.info.asset_id,
560            EverythingWithSignatureTailArgs::curry_tree_hash(alice.pk).into()
561        );
562        assert!(sim.coin_state(cat.coin.coin_id()).is_some());
563
564        Ok(())
565    }
566
567    #[test]
568    fn test_zero_cat_issuance() -> anyhow::Result<()> {
569        let mut sim = Simulator::new();
570        let ctx = &mut SpendContext::new();
571
572        let alice = sim.bls(0);
573        let alice_p2 = StandardLayer::new(alice.pk);
574
575        let memos = ctx.hint(alice.puzzle_hash)?;
576        let (issue_cat, cats) = Cat::single_issuance(
577            ctx,
578            alice.coin.coin_id(),
579            None,
580            0,
581            Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
582        )?;
583        alice_p2.spend(ctx, alice.coin, issue_cat)?;
584
585        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
586
587        let cat = cats[0];
588        assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
589        assert_eq!(
590            cat.info.asset_id,
591            GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
592        );
593        assert!(sim.coin_state(cat.coin.coin_id()).is_some());
594
595        let cat_spend = CatSpend::new(
596            cat,
597            alice_p2.spend_with_conditions(
598                ctx,
599                Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
600            )?,
601        );
602        Cat::spend_all(ctx, &[cat_spend])?;
603        sim.spend_coins(ctx.take(), &[alice.sk])?;
604
605        Ok(())
606    }
607
608    #[test]
609    fn test_missing_cat_issuance_output() -> anyhow::Result<()> {
610        let mut sim = Simulator::new();
611        let ctx = &mut SpendContext::new();
612
613        let alice = sim.bls(1);
614        let alice_p2 = StandardLayer::new(alice.pk);
615
616        let (issue_cat, _cats) =
617            Cat::single_issuance(ctx, alice.coin.coin_id(), None, 1, Conditions::new())?;
618        alice_p2.spend(ctx, alice.coin, issue_cat)?;
619
620        assert_eq!(
621            sim.spend_coins(ctx.take(), &[alice.sk])
622                .unwrap_err()
623                .to_string(),
624            "Signer error: Eval error: clvm raise"
625        );
626
627        Ok(())
628    }
629
630    #[test]
631    fn test_exceeded_cat_issuance_output() -> anyhow::Result<()> {
632        let mut sim = Simulator::new();
633        let ctx = &mut SpendContext::new();
634
635        let alice = sim.bls(2);
636        let alice_p2 = StandardLayer::new(alice.pk);
637
638        let memos = ctx.hint(alice.puzzle_hash)?;
639        let (issue_cat, _cats) = Cat::single_issuance(
640            ctx,
641            alice.coin.coin_id(),
642            None,
643            1,
644            Conditions::new().create_coin(alice.puzzle_hash, 2, memos),
645        )?;
646        alice_p2.spend(ctx, alice.coin, issue_cat)?;
647
648        assert_eq!(
649            sim.spend_coins(ctx.take(), &[alice.sk])
650                .unwrap_err()
651                .to_string(),
652            "Signer error: Eval error: clvm raise"
653        );
654
655        Ok(())
656    }
657
658    #[rstest]
659    #[case(1)]
660    #[case(2)]
661    #[case(3)]
662    #[case(10)]
663    fn test_cat_spends(#[case] coins: usize) -> anyhow::Result<()> {
664        let mut sim = Simulator::new();
665        let ctx = &mut SpendContext::new();
666
667        // All of the amounts are different to prevent coin id collisions.
668        let mut amounts = Vec::with_capacity(coins);
669
670        for amount in 0..coins {
671            amounts.push(amount as u64);
672        }
673
674        // Create the coin with the sum of all the amounts we need to issue.
675        let sum = amounts.iter().sum::<u64>();
676
677        let alice = sim.bls(sum);
678        let alice_p2 = StandardLayer::new(alice.pk);
679
680        // Issue the CAT coins with those amounts.
681        let mut conditions = Conditions::new();
682
683        let memos = ctx.hint(alice.puzzle_hash)?;
684        for &amount in &amounts {
685            conditions = conditions.create_coin(alice.puzzle_hash, amount, memos);
686        }
687
688        let (issue_cat, mut cats) =
689            Cat::single_issuance(ctx, alice.coin.coin_id(), None, sum, conditions)?;
690        alice_p2.spend(ctx, alice.coin, issue_cat)?;
691
692        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
693
694        // Spend the CAT coins a few times.
695        for _ in 0..3 {
696            let cat_spends: Vec<CatSpend> = cats
697                .iter()
698                .map(|cat| {
699                    Ok(CatSpend::new(
700                        *cat,
701                        alice_p2.spend_with_conditions(
702                            ctx,
703                            Conditions::new().create_coin(
704                                alice.puzzle_hash,
705                                cat.coin.amount,
706                                memos,
707                            ),
708                        )?,
709                    ))
710                })
711                .collect::<anyhow::Result<_>>()?;
712
713            cats = Cat::spend_all(ctx, &cat_spends)?;
714            sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
715        }
716
717        Ok(())
718    }
719
720    #[test]
721    fn test_different_cat_p2_puzzles() -> anyhow::Result<()> {
722        let mut sim = Simulator::new();
723        let ctx = &mut SpendContext::new();
724
725        let alice = sim.bls(2);
726        let alice_p2 = StandardLayer::new(alice.pk);
727
728        // This will just return the solution verbatim.
729        let custom_p2 = ctx.alloc(&1)?;
730        let custom_p2_puzzle_hash = ctx.tree_hash(custom_p2).into();
731
732        let memos = ctx.hint(alice.puzzle_hash)?;
733        let custom_memos = ctx.hint(custom_p2_puzzle_hash)?;
734        let (issue_cat, cats) = Cat::single_issuance(
735            ctx,
736            alice.coin.coin_id(),
737            None,
738            2,
739            Conditions::new()
740                .create_coin(alice.puzzle_hash, 1, memos)
741                .create_coin(custom_p2_puzzle_hash, 1, custom_memos),
742        )?;
743        alice_p2.spend(ctx, alice.coin, issue_cat)?;
744        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
745
746        let spends = [
747            CatSpend::new(
748                cats[0],
749                alice_p2.spend_with_conditions(
750                    ctx,
751                    Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
752                )?,
753            ),
754            CatSpend::new(
755                cats[1],
756                Spend::new(
757                    custom_p2,
758                    ctx.alloc(&[CreateCoin::new(custom_p2_puzzle_hash, 1, custom_memos)])?,
759                ),
760            ),
761        ];
762
763        Cat::spend_all(ctx, &spends)?;
764        sim.spend_coins(ctx.take(), &[alice.sk])?;
765
766        Ok(())
767    }
768
769    #[test]
770    fn test_cat_melt() -> anyhow::Result<()> {
771        let mut sim = Simulator::new();
772        let ctx = &mut SpendContext::new();
773
774        let alice = sim.bls(10000);
775        let alice_p2 = StandardLayer::new(alice.pk);
776        let hint = ctx.hint(alice.puzzle_hash)?;
777
778        let conditions = Conditions::new().create_coin(alice.puzzle_hash, 10000, hint);
779
780        let (issue_cat, cats) =
781            Cat::multi_issuance(ctx, alice.coin.coin_id(), alice.pk, None, 10000, conditions)?;
782
783        alice_p2.spend(ctx, alice.coin, issue_cat)?;
784
785        let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
786
787        let cat_spend = CatSpend::new(
788            cats[0],
789            alice_p2.spend_with_conditions(
790                ctx,
791                Conditions::new()
792                    .create_coin(alice.puzzle_hash, 7000, hint)
793                    .run_cat_tail(tail, NodePtr::NIL),
794            )?,
795        );
796
797        Cat::spend_all(ctx, &[cat_spend])?;
798
799        sim.spend_coins(ctx.take(), &[alice.sk])?;
800
801        Ok(())
802    }
803
804    #[rstest]
805    fn test_cat_tail_reveal(
806        #[values(0, 1, 2)] tail_index: usize,
807        #[values(true, false)] melt: bool,
808    ) -> anyhow::Result<()> {
809        let mut sim = Simulator::new();
810        let ctx = &mut SpendContext::new();
811
812        let alice = sim.bls(15000);
813        let alice_p2 = StandardLayer::new(alice.pk);
814        let hint = ctx.hint(alice.puzzle_hash)?;
815
816        let conditions = Conditions::new()
817            .create_coin(alice.puzzle_hash, 3000, hint)
818            .create_coin(alice.puzzle_hash, 6000, hint)
819            .create_coin(alice.puzzle_hash, 1000, hint);
820
821        let (issue_cat, cats) =
822            Cat::multi_issuance(ctx, alice.coin.coin_id(), alice.pk, None, 10000, conditions)?;
823
824        alice_p2.spend(ctx, alice.coin, issue_cat)?;
825
826        let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
827
828        let cat_spends = cats
829            .into_iter()
830            .enumerate()
831            .map(|(i, cat)| {
832                let mut conditions = Conditions::new();
833
834                // Add the TAIL reveal to the second spend, to ensure the order doesn't matter
835                if i == tail_index {
836                    conditions.push(RunCatTail::new(tail, NodePtr::NIL));
837
838                    if !melt {
839                        conditions.push(CreateCoin::new(alice.puzzle_hash, 15000, hint));
840                    }
841                }
842
843                Ok(CatSpend::new(
844                    cat,
845                    alice_p2.spend_with_conditions(ctx, conditions)?,
846                ))
847            })
848            .collect::<anyhow::Result<Vec<_>>>()?;
849
850        Cat::spend_all(ctx, &cat_spends)?;
851
852        sim.spend_coins(ctx.take(), &[alice.sk])?;
853
854        Ok(())
855    }
856
857    #[test]
858    fn test_revocable_cat() -> anyhow::Result<()> {
859        let mut sim = Simulator::new();
860        let mut ctx = SpendContext::new();
861
862        let alice = sim.bls(10);
863        let alice_p2 = StandardLayer::new(alice.pk);
864
865        let bob = sim.bls(0);
866        let bob_p2 = StandardLayer::new(bob.pk);
867
868        let asset_id = EverythingWithSignatureTailArgs::curry_tree_hash(alice.pk).into();
869        let hint = ctx.hint(bob.puzzle_hash)?;
870
871        let (issue_cat, cats) = Cat::multi_issuance(
872            &mut ctx,
873            alice.coin.coin_id(),
874            alice.pk,
875            Some(alice.puzzle_hash),
876            10,
877            Conditions::new().create_coin(bob.puzzle_hash, 10, hint),
878        )?;
879        alice_p2.spend(&mut ctx, alice.coin, issue_cat)?;
880
881        // Bob can spend the CAT because he owns it
882        let cat_spend = CatSpend::new(
883            cats[0],
884            bob_p2.spend_with_conditions(
885                &mut ctx,
886                Conditions::new().create_coin(bob.puzzle_hash, 10, hint),
887            )?,
888        );
889        let cats = Cat::spend_all(&mut ctx, &[cat_spend])?;
890
891        // But Alice can also spend (revoke) it because she owns the revocation key
892        let hint = ctx.hint(alice.puzzle_hash)?;
893
894        let revocable_puzzle_hash = RevocationArgs::new(alice.puzzle_hash, alice.puzzle_hash)
895            .curry_tree_hash()
896            .into();
897
898        let cat_spend = CatSpend::revoke(
899            cats[0],
900            alice_p2.spend_with_conditions(
901                &mut ctx,
902                Conditions::new()
903                    .create_coin(alice.puzzle_hash, 5, hint)
904                    .create_coin(revocable_puzzle_hash, 5, hint),
905            )?,
906        );
907
908        let cats = Cat::spend_all(&mut ctx, &[cat_spend])?;
909
910        // Validate the transaction
911        sim.spend_coins(ctx.take(), &[alice.sk.clone(), bob.sk.clone()])?;
912
913        // The first coin should exist and not be revocable
914        assert_ne!(sim.coin_state(cats[0].coin.coin_id()), None);
915        assert_eq!(cats[0].info.p2_puzzle_hash, alice.puzzle_hash);
916        assert_eq!(cats[0].info.asset_id, asset_id);
917        assert_eq!(cats[0].info.hidden_puzzle_hash, None);
918
919        // The second coin should exist and be revocable
920        assert_ne!(sim.coin_state(cats[1].coin.coin_id()), None);
921        assert_eq!(cats[1].info.p2_puzzle_hash, alice.puzzle_hash);
922        assert_eq!(cats[1].info.asset_id, asset_id);
923        assert_eq!(cats[1].info.hidden_puzzle_hash, Some(alice.puzzle_hash));
924
925        let lineage_proof = cats[0].lineage_proof;
926
927        let parent_spend = sim.coin_spend(cats[0].coin.parent_coin_info).unwrap();
928        let parent_puzzle = ctx.alloc(&parent_spend.puzzle_reveal)?;
929        let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle);
930        let parent_solution = ctx.alloc(&parent_spend.solution)?;
931
932        let cats =
933            Cat::parse_children(&mut ctx, parent_spend.coin, parent_puzzle, parent_solution)?
934                .unwrap();
935
936        // The first coin should exist and not be revocable
937        assert_ne!(sim.coin_state(cats[0].coin.coin_id()), None);
938        assert_eq!(cats[0].info.p2_puzzle_hash, alice.puzzle_hash);
939        assert_eq!(cats[0].info.asset_id, asset_id);
940        assert_eq!(cats[0].info.hidden_puzzle_hash, None);
941
942        // The second coin should exist and be revocable
943        assert_ne!(sim.coin_state(cats[1].coin.coin_id()), None);
944        assert_eq!(cats[1].info.p2_puzzle_hash, alice.puzzle_hash);
945        assert_eq!(cats[1].info.asset_id, asset_id);
946        assert_eq!(cats[1].info.hidden_puzzle_hash, Some(alice.puzzle_hash));
947
948        assert_eq!(cats[0].lineage_proof, lineage_proof);
949
950        let cat_spends = cats
951            .into_iter()
952            .map(|cat| {
953                Ok(CatSpend::revoke(
954                    cat,
955                    alice_p2.spend_with_conditions(
956                        &mut ctx,
957                        Conditions::new().create_coin(alice.puzzle_hash, 5, hint),
958                    )?,
959                ))
960            })
961            .collect::<anyhow::Result<Vec<_>>>()?;
962
963        _ = Cat::spend_all(&mut ctx, &cat_spends)?;
964
965        // Validate the transaction
966        sim.spend_coins(ctx.take(), &[alice.sk, bob.sk])?;
967
968        Ok(())
969    }
970}