chia_sdk_driver/primitives/
cat.rs

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