1use chia_bls::PublicKey;
2use chia_protocol::{Bytes32, Coin};
3use chia_puzzle_types::{
4 CoinProof, LineageProof, Memos,
5 cat::{CatSolution, EverythingWithSignatureTailArgs, GenesisByCoinIdTailArgs},
6};
7use chia_sdk_types::{
8 Condition, Conditions,
9 conditions::{CreateCoin, RunCatTail},
10 puzzles::RevocationSolution,
11 run_puzzle,
12};
13use clvm_traits::FromClvm;
14use clvm_utils::ToTreeHash;
15use clvmr::{Allocator, NodePtr};
16
17use crate::{CatLayer, DriverError, Layer, Puzzle, RevocationLayer, Spend, SpendContext};
18
19mod cat_info;
20mod cat_spend;
21mod parsed_cat;
22mod single_cat_spend;
23
24pub use cat_info::*;
25pub use cat_spend::*;
26pub use parsed_cat::*;
27pub use single_cat_spend::*;
28
29#[must_use]
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Cat {
41 pub coin: Coin,
43
44 pub lineage_proof: Option<LineageProof>,
53
54 pub info: CatInfo,
56}
57
58impl Cat {
59 pub fn new(coin: Coin, lineage_proof: Option<LineageProof>, info: CatInfo) -> Self {
60 Self {
61 coin,
62 lineage_proof,
63 info,
64 }
65 }
66
67 pub fn single_issuance(
68 ctx: &mut SpendContext,
69 parent_coin_id: Bytes32,
70 hidden_puzzle_hash: Option<Bytes32>,
71 amount: u64,
72 extra_conditions: Conditions,
73 ) -> Result<(Conditions, Vec<Cat>), DriverError> {
74 let tail = ctx.curry(GenesisByCoinIdTailArgs::new(parent_coin_id))?;
75
76 Self::issue(
77 ctx,
78 parent_coin_id,
79 hidden_puzzle_hash,
80 amount,
81 RunCatTail::new(tail, NodePtr::NIL),
82 extra_conditions,
83 )
84 }
85
86 pub fn multi_issuance(
87 ctx: &mut SpendContext,
88 parent_coin_id: Bytes32,
89 public_key: PublicKey,
90 hidden_puzzle_hash: Option<Bytes32>,
91 amount: u64,
92 extra_conditions: Conditions,
93 ) -> Result<(Conditions, Vec<Cat>), DriverError> {
94 let tail = ctx.curry(EverythingWithSignatureTailArgs::new(public_key))?;
95
96 Self::issue(
97 ctx,
98 parent_coin_id,
99 hidden_puzzle_hash,
100 amount,
101 RunCatTail::new(tail, NodePtr::NIL),
102 extra_conditions,
103 )
104 }
105
106 pub fn issue(
107 ctx: &mut SpendContext,
108 parent_coin_id: Bytes32,
109 hidden_puzzle_hash: Option<Bytes32>,
110 amount: u64,
111 run_tail: RunCatTail<NodePtr, NodePtr>,
112 conditions: Conditions,
113 ) -> Result<(Conditions, Vec<Cat>), DriverError> {
114 let delegated_spend = ctx.delegated_spend(conditions.with(run_tail))?;
115 let eve_info = CatInfo::new(
116 ctx.tree_hash(run_tail.program).into(),
117 hidden_puzzle_hash,
118 ctx.tree_hash(delegated_spend.puzzle).into(),
119 );
120
121 let eve = Cat::new(
122 Coin::new(parent_coin_id, eve_info.puzzle_hash().into(), amount),
123 None,
124 eve_info,
125 );
126
127 let children = Cat::spend_all(ctx, &[CatSpend::new(eve, delegated_spend)])?;
128
129 Ok((
130 Conditions::new().create_coin(eve.coin.puzzle_hash, eve.coin.amount, Memos::None),
131 children,
132 ))
133 }
134
135 pub fn spend_all(
148 ctx: &mut SpendContext,
149 cat_spends: &[CatSpend],
150 ) -> Result<Vec<Cat>, DriverError> {
151 let len = cat_spends.len();
152
153 let mut total_delta = 0;
154 let mut prev_subtotals = Vec::new();
155 let mut run_tail_index = None;
156 let mut children = Vec::new();
157
158 for (index, &item) in cat_spends.iter().enumerate() {
159 let output = ctx.run(item.spend.puzzle, item.spend.solution)?;
160 let conditions: Vec<Condition> = ctx.extract(output)?;
161
162 if run_tail_index.is_none() && conditions.iter().any(Condition::is_run_cat_tail) {
164 run_tail_index = Some(index);
165 }
166
167 let create_coins: Vec<CreateCoin<NodePtr>> = conditions
168 .into_iter()
169 .filter_map(Condition::into_create_coin)
170 .collect();
171
172 let delta = create_coins
174 .iter()
175 .fold(i128::from(item.cat.coin.amount), |delta, create_coin| {
176 delta - i128::from(create_coin.amount)
177 });
178
179 prev_subtotals.push(total_delta);
181
182 total_delta += delta;
184
185 for create_coin in create_coins {
186 children.push(
187 item.cat
188 .child_from_p2_create_coin(ctx, create_coin, item.hidden),
189 );
190 }
191 }
192
193 if let Some(tail_index) = run_tail_index {
195 let tail_adjustment = -total_delta;
196
197 prev_subtotals
198 .iter_mut()
199 .skip(tail_index + 1)
200 .for_each(|subtotal| {
201 *subtotal += tail_adjustment;
202 });
203 }
204
205 for (index, item) in cat_spends.iter().enumerate() {
206 let prev = &cat_spends[if index == 0 { len - 1 } else { index - 1 }];
208 let next = &cat_spends[if index == len - 1 { 0 } else { index + 1 }];
209
210 let next_inner_puzzle_hash = next.cat.info.inner_puzzle_hash();
211
212 item.cat.spend(
213 ctx,
214 SingleCatSpend {
215 p2_spend: item.spend,
216 prev_coin_id: prev.cat.coin.coin_id(),
217 next_coin_proof: CoinProof {
218 parent_coin_info: next.cat.coin.parent_coin_info,
219 inner_puzzle_hash: next_inner_puzzle_hash.into(),
220 amount: next.cat.coin.amount,
221 },
222 prev_subtotal: prev_subtotals[index].try_into()?,
223 extra_delta: if run_tail_index.is_some_and(|i| i == index) {
225 -total_delta.try_into()?
226 } else {
227 0
228 },
229 revoke: item.hidden,
230 },
231 )?;
232 }
233
234 Ok(children)
235 }
236
237 pub fn spend(&self, ctx: &mut SpendContext, info: SingleCatSpend) -> Result<(), DriverError> {
244 let mut spend = info.p2_spend;
245
246 if let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash {
247 spend = RevocationLayer::new(hidden_puzzle_hash, self.info.p2_puzzle_hash)
248 .construct_spend(
249 ctx,
250 RevocationSolution::new(info.revoke, spend.puzzle, spend.solution),
251 )?;
252 }
253
254 spend = CatLayer::new(self.info.asset_id, spend.puzzle).construct_spend(
255 ctx,
256 CatSolution {
257 lineage_proof: self.lineage_proof,
258 inner_puzzle_solution: spend.solution,
259 prev_coin_id: info.prev_coin_id,
260 this_coin_info: self.coin,
261 next_coin_proof: info.next_coin_proof,
262 extra_delta: info.extra_delta,
263 prev_subtotal: info.prev_subtotal,
264 },
265 )?;
266
267 ctx.spend(self.coin, spend)?;
268
269 Ok(())
270 }
271
272 pub fn child_lineage_proof(&self) -> LineageProof {
274 LineageProof {
275 parent_parent_coin_info: self.coin.parent_coin_info,
276 parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
277 parent_amount: self.coin.amount,
278 }
279 }
280
281 pub fn child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
286 self.child_with(
287 CatInfo {
288 p2_puzzle_hash,
289 ..self.info
290 },
291 amount,
292 )
293 }
294
295 pub fn unrevocable_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
300 self.child_with(
301 CatInfo {
302 p2_puzzle_hash,
303 hidden_puzzle_hash: None,
304 ..self.info
305 },
306 amount,
307 )
308 }
309
310 pub fn child_with(&self, info: CatInfo, amount: u64) -> Self {
315 Self {
316 coin: Coin::new(self.coin.coin_id(), info.puzzle_hash().into(), amount),
317 lineage_proof: Some(self.child_lineage_proof()),
318 info,
319 }
320 }
321
322 pub fn parse(
327 allocator: &Allocator,
328 coin: Coin,
329 puzzle: Puzzle,
330 solution: NodePtr,
331 ) -> Result<Option<ParsedCat>, DriverError> {
332 let Some(cat_layer) = CatLayer::<Puzzle>::parse_puzzle(allocator, puzzle)? else {
333 return Ok(None);
334 };
335 let cat_solution = CatLayer::<Puzzle>::parse_solution(allocator, solution)?;
336
337 if let Some(revocation_layer) =
338 RevocationLayer::parse_puzzle(allocator, cat_layer.inner_puzzle)?
339 {
340 let revocation_solution =
341 RevocationLayer::parse_solution(allocator, cat_solution.inner_puzzle_solution)?;
342
343 let cat = Self::new(
344 coin,
345 cat_solution.lineage_proof,
346 CatInfo::new(
347 cat_layer.asset_id,
348 Some(revocation_layer.hidden_puzzle_hash),
349 revocation_layer.inner_puzzle_hash,
350 ),
351 );
352
353 Ok(Some(ParsedCat {
354 cat,
355 p2_puzzle: Puzzle::parse(allocator, revocation_solution.puzzle),
356 p2_solution: revocation_solution.solution,
357 revoked: revocation_solution.hidden,
358 }))
359 } else {
360 let cat = Self::new(
361 coin,
362 cat_solution.lineage_proof,
363 CatInfo::new(
364 cat_layer.asset_id,
365 None,
366 cat_layer.inner_puzzle.curried_puzzle_hash().into(),
367 ),
368 );
369
370 Ok(Some(ParsedCat {
371 cat,
372 p2_puzzle: cat_layer.inner_puzzle,
373 p2_solution: cat_solution.inner_puzzle_solution,
374 revoked: false,
375 }))
376 }
377 }
378
379 pub fn parse_children(
388 allocator: &mut Allocator,
389 parent_coin: Coin,
390 parent_puzzle: Puzzle,
391 parent_solution: NodePtr,
392 ) -> Result<Option<Vec<Self>>, DriverError> {
393 let Some(parent_layer) = CatLayer::<Puzzle>::parse_puzzle(allocator, parent_puzzle)? else {
394 return Ok(None);
395 };
396 let parent_solution = CatLayer::<Puzzle>::parse_solution(allocator, parent_solution)?;
397
398 let mut hidden_puzzle_hash = None;
399 let mut p2_puzzle_hash = parent_layer.inner_puzzle.curried_puzzle_hash().into();
400 let mut inner_spend = Spend::new(
401 parent_layer.inner_puzzle.ptr(),
402 parent_solution.inner_puzzle_solution,
403 );
404 let mut revoke = false;
405
406 if let Some(revocation_layer) =
407 RevocationLayer::parse_puzzle(allocator, parent_layer.inner_puzzle)?
408 {
409 hidden_puzzle_hash = Some(revocation_layer.hidden_puzzle_hash);
410 p2_puzzle_hash = revocation_layer.inner_puzzle_hash;
411
412 let revocation_solution =
413 RevocationLayer::parse_solution(allocator, parent_solution.inner_puzzle_solution)?;
414
415 inner_spend = Spend::new(revocation_solution.puzzle, revocation_solution.solution);
416 revoke = revocation_solution.hidden;
417 }
418
419 let cat = Cat::new(
420 parent_coin,
421 parent_solution.lineage_proof,
422 CatInfo::new(parent_layer.asset_id, hidden_puzzle_hash, p2_puzzle_hash),
423 );
424
425 let output = run_puzzle(allocator, inner_spend.puzzle, inner_spend.solution)?;
426 let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
427
428 let outputs = conditions
429 .into_iter()
430 .filter_map(Condition::into_create_coin)
431 .map(|create_coin| cat.child_from_p2_create_coin(allocator, create_coin, revoke))
432 .collect();
433
434 Ok(Some(outputs))
435 }
436
437 pub fn child_from_p2_create_coin(
445 &self,
446 allocator: &Allocator,
447 create_coin: CreateCoin<NodePtr>,
448 revoke: bool,
449 ) -> Self {
450 let child = self.child(create_coin.puzzle_hash, create_coin.amount);
452
453 let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash else {
455 return child;
456 };
457
458 if !revoke {
460 return child;
461 }
462
463 let unrevocable_child = self.unrevocable_child(create_coin.puzzle_hash, create_coin.amount);
465
466 let Memos::Some(memos) = create_coin.memos else {
468 return unrevocable_child;
469 };
470
471 let Some((hint, _)) = <(Bytes32, NodePtr)>::from_clvm(allocator, memos).ok() else {
472 return unrevocable_child;
473 };
474
475 if create_coin.puzzle_hash
478 == RevocationLayer::new(hidden_puzzle_hash, hint)
479 .tree_hash()
480 .into()
481 {
482 return self.child(hint, create_coin.amount);
483 }
484
485 unrevocable_child
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use std::slice;
495
496 use chia_puzzle_types::cat::EverythingWithSignatureTailArgs;
497 use chia_sdk_test::Simulator;
498 use chia_sdk_types::{Mod, puzzles::RevocationArgs};
499 use rstest::rstest;
500
501 use crate::{SpendWithConditions, StandardLayer};
502
503 use super::*;
504
505 #[test]
506 fn test_single_issuance_cat() -> anyhow::Result<()> {
507 let mut sim = Simulator::new();
508 let ctx = &mut SpendContext::new();
509
510 let alice = sim.bls(1);
511 let alice_p2 = StandardLayer::new(alice.pk);
512
513 let memos = ctx.hint(alice.puzzle_hash)?;
514 let (issue_cat, cats) = Cat::single_issuance(
515 ctx,
516 alice.coin.coin_id(),
517 None,
518 1,
519 Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
520 )?;
521 alice_p2.spend(ctx, alice.coin, issue_cat)?;
522
523 sim.spend_coins(ctx.take(), &[alice.sk])?;
524
525 let cat = cats[0];
526 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
527 assert_eq!(
528 cat.info.asset_id,
529 GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
530 );
531 assert!(sim.coin_state(cat.coin.coin_id()).is_some());
532
533 Ok(())
534 }
535
536 #[test]
537 fn test_multi_issuance_cat() -> anyhow::Result<()> {
538 let mut sim = Simulator::new();
539 let ctx = &mut SpendContext::new();
540
541 let alice = sim.bls(1);
542 let alice_p2 = StandardLayer::new(alice.pk);
543
544 let memos = ctx.hint(alice.puzzle_hash)?;
545 let (issue_cat, cats) = Cat::multi_issuance(
546 ctx,
547 alice.coin.coin_id(),
548 alice.pk,
549 None,
550 1,
551 Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
552 )?;
553 alice_p2.spend(ctx, alice.coin, issue_cat)?;
554 sim.spend_coins(ctx.take(), &[alice.sk])?;
555
556 let cat = cats[0];
557 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
558 assert_eq!(
559 cat.info.asset_id,
560 EverythingWithSignatureTailArgs::curry_tree_hash(alice.pk).into()
561 );
562 assert!(sim.coin_state(cat.coin.coin_id()).is_some());
563
564 Ok(())
565 }
566
567 #[test]
568 fn test_zero_cat_issuance() -> anyhow::Result<()> {
569 let mut sim = Simulator::new();
570 let ctx = &mut SpendContext::new();
571
572 let alice = sim.bls(0);
573 let alice_p2 = StandardLayer::new(alice.pk);
574
575 let memos = ctx.hint(alice.puzzle_hash)?;
576 let (issue_cat, cats) = Cat::single_issuance(
577 ctx,
578 alice.coin.coin_id(),
579 None,
580 0,
581 Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
582 )?;
583 alice_p2.spend(ctx, alice.coin, issue_cat)?;
584
585 sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
586
587 let cat = cats[0];
588 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
589 assert_eq!(
590 cat.info.asset_id,
591 GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
592 );
593 assert!(sim.coin_state(cat.coin.coin_id()).is_some());
594
595 let cat_spend = CatSpend::new(
596 cat,
597 alice_p2.spend_with_conditions(
598 ctx,
599 Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
600 )?,
601 );
602 Cat::spend_all(ctx, &[cat_spend])?;
603 sim.spend_coins(ctx.take(), &[alice.sk])?;
604
605 Ok(())
606 }
607
608 #[test]
609 fn test_missing_cat_issuance_output() -> anyhow::Result<()> {
610 let mut sim = Simulator::new();
611 let ctx = &mut SpendContext::new();
612
613 let alice = sim.bls(1);
614 let alice_p2 = StandardLayer::new(alice.pk);
615
616 let (issue_cat, _cats) =
617 Cat::single_issuance(ctx, alice.coin.coin_id(), None, 1, Conditions::new())?;
618 alice_p2.spend(ctx, alice.coin, issue_cat)?;
619
620 assert_eq!(
621 sim.spend_coins(ctx.take(), &[alice.sk])
622 .unwrap_err()
623 .to_string(),
624 "Signer error: Eval error: clvm raise"
625 );
626
627 Ok(())
628 }
629
630 #[test]
631 fn test_exceeded_cat_issuance_output() -> anyhow::Result<()> {
632 let mut sim = Simulator::new();
633 let ctx = &mut SpendContext::new();
634
635 let alice = sim.bls(2);
636 let alice_p2 = StandardLayer::new(alice.pk);
637
638 let memos = ctx.hint(alice.puzzle_hash)?;
639 let (issue_cat, _cats) = Cat::single_issuance(
640 ctx,
641 alice.coin.coin_id(),
642 None,
643 1,
644 Conditions::new().create_coin(alice.puzzle_hash, 2, memos),
645 )?;
646 alice_p2.spend(ctx, alice.coin, issue_cat)?;
647
648 assert_eq!(
649 sim.spend_coins(ctx.take(), &[alice.sk])
650 .unwrap_err()
651 .to_string(),
652 "Signer error: Eval error: clvm raise"
653 );
654
655 Ok(())
656 }
657
658 #[rstest]
659 #[case(1)]
660 #[case(2)]
661 #[case(3)]
662 #[case(10)]
663 fn test_cat_spends(#[case] coins: usize) -> anyhow::Result<()> {
664 let mut sim = Simulator::new();
665 let ctx = &mut SpendContext::new();
666
667 let mut amounts = Vec::with_capacity(coins);
669
670 for amount in 0..coins {
671 amounts.push(amount as u64);
672 }
673
674 let sum = amounts.iter().sum::<u64>();
676
677 let alice = sim.bls(sum);
678 let alice_p2 = StandardLayer::new(alice.pk);
679
680 let mut conditions = Conditions::new();
682
683 let memos = ctx.hint(alice.puzzle_hash)?;
684 for &amount in &amounts {
685 conditions = conditions.create_coin(alice.puzzle_hash, amount, memos);
686 }
687
688 let (issue_cat, mut cats) =
689 Cat::single_issuance(ctx, alice.coin.coin_id(), None, sum, conditions)?;
690 alice_p2.spend(ctx, alice.coin, issue_cat)?;
691
692 sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
693
694 for _ in 0..3 {
696 let cat_spends: Vec<CatSpend> = cats
697 .iter()
698 .map(|cat| {
699 Ok(CatSpend::new(
700 *cat,
701 alice_p2.spend_with_conditions(
702 ctx,
703 Conditions::new().create_coin(
704 alice.puzzle_hash,
705 cat.coin.amount,
706 memos,
707 ),
708 )?,
709 ))
710 })
711 .collect::<anyhow::Result<_>>()?;
712
713 cats = Cat::spend_all(ctx, &cat_spends)?;
714 sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
715 }
716
717 Ok(())
718 }
719
720 #[test]
721 fn test_different_cat_p2_puzzles() -> anyhow::Result<()> {
722 let mut sim = Simulator::new();
723 let ctx = &mut SpendContext::new();
724
725 let alice = sim.bls(2);
726 let alice_p2 = StandardLayer::new(alice.pk);
727
728 let custom_p2 = ctx.alloc(&1)?;
730 let custom_p2_puzzle_hash = ctx.tree_hash(custom_p2).into();
731
732 let memos = ctx.hint(alice.puzzle_hash)?;
733 let custom_memos = ctx.hint(custom_p2_puzzle_hash)?;
734 let (issue_cat, cats) = Cat::single_issuance(
735 ctx,
736 alice.coin.coin_id(),
737 None,
738 2,
739 Conditions::new()
740 .create_coin(alice.puzzle_hash, 1, memos)
741 .create_coin(custom_p2_puzzle_hash, 1, custom_memos),
742 )?;
743 alice_p2.spend(ctx, alice.coin, issue_cat)?;
744 sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
745
746 let spends = [
747 CatSpend::new(
748 cats[0],
749 alice_p2.spend_with_conditions(
750 ctx,
751 Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
752 )?,
753 ),
754 CatSpend::new(
755 cats[1],
756 Spend::new(
757 custom_p2,
758 ctx.alloc(&[CreateCoin::new(custom_p2_puzzle_hash, 1, custom_memos)])?,
759 ),
760 ),
761 ];
762
763 Cat::spend_all(ctx, &spends)?;
764 sim.spend_coins(ctx.take(), &[alice.sk])?;
765
766 Ok(())
767 }
768
769 #[test]
770 fn test_cat_melt() -> anyhow::Result<()> {
771 let mut sim = Simulator::new();
772 let ctx = &mut SpendContext::new();
773
774 let alice = sim.bls(10000);
775 let alice_p2 = StandardLayer::new(alice.pk);
776 let hint = ctx.hint(alice.puzzle_hash)?;
777
778 let conditions = Conditions::new().create_coin(alice.puzzle_hash, 10000, hint);
779
780 let (issue_cat, cats) =
781 Cat::multi_issuance(ctx, alice.coin.coin_id(), alice.pk, None, 10000, conditions)?;
782
783 alice_p2.spend(ctx, alice.coin, issue_cat)?;
784
785 let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
786
787 let cat_spend = CatSpend::new(
788 cats[0],
789 alice_p2.spend_with_conditions(
790 ctx,
791 Conditions::new()
792 .create_coin(alice.puzzle_hash, 7000, hint)
793 .run_cat_tail(tail, NodePtr::NIL),
794 )?,
795 );
796
797 Cat::spend_all(ctx, &[cat_spend])?;
798
799 sim.spend_coins(ctx.take(), &[alice.sk])?;
800
801 Ok(())
802 }
803
804 #[rstest]
805 fn test_cat_tail_reveal(
806 #[values(0, 1, 2)] tail_index: usize,
807 #[values(true, false)] melt: bool,
808 ) -> anyhow::Result<()> {
809 let mut sim = Simulator::new();
810 let ctx = &mut SpendContext::new();
811
812 let alice = sim.bls(15000);
813 let alice_p2 = StandardLayer::new(alice.pk);
814 let hint = ctx.hint(alice.puzzle_hash)?;
815
816 let conditions = Conditions::new()
817 .create_coin(alice.puzzle_hash, 3000, hint)
818 .create_coin(alice.puzzle_hash, 6000, hint)
819 .create_coin(alice.puzzle_hash, 1000, hint);
820
821 let (issue_cat, cats) =
822 Cat::multi_issuance(ctx, alice.coin.coin_id(), alice.pk, None, 10000, conditions)?;
823
824 alice_p2.spend(ctx, alice.coin, issue_cat)?;
825
826 let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
827
828 let cat_spends = cats
829 .into_iter()
830 .enumerate()
831 .map(|(i, cat)| {
832 let mut conditions = Conditions::new();
833
834 if i == tail_index {
836 conditions.push(RunCatTail::new(tail, NodePtr::NIL));
837
838 if !melt {
839 conditions.push(CreateCoin::new(alice.puzzle_hash, 15000, hint));
840 }
841 }
842
843 Ok(CatSpend::new(
844 cat,
845 alice_p2.spend_with_conditions(ctx, conditions)?,
846 ))
847 })
848 .collect::<anyhow::Result<Vec<_>>>()?;
849
850 Cat::spend_all(ctx, &cat_spends)?;
851
852 sim.spend_coins(ctx.take(), &[alice.sk])?;
853
854 Ok(())
855 }
856
857 #[test]
858 fn test_revocable_cat() -> anyhow::Result<()> {
859 let mut sim = Simulator::new();
860 let mut ctx = SpendContext::new();
861
862 let alice = sim.bls(10);
863 let alice_p2 = StandardLayer::new(alice.pk);
864
865 let bob = sim.bls(0);
866 let bob_p2 = StandardLayer::new(bob.pk);
867
868 let asset_id = EverythingWithSignatureTailArgs::curry_tree_hash(alice.pk).into();
869 let hint = ctx.hint(bob.puzzle_hash)?;
870
871 let (issue_cat, cats) = Cat::multi_issuance(
872 &mut ctx,
873 alice.coin.coin_id(),
874 alice.pk,
875 Some(alice.puzzle_hash),
876 10,
877 Conditions::new().create_coin(bob.puzzle_hash, 10, hint),
878 )?;
879 alice_p2.spend(&mut ctx, alice.coin, issue_cat)?;
880
881 let cat_spend = CatSpend::new(
883 cats[0],
884 bob_p2.spend_with_conditions(
885 &mut ctx,
886 Conditions::new().create_coin(bob.puzzle_hash, 10, hint),
887 )?,
888 );
889 let cats = Cat::spend_all(&mut ctx, &[cat_spend])?;
890
891 let hint = ctx.hint(alice.puzzle_hash)?;
893
894 let revocable_puzzle_hash = RevocationArgs::new(alice.puzzle_hash, alice.puzzle_hash)
895 .curry_tree_hash()
896 .into();
897
898 let cat_spend = CatSpend::revoke(
899 cats[0],
900 alice_p2.spend_with_conditions(
901 &mut ctx,
902 Conditions::new()
903 .create_coin(alice.puzzle_hash, 5, hint)
904 .create_coin(revocable_puzzle_hash, 5, hint),
905 )?,
906 );
907
908 let cats = Cat::spend_all(&mut ctx, &[cat_spend])?;
909
910 sim.spend_coins(ctx.take(), &[alice.sk.clone(), bob.sk.clone()])?;
912
913 assert_ne!(sim.coin_state(cats[0].coin.coin_id()), None);
915 assert_eq!(cats[0].info.p2_puzzle_hash, alice.puzzle_hash);
916 assert_eq!(cats[0].info.asset_id, asset_id);
917 assert_eq!(cats[0].info.hidden_puzzle_hash, None);
918
919 assert_ne!(sim.coin_state(cats[1].coin.coin_id()), None);
921 assert_eq!(cats[1].info.p2_puzzle_hash, alice.puzzle_hash);
922 assert_eq!(cats[1].info.asset_id, asset_id);
923 assert_eq!(cats[1].info.hidden_puzzle_hash, Some(alice.puzzle_hash));
924
925 let lineage_proof = cats[0].lineage_proof;
926
927 let parent_spend = sim.coin_spend(cats[0].coin.parent_coin_info).unwrap();
928 let parent_puzzle = ctx.alloc(&parent_spend.puzzle_reveal)?;
929 let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle);
930 let parent_solution = ctx.alloc(&parent_spend.solution)?;
931
932 let cats =
933 Cat::parse_children(&mut ctx, parent_spend.coin, parent_puzzle, parent_solution)?
934 .unwrap();
935
936 assert_ne!(sim.coin_state(cats[0].coin.coin_id()), None);
938 assert_eq!(cats[0].info.p2_puzzle_hash, alice.puzzle_hash);
939 assert_eq!(cats[0].info.asset_id, asset_id);
940 assert_eq!(cats[0].info.hidden_puzzle_hash, None);
941
942 assert_ne!(sim.coin_state(cats[1].coin.coin_id()), None);
944 assert_eq!(cats[1].info.p2_puzzle_hash, alice.puzzle_hash);
945 assert_eq!(cats[1].info.asset_id, asset_id);
946 assert_eq!(cats[1].info.hidden_puzzle_hash, Some(alice.puzzle_hash));
947
948 assert_eq!(cats[0].lineage_proof, lineage_proof);
949
950 let cat_spends = cats
951 .into_iter()
952 .map(|cat| {
953 Ok(CatSpend::revoke(
954 cat,
955 alice_p2.spend_with_conditions(
956 &mut ctx,
957 Conditions::new().create_coin(alice.puzzle_hash, 5, hint),
958 )?,
959 ))
960 })
961 .collect::<anyhow::Result<Vec<_>>>()?;
962
963 _ = Cat::spend_all(&mut ctx, &cat_spends)?;
964
965 sim.spend_coins(ctx.take(), &[alice.sk, bob.sk])?;
967
968 Ok(())
969 }
970}