chik_sdk_driver/primitives/
cat.rs

1use chik_bls::PublicKey;
2use chik_protocol::{Bytes32, Coin};
3use chik_puzzle_types::{
4    cat::{CatSolution, EverythingWithSignatureTailArgs, GenesisByCoinIdTailArgs},
5    CoinProof, LineageProof, Memos,
6};
7use chik_sdk_types::{
8    conditions::{CreateCoin, RunCatTail},
9    puzzles::RevocationSolution,
10    run_puzzle, Condition, Conditions,
11};
12use klvm_traits::{klvm_quote, FromKlvm};
13use klvm_utils::{tree_hash, ToTreeHash};
14use klvmr::{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            ctx.tree_hash(tail).into(),
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            ctx.tree_hash(tail).into(),
95            amount,
96            RunCatTail::new(tail, NodePtr::NIL),
97            extra_conditions,
98        )
99    }
100
101    pub fn issue(
102        ctx: &mut SpendContext,
103        parent_coin_id: Bytes32,
104        asset_id: Bytes32,
105        amount: u64,
106        run_tail: RunCatTail<NodePtr, NodePtr>,
107        conditions: Conditions,
108    ) -> Result<(Conditions, Vec<Cat>), DriverError> {
109        let p2_puzzle = ctx.alloc_hashed(&klvm_quote!(conditions.with(run_tail)))?;
110        let puzzle_hash = CatLayer::new(asset_id, p2_puzzle).tree_hash().into();
111
112        let eve = Cat::new(
113            Coin::new(parent_coin_id, puzzle_hash, amount),
114            None,
115            CatInfo::new(asset_id, None, p2_puzzle.tree_hash().into()),
116        );
117
118        let children = Cat::spend_all(
119            ctx,
120            &[CatSpend::new(
121                eve,
122                Spend::new(p2_puzzle.ptr(), NodePtr::NIL),
123            )],
124        )?;
125
126        Ok((
127            Conditions::new().create_coin(puzzle_hash, amount, Memos::None),
128            children,
129        ))
130    }
131
132    /// Constructs a [`CoinSpend`](chik_protocol::CoinSpend) for each [`CatSpend`] in the list.
133    /// The spends are added to the [`SpendContext`] (in order) for convenience.
134    ///
135    /// All of the ring announcements and proofs required by the CAT puzzle are calculated automatically.
136    /// This requires running the inner spends to get the conditions, so any errors will be propagated.
137    ///
138    /// It's important not to spend CATs with different asset IDs at the same time, since they are not
139    /// compatible.
140    ///
141    /// Additionally, you should group all CAT spends done in the same transaction together
142    /// so that the value of one coin can be freely used in the output of another. If you spend them
143    /// separately, there will be multiple announcement rings and a non-zero delta will be calculated.
144    pub fn spend_all(
145        ctx: &mut SpendContext,
146        cat_spends: &[CatSpend],
147    ) -> Result<Vec<Cat>, DriverError> {
148        let len = cat_spends.len();
149
150        let mut total_delta = 0;
151        let mut children = Vec::new();
152
153        for (index, cat_spend) in cat_spends.iter().enumerate() {
154            let CatSpend {
155                cat,
156                inner_spend,
157                extra_delta,
158                revoke,
159            } = cat_spend;
160
161            // Calculate the delta and add it to the subtotal.
162            let output = ctx.run(inner_spend.puzzle, inner_spend.solution)?;
163            let conditions: Vec<NodePtr> = ctx.extract(output)?;
164
165            let create_coins: Vec<CreateCoin<NodePtr>> = conditions
166                .into_iter()
167                .filter_map(|ptr| ctx.extract::<CreateCoin<NodePtr>>(ptr).ok())
168                .collect();
169
170            let delta = create_coins.iter().fold(
171                i128::from(cat.coin.amount) - i128::from(*extra_delta),
172                |delta, create_coin| delta - i128::from(create_coin.amount),
173            );
174
175            let prev_subtotal = total_delta;
176            total_delta += delta;
177
178            // Find information of neighboring coins on the ring.
179            let prev = &cat_spends[if index == 0 { len - 1 } else { index - 1 }];
180            let next = &cat_spends[if index == len - 1 { 0 } else { index + 1 }];
181
182            cat.spend(
183                ctx,
184                SingleCatSpend {
185                    inner_spend: *inner_spend,
186                    prev_coin_id: prev.cat.coin.coin_id(),
187                    next_coin_proof: CoinProof {
188                        parent_coin_info: next.cat.coin.parent_coin_info,
189                        inner_puzzle_hash: ctx.tree_hash(next.inner_spend.puzzle).into(),
190                        amount: next.cat.coin.amount,
191                    },
192                    prev_subtotal: prev_subtotal.try_into()?,
193                    extra_delta: *extra_delta,
194                    revoke: *revoke,
195                },
196            )?;
197
198            for create_coin in create_coins {
199                children.push(cat.child_from_p2_create_coin(ctx, create_coin, *revoke));
200            }
201        }
202
203        Ok(children)
204    }
205
206    /// Spends this CAT coin with the provided solution parameters. Other parameters are inferred from
207    /// the [`Cat`] instance.
208    ///
209    /// This is useful if you have already calculated the conditions and want to spend the coin directly.
210    /// However, it's more common to use [`Cat::spend_all`] which handles the details of calculating the
211    /// solution (including ring announcements) for multiple CATs and spending them all at once.
212    pub fn spend(&self, ctx: &mut SpendContext, info: SingleCatSpend) -> Result<(), DriverError> {
213        let mut spend = info.inner_spend;
214
215        if let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash {
216            spend = RevocationLayer::new(hidden_puzzle_hash, self.info.p2_puzzle_hash)
217                .construct_spend(
218                    ctx,
219                    RevocationSolution::new(info.revoke, spend.puzzle, spend.solution),
220                )?;
221        }
222
223        spend = CatLayer::new(self.info.asset_id, spend.puzzle).construct_spend(
224            ctx,
225            CatSolution {
226                lineage_proof: self.lineage_proof,
227                inner_puzzle_solution: spend.solution,
228                prev_coin_id: info.prev_coin_id,
229                this_coin_info: self.coin,
230                next_coin_proof: info.next_coin_proof,
231                extra_delta: info.extra_delta,
232                prev_subtotal: info.prev_subtotal,
233            },
234        )?;
235
236        ctx.spend(self.coin, spend)?;
237
238        Ok(())
239    }
240
241    /// Creates a [`LineageProof`] for which would be valid for any children created by this [`Cat`].
242    pub fn child_lineage_proof(&self) -> LineageProof {
243        LineageProof {
244            parent_parent_coin_info: self.coin.parent_coin_info,
245            parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
246            parent_amount: self.coin.amount,
247        }
248    }
249
250    /// Creates a new [`Cat`] that represents a child of this one.
251    /// The child will have the same revocation layer (or lack thereof) as the current [`Cat`].
252    ///
253    /// If you need to construct a child without the revocation layer, use [`Cat::unrevocable_child`].
254    pub fn child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
255        self.child_with(
256            CatInfo {
257                p2_puzzle_hash,
258                ..self.info
259            },
260            amount,
261        )
262    }
263
264    /// Creates a new [`Cat`] that represents a child of this one.
265    /// The child will not have a revocation layer.
266    ///
267    /// If you need to construct a child with the same revocation layer, use [`Cat::child`].
268    pub fn unrevocable_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
269        self.child_with(
270            CatInfo {
271                p2_puzzle_hash,
272                hidden_puzzle_hash: None,
273                ..self.info
274            },
275            amount,
276        )
277    }
278
279    /// Creates a new [`Cat`] that represents a child of this one.
280    ///
281    /// You can specify the [`CatInfo`] to use for the child manually.
282    /// In most cases, you will want to use [`Cat::child`] or [`Cat::unrevocable_child`] instead.
283    pub fn child_with(&self, info: CatInfo, amount: u64) -> Self {
284        Self {
285            coin: Coin::new(self.coin.coin_id(), info.puzzle_hash().into(), amount),
286            lineage_proof: Some(self.child_lineage_proof()),
287            info,
288        }
289    }
290}
291
292impl Cat {
293    /// Parses the children of a [`Cat`] from the parent coin spend.
294    ///
295    /// This can be used to construct a valid spendable [`Cat`] for a hinted coin.
296    /// You simply need to look up the parent coin's spend, parse the children, and
297    /// find the one that matches the hinted coin.
298    ///
299    /// There is special handling for the revocation layer.
300    /// See [`Cat::child_from_p2_create_coin`] for more details.
301    pub fn parse_children(
302        allocator: &mut Allocator,
303        parent_coin: Coin,
304        parent_puzzle: Puzzle,
305        parent_solution: NodePtr,
306    ) -> Result<Option<Vec<Self>>, DriverError>
307    where
308        Self: Sized,
309    {
310        let Some(parent_layer) = CatLayer::<Puzzle>::parse_puzzle(allocator, parent_puzzle)? else {
311            return Ok(None);
312        };
313        let parent_solution = CatLayer::<Puzzle>::parse_solution(allocator, parent_solution)?;
314
315        let mut hidden_puzzle_hash = None;
316        let mut inner_spend = Spend::new(
317            parent_layer.inner_puzzle.ptr(),
318            parent_solution.inner_puzzle_solution,
319        );
320        let mut revoke = false;
321
322        if let Some(revocation_layer) =
323            RevocationLayer::parse_puzzle(allocator, parent_layer.inner_puzzle)?
324        {
325            hidden_puzzle_hash = Some(revocation_layer.hidden_puzzle_hash);
326
327            let revocation_solution =
328                RevocationLayer::parse_solution(allocator, parent_solution.inner_puzzle_solution)?;
329
330            inner_spend = Spend::new(revocation_solution.puzzle, revocation_solution.solution);
331            revoke = revocation_solution.hidden;
332        }
333
334        let cat = Cat::new(
335            parent_coin,
336            parent_solution.lineage_proof,
337            CatInfo::new(
338                parent_layer.asset_id,
339                hidden_puzzle_hash,
340                tree_hash(allocator, inner_spend.puzzle).into(),
341            ),
342        );
343
344        let output = run_puzzle(allocator, inner_spend.puzzle, inner_spend.solution)?;
345        let conditions = Vec::<Condition>::from_klvm(allocator, output)?;
346
347        let outputs = conditions
348            .into_iter()
349            .filter_map(Condition::into_create_coin)
350            .map(|create_coin| cat.child_from_p2_create_coin(allocator, create_coin, revoke))
351            .collect();
352
353        Ok(Some(outputs))
354    }
355
356    /// Creates a new [`Cat`] that reflects the create coin condition in the p2 spend's conditions.
357    ///
358    /// There is special handling for the revocation layer:
359    /// 1. If there is no revocation layer for the parent, the child will not have one either.
360    /// 2. If the parent was not revoked, the child will have the same revocation layer.
361    /// 3. If the parent was revoked, the child will not have a revocation layer.
362    /// 4. If the parent was revoked, and the child was hinted (and wrapped with the revocation layer), it will detect it.
363    pub fn child_from_p2_create_coin(
364        &self,
365        allocator: &Allocator,
366        create_coin: CreateCoin<NodePtr>,
367        revoke: bool,
368    ) -> Self {
369        // Child with the same hidden puzzle hash as the parent
370        let child = self.child(create_coin.puzzle_hash, create_coin.amount);
371
372        // If the parent is not revocable, we don't need to add a revocation layer
373        let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash else {
374            return child;
375        };
376
377        // If we're not doing a revocation spend, we know it's wrapped in the same revocation layer
378        if !revoke {
379            return child;
380        }
381
382        // Child without a hidden puzzle hash but with the create coin puzzle hash as the p2 puzzle hash
383        let unrevocable_child = self.unrevocable_child(create_coin.puzzle_hash, create_coin.amount);
384
385        // If the hint is missing, just assume the child doesn't have a hidden puzzle hash
386        let Memos::Some(memos) = create_coin.memos else {
387            return unrevocable_child;
388        };
389
390        let Some((hint, _)) = <(Bytes32, NodePtr)>::from_klvm(allocator, memos).ok() else {
391            return unrevocable_child;
392        };
393
394        // If the hint wrapped in the revocation layer of the parent matches the create coin's puzzle hash,
395        // then we know that the hint is the p2 puzzle hash and the child has the same revocation layer as the parent
396        if hint
397            == RevocationLayer::new(hidden_puzzle_hash, hint)
398                .tree_hash()
399                .into()
400        {
401            return self.child(hint, create_coin.amount);
402        }
403
404        // Otherwise, we can't determine whether there is a revocation layer or not, so we will just assume it's unrevocable
405        // In practice, this should never happen while parsing a coin which is still spendable (not an ephemeral spend)
406        // 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
407        unrevocable_child
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use chik_consensus::validation_error::ErrorCode;
414    use chik_puzzle_types::cat::EverythingWithSignatureTailArgs;
415    use chik_sdk_test::{Simulator, SimulatorError};
416    use rstest::rstest;
417
418    use crate::{SpendWithConditions, StandardLayer};
419
420    use super::*;
421
422    #[test]
423    fn test_single_issuance_cat() -> anyhow::Result<()> {
424        let mut sim = Simulator::new();
425        let ctx = &mut SpendContext::new();
426
427        let alice = sim.bls(1);
428        let alice_p2 = StandardLayer::new(alice.pk);
429
430        let memos = ctx.hint(alice.puzzle_hash)?;
431        let (issue_cat, cats) = Cat::issue_with_coin(
432            ctx,
433            alice.coin.coin_id(),
434            1,
435            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
436        )?;
437        alice_p2.spend(ctx, alice.coin, issue_cat)?;
438
439        sim.spend_coins(ctx.take(), &[alice.sk])?;
440
441        let cat = cats[0];
442        assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
443        assert_eq!(
444            cat.info.asset_id,
445            GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
446        );
447        assert!(sim.coin_state(cat.coin.coin_id()).is_some());
448
449        Ok(())
450    }
451
452    #[test]
453    fn test_multi_issuance_cat() -> anyhow::Result<()> {
454        let mut sim = Simulator::new();
455        let ctx = &mut SpendContext::new();
456
457        let alice = sim.bls(1);
458        let alice_p2 = StandardLayer::new(alice.pk);
459
460        let memos = ctx.hint(alice.puzzle_hash)?;
461        let (issue_cat, cats) = Cat::issue_with_key(
462            ctx,
463            alice.coin.coin_id(),
464            alice.pk,
465            1,
466            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
467        )?;
468        alice_p2.spend(ctx, alice.coin, issue_cat)?;
469        sim.spend_coins(ctx.take(), &[alice.sk])?;
470
471        let cat = cats[0];
472        assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
473        assert_eq!(
474            cat.info.asset_id,
475            EverythingWithSignatureTailArgs::curry_tree_hash(alice.pk).into()
476        );
477        assert!(sim.coin_state(cat.coin.coin_id()).is_some());
478
479        Ok(())
480    }
481
482    #[test]
483    fn test_zero_cat_issuance() -> anyhow::Result<()> {
484        let mut sim = Simulator::new();
485        let ctx = &mut SpendContext::new();
486
487        let alice = sim.bls(0);
488        let alice_p2 = StandardLayer::new(alice.pk);
489
490        let memos = ctx.hint(alice.puzzle_hash)?;
491        let (issue_cat, cats) = Cat::issue_with_coin(
492            ctx,
493            alice.coin.coin_id(),
494            0,
495            Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
496        )?;
497        alice_p2.spend(ctx, alice.coin, issue_cat)?;
498
499        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
500
501        let cat = cats[0];
502        assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
503        assert_eq!(
504            cat.info.asset_id,
505            GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
506        );
507        assert!(sim.coin_state(cat.coin.coin_id()).is_some());
508
509        let cat_spend = CatSpend::new(
510            cat,
511            alice_p2.spend_with_conditions(
512                ctx,
513                Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
514            )?,
515        );
516        Cat::spend_all(ctx, &[cat_spend])?;
517        sim.spend_coins(ctx.take(), &[alice.sk])?;
518
519        Ok(())
520    }
521
522    #[test]
523    fn test_missing_cat_issuance_output() -> anyhow::Result<()> {
524        let mut sim = Simulator::new();
525        let ctx = &mut SpendContext::new();
526
527        let alice = sim.bls(1);
528        let alice_p2 = StandardLayer::new(alice.pk);
529
530        let (issue_cat, _cats) =
531            Cat::issue_with_coin(ctx, alice.coin.coin_id(), 1, Conditions::new())?;
532        alice_p2.spend(ctx, alice.coin, issue_cat)?;
533
534        assert!(matches!(
535            sim.spend_coins(ctx.take(), &[alice.sk]).unwrap_err(),
536            SimulatorError::Validation(ErrorCode::AssertCoinAnnouncementFailed)
537        ));
538
539        Ok(())
540    }
541
542    #[test]
543    fn test_exceeded_cat_issuance_output() -> anyhow::Result<()> {
544        let mut sim = Simulator::new();
545        let ctx = &mut SpendContext::new();
546
547        let alice = sim.bls(2);
548        let alice_p2 = StandardLayer::new(alice.pk);
549
550        let memos = ctx.hint(alice.puzzle_hash)?;
551        let (issue_cat, _cats) = Cat::issue_with_coin(
552            ctx,
553            alice.coin.coin_id(),
554            1,
555            Conditions::new().create_coin(alice.puzzle_hash, 2, memos),
556        )?;
557        alice_p2.spend(ctx, alice.coin, issue_cat)?;
558
559        assert!(matches!(
560            sim.spend_coins(ctx.take(), &[alice.sk]).unwrap_err(),
561            SimulatorError::Validation(ErrorCode::AssertCoinAnnouncementFailed)
562        ));
563
564        Ok(())
565    }
566
567    #[rstest]
568    #[case(1)]
569    #[case(2)]
570    #[case(3)]
571    #[case(10)]
572    fn test_cat_spends(#[case] coins: usize) -> anyhow::Result<()> {
573        let mut sim = Simulator::new();
574        let ctx = &mut SpendContext::new();
575
576        // All of the amounts are different to prevent coin id collisions.
577        let mut amounts = Vec::with_capacity(coins);
578
579        for amount in 0..coins {
580            amounts.push(amount as u64);
581        }
582
583        // Create the coin with the sum of all the amounts we need to issue.
584        let sum = amounts.iter().sum::<u64>();
585
586        let alice = sim.bls(sum);
587        let alice_p2 = StandardLayer::new(alice.pk);
588
589        // Issue the CAT coins with those amounts.
590        let mut conditions = Conditions::new();
591
592        let memos = ctx.hint(alice.puzzle_hash)?;
593        for &amount in &amounts {
594            conditions = conditions.create_coin(alice.puzzle_hash, amount, memos);
595        }
596
597        let (issue_cat, mut cats) =
598            Cat::issue_with_coin(ctx, alice.coin.coin_id(), sum, conditions)?;
599        alice_p2.spend(ctx, alice.coin, issue_cat)?;
600
601        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
602
603        // Spend the CAT coins a few times.
604        for _ in 0..3 {
605            let cat_spends: Vec<CatSpend> = cats
606                .iter()
607                .map(|cat| {
608                    Ok(CatSpend::new(
609                        *cat,
610                        alice_p2.spend_with_conditions(
611                            ctx,
612                            Conditions::new().create_coin(
613                                alice.puzzle_hash,
614                                cat.coin.amount,
615                                memos,
616                            ),
617                        )?,
618                    ))
619                })
620                .collect::<anyhow::Result<_>>()?;
621
622            cats = Cat::spend_all(ctx, &cat_spends)?;
623            sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
624        }
625
626        Ok(())
627    }
628
629    #[test]
630    fn test_different_cat_p2_puzzles() -> anyhow::Result<()> {
631        let mut sim = Simulator::new();
632        let ctx = &mut SpendContext::new();
633
634        let alice = sim.bls(2);
635        let alice_p2 = StandardLayer::new(alice.pk);
636
637        // This will just return the solution verbatim.
638        let custom_p2 = ctx.alloc(&1)?;
639        let custom_p2_puzzle_hash = ctx.tree_hash(custom_p2).into();
640
641        let memos = ctx.hint(alice.puzzle_hash)?;
642        let custom_memos = ctx.hint(custom_p2_puzzle_hash)?;
643        let (issue_cat, cats) = Cat::issue_with_coin(
644            ctx,
645            alice.coin.coin_id(),
646            2,
647            Conditions::new()
648                .create_coin(alice.puzzle_hash, 1, memos)
649                .create_coin(custom_p2_puzzle_hash, 1, custom_memos),
650        )?;
651        alice_p2.spend(ctx, alice.coin, issue_cat)?;
652        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
653
654        let spends = [
655            CatSpend::new(
656                cats[0],
657                alice_p2.spend_with_conditions(
658                    ctx,
659                    Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
660                )?,
661            ),
662            CatSpend::new(
663                cats[1],
664                Spend::new(
665                    custom_p2,
666                    ctx.alloc(&[CreateCoin::new(custom_p2_puzzle_hash, 1, custom_memos)])?,
667                ),
668            ),
669        ];
670
671        Cat::spend_all(ctx, &spends)?;
672        sim.spend_coins(ctx.take(), &[alice.sk])?;
673
674        Ok(())
675    }
676
677    #[test]
678    fn test_cat_melt() -> anyhow::Result<()> {
679        let mut sim = Simulator::new();
680        let ctx = &mut SpendContext::new();
681
682        let alice = sim.bls(10000);
683        let alice_p2 = StandardLayer::new(alice.pk);
684
685        let memos = ctx.hint(alice.puzzle_hash)?;
686        let conditions = Conditions::new().create_coin(alice.puzzle_hash, 10000, memos);
687        let (issue_cat, cats) =
688            Cat::issue_with_key(ctx, alice.coin.coin_id(), alice.pk, 10000, conditions)?;
689        alice_p2.spend(ctx, alice.coin, issue_cat)?;
690
691        let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
692
693        let cat_spend = CatSpend::with_extra_delta(
694            cats[0],
695            alice_p2.spend_with_conditions(
696                ctx,
697                Conditions::new()
698                    .create_coin(alice.puzzle_hash, 7000, memos)
699                    .run_cat_tail(tail, NodePtr::NIL),
700            )?,
701            -3000,
702        );
703
704        Cat::spend_all(ctx, &[cat_spend])?;
705
706        sim.spend_coins(ctx.take(), &[alice.sk])?;
707
708        Ok(())
709    }
710}