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