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::{RevocationArgs, RevocationSolution},
10 run_puzzle, Condition, Conditions, Mod,
11};
12use clvm_traits::{clvm_quote, FromClvm};
13use clvm_utils::{tree_hash, 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#[must_use]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Cat {
38 pub coin: Coin,
40
41 pub lineage_proof: Option<LineageProof>,
50
51 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(&clvm_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 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 prev_subtotals = Vec::new();
152 let mut run_tail_index = None;
153 let mut children = Vec::new();
154
155 for (index, &item) in cat_spends.iter().enumerate() {
156 let output = ctx.run(item.inner_spend.puzzle, item.inner_spend.solution)?;
158 let conditions: Vec<Condition> = ctx.extract(output)?;
159
160 if conditions.iter().any(Condition::is_run_cat_tail) {
161 run_tail_index = Some(index);
162 }
163
164 let create_coins: Vec<CreateCoin<NodePtr>> = conditions
165 .into_iter()
166 .filter_map(Condition::into_create_coin)
167 .collect();
168
169 let delta = create_coins
170 .iter()
171 .fold(i128::from(item.cat.coin.amount), |delta, create_coin| {
172 delta - i128::from(create_coin.amount)
173 });
174
175 let prev_subtotal = total_delta;
176 total_delta += delta;
177
178 prev_subtotals.push(prev_subtotal);
179
180 for create_coin in create_coins {
181 children.push(
182 item.cat
183 .child_from_p2_create_coin(ctx, create_coin, item.revoke),
184 );
185 }
186 }
187
188 for (index, item) in cat_spends.iter().enumerate() {
189 let prev = &cat_spends[if index == 0 { len - 1 } else { index - 1 }];
191 let next = &cat_spends[if index == len - 1 { 0 } else { index + 1 }];
192
193 let next_p2_puzzle_hash = ctx.tree_hash(next.inner_spend.puzzle).into();
194
195 item.cat.spend(
196 ctx,
197 SingleCatSpend {
198 inner_spend: item.inner_spend,
199 prev_coin_id: prev.cat.coin.coin_id(),
200 next_coin_proof: CoinProof {
201 parent_coin_info: next.cat.coin.parent_coin_info,
202 inner_puzzle_hash: if let Some(hidden_puzzle_hash) =
203 item.cat.info.hidden_puzzle_hash
204 {
205 RevocationArgs::new(hidden_puzzle_hash, next_p2_puzzle_hash)
206 .curry_tree_hash()
207 .into()
208 } else {
209 next_p2_puzzle_hash
210 },
211 amount: next.cat.coin.amount,
212 },
213 prev_subtotal: prev_subtotals[index].try_into()?,
214 extra_delta: if run_tail_index.is_some_and(|i| i == index) {
215 -total_delta.try_into()?
216 } else {
217 0
218 },
219 revoke: item.revoke,
220 },
221 )?;
222 }
223
224 Ok(children)
225 }
226
227 pub fn spend(&self, ctx: &mut SpendContext, info: SingleCatSpend) -> Result<(), DriverError> {
234 let mut spend = info.inner_spend;
235
236 if let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash {
237 spend = RevocationLayer::new(hidden_puzzle_hash, self.info.p2_puzzle_hash)
238 .construct_spend(
239 ctx,
240 RevocationSolution::new(info.revoke, spend.puzzle, spend.solution),
241 )?;
242 }
243
244 spend = CatLayer::new(self.info.asset_id, spend.puzzle).construct_spend(
245 ctx,
246 CatSolution {
247 lineage_proof: self.lineage_proof,
248 inner_puzzle_solution: spend.solution,
249 prev_coin_id: info.prev_coin_id,
250 this_coin_info: self.coin,
251 next_coin_proof: info.next_coin_proof,
252 extra_delta: info.extra_delta,
253 prev_subtotal: info.prev_subtotal,
254 },
255 )?;
256
257 ctx.spend(self.coin, spend)?;
258
259 Ok(())
260 }
261
262 pub fn child_lineage_proof(&self) -> LineageProof {
264 LineageProof {
265 parent_parent_coin_info: self.coin.parent_coin_info,
266 parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
267 parent_amount: self.coin.amount,
268 }
269 }
270
271 pub fn child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
276 self.child_with(
277 CatInfo {
278 p2_puzzle_hash,
279 ..self.info
280 },
281 amount,
282 )
283 }
284
285 pub fn unrevocable_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
290 self.child_with(
291 CatInfo {
292 p2_puzzle_hash,
293 hidden_puzzle_hash: None,
294 ..self.info
295 },
296 amount,
297 )
298 }
299
300 pub fn child_with(&self, info: CatInfo, amount: u64) -> Self {
305 Self {
306 coin: Coin::new(self.coin.coin_id(), info.puzzle_hash().into(), amount),
307 lineage_proof: Some(self.child_lineage_proof()),
308 info,
309 }
310 }
311}
312
313impl Cat {
314 pub fn parse_children(
323 allocator: &mut Allocator,
324 parent_coin: Coin,
325 parent_puzzle: Puzzle,
326 parent_solution: NodePtr,
327 ) -> Result<Option<Vec<Self>>, DriverError>
328 where
329 Self: Sized,
330 {
331 let Some(parent_layer) = CatLayer::<Puzzle>::parse_puzzle(allocator, parent_puzzle)? else {
332 return Ok(None);
333 };
334 let parent_solution = CatLayer::<Puzzle>::parse_solution(allocator, parent_solution)?;
335
336 let mut hidden_puzzle_hash = None;
337 let mut inner_spend = Spend::new(
338 parent_layer.inner_puzzle.ptr(),
339 parent_solution.inner_puzzle_solution,
340 );
341 let mut revoke = false;
342
343 if let Some(revocation_layer) =
344 RevocationLayer::parse_puzzle(allocator, parent_layer.inner_puzzle)?
345 {
346 hidden_puzzle_hash = Some(revocation_layer.hidden_puzzle_hash);
347
348 let revocation_solution =
349 RevocationLayer::parse_solution(allocator, parent_solution.inner_puzzle_solution)?;
350
351 inner_spend = Spend::new(revocation_solution.puzzle, revocation_solution.solution);
352 revoke = revocation_solution.hidden;
353 }
354
355 let cat = Cat::new(
356 parent_coin,
357 parent_solution.lineage_proof,
358 CatInfo::new(
359 parent_layer.asset_id,
360 hidden_puzzle_hash,
361 tree_hash(allocator, inner_spend.puzzle).into(),
362 ),
363 );
364
365 let output = run_puzzle(allocator, inner_spend.puzzle, inner_spend.solution)?;
366 let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
367
368 let outputs = conditions
369 .into_iter()
370 .filter_map(Condition::into_create_coin)
371 .map(|create_coin| cat.child_from_p2_create_coin(allocator, create_coin, revoke))
372 .collect();
373
374 Ok(Some(outputs))
375 }
376
377 pub fn child_from_p2_create_coin(
385 &self,
386 allocator: &Allocator,
387 create_coin: CreateCoin<NodePtr>,
388 revoke: bool,
389 ) -> Self {
390 let child = self.child(create_coin.puzzle_hash, create_coin.amount);
392
393 let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash else {
395 return child;
396 };
397
398 if !revoke {
400 return child;
401 }
402
403 let unrevocable_child = self.unrevocable_child(create_coin.puzzle_hash, create_coin.amount);
405
406 let Memos::Some(memos) = create_coin.memos else {
408 return unrevocable_child;
409 };
410
411 let Some((hint, _)) = <(Bytes32, NodePtr)>::from_clvm(allocator, memos).ok() else {
412 return unrevocable_child;
413 };
414
415 if hint
418 == RevocationLayer::new(hidden_puzzle_hash, hint)
419 .tree_hash()
420 .into()
421 {
422 return self.child(hint, create_coin.amount);
423 }
424
425 unrevocable_child
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use chia_puzzle_types::cat::EverythingWithSignatureTailArgs;
435 use chia_sdk_test::Simulator;
436 use rstest::rstest;
437
438 use crate::{SpendWithConditions, StandardLayer};
439
440 use super::*;
441
442 #[test]
443 fn test_single_issuance_cat() -> anyhow::Result<()> {
444 let mut sim = Simulator::new();
445 let ctx = &mut SpendContext::new();
446
447 let alice = sim.bls(1);
448 let alice_p2 = StandardLayer::new(alice.pk);
449
450 let memos = ctx.hint(alice.puzzle_hash)?;
451 let (issue_cat, cats) = Cat::issue_with_coin(
452 ctx,
453 alice.coin.coin_id(),
454 1,
455 Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
456 )?;
457 alice_p2.spend(ctx, alice.coin, issue_cat)?;
458
459 sim.spend_coins(ctx.take(), &[alice.sk])?;
460
461 let cat = cats[0];
462 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
463 assert_eq!(
464 cat.info.asset_id,
465 GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
466 );
467 assert!(sim.coin_state(cat.coin.coin_id()).is_some());
468
469 Ok(())
470 }
471
472 #[test]
473 fn test_multi_issuance_cat() -> anyhow::Result<()> {
474 let mut sim = Simulator::new();
475 let ctx = &mut SpendContext::new();
476
477 let alice = sim.bls(1);
478 let alice_p2 = StandardLayer::new(alice.pk);
479
480 let memos = ctx.hint(alice.puzzle_hash)?;
481 let (issue_cat, cats) = Cat::issue_with_key(
482 ctx,
483 alice.coin.coin_id(),
484 alice.pk,
485 1,
486 Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
487 )?;
488 alice_p2.spend(ctx, alice.coin, issue_cat)?;
489 sim.spend_coins(ctx.take(), &[alice.sk])?;
490
491 let cat = cats[0];
492 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
493 assert_eq!(
494 cat.info.asset_id,
495 EverythingWithSignatureTailArgs::curry_tree_hash(alice.pk).into()
496 );
497 assert!(sim.coin_state(cat.coin.coin_id()).is_some());
498
499 Ok(())
500 }
501
502 #[test]
503 fn test_zero_cat_issuance() -> anyhow::Result<()> {
504 let mut sim = Simulator::new();
505 let ctx = &mut SpendContext::new();
506
507 let alice = sim.bls(0);
508 let alice_p2 = StandardLayer::new(alice.pk);
509
510 let memos = ctx.hint(alice.puzzle_hash)?;
511 let (issue_cat, cats) = Cat::issue_with_coin(
512 ctx,
513 alice.coin.coin_id(),
514 0,
515 Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
516 )?;
517 alice_p2.spend(ctx, alice.coin, issue_cat)?;
518
519 sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
520
521 let cat = cats[0];
522 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
523 assert_eq!(
524 cat.info.asset_id,
525 GenesisByCoinIdTailArgs::curry_tree_hash(alice.coin.coin_id()).into()
526 );
527 assert!(sim.coin_state(cat.coin.coin_id()).is_some());
528
529 let cat_spend = CatSpend::new(
530 cat,
531 alice_p2.spend_with_conditions(
532 ctx,
533 Conditions::new().create_coin(alice.puzzle_hash, 0, memos),
534 )?,
535 );
536 Cat::spend_all(ctx, &[cat_spend])?;
537 sim.spend_coins(ctx.take(), &[alice.sk])?;
538
539 Ok(())
540 }
541
542 #[test]
543 fn test_missing_cat_issuance_output() -> anyhow::Result<()> {
544 let mut sim = Simulator::new();
545 let ctx = &mut SpendContext::new();
546
547 let alice = sim.bls(1);
548 let alice_p2 = StandardLayer::new(alice.pk);
549
550 let (issue_cat, _cats) =
551 Cat::issue_with_coin(ctx, alice.coin.coin_id(), 1, Conditions::new())?;
552 alice_p2.spend(ctx, alice.coin, issue_cat)?;
553
554 assert_eq!(
555 sim.spend_coins(ctx.take(), &[alice.sk])
556 .unwrap_err()
557 .to_string(),
558 "Signer error: Eval error: Error at NodePtr(SmallAtom, 0): clvm raise"
559 );
560
561 Ok(())
562 }
563
564 #[test]
565 fn test_exceeded_cat_issuance_output() -> anyhow::Result<()> {
566 let mut sim = Simulator::new();
567 let ctx = &mut SpendContext::new();
568
569 let alice = sim.bls(2);
570 let alice_p2 = StandardLayer::new(alice.pk);
571
572 let memos = ctx.hint(alice.puzzle_hash)?;
573 let (issue_cat, _cats) = Cat::issue_with_coin(
574 ctx,
575 alice.coin.coin_id(),
576 1,
577 Conditions::new().create_coin(alice.puzzle_hash, 2, memos),
578 )?;
579 alice_p2.spend(ctx, alice.coin, issue_cat)?;
580
581 assert_eq!(
582 sim.spend_coins(ctx.take(), &[alice.sk])
583 .unwrap_err()
584 .to_string(),
585 "Signer error: Eval error: Error at NodePtr(SmallAtom, 0): clvm raise"
586 );
587
588 Ok(())
589 }
590
591 #[rstest]
592 #[case(1)]
593 #[case(2)]
594 #[case(3)]
595 #[case(10)]
596 fn test_cat_spends(#[case] coins: usize) -> anyhow::Result<()> {
597 let mut sim = Simulator::new();
598 let ctx = &mut SpendContext::new();
599
600 let mut amounts = Vec::with_capacity(coins);
602
603 for amount in 0..coins {
604 amounts.push(amount as u64);
605 }
606
607 let sum = amounts.iter().sum::<u64>();
609
610 let alice = sim.bls(sum);
611 let alice_p2 = StandardLayer::new(alice.pk);
612
613 let mut conditions = Conditions::new();
615
616 let memos = ctx.hint(alice.puzzle_hash)?;
617 for &amount in &amounts {
618 conditions = conditions.create_coin(alice.puzzle_hash, amount, memos);
619 }
620
621 let (issue_cat, mut cats) =
622 Cat::issue_with_coin(ctx, alice.coin.coin_id(), sum, conditions)?;
623 alice_p2.spend(ctx, alice.coin, issue_cat)?;
624
625 sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
626
627 for _ in 0..3 {
629 let cat_spends: Vec<CatSpend> = cats
630 .iter()
631 .map(|cat| {
632 Ok(CatSpend::new(
633 *cat,
634 alice_p2.spend_with_conditions(
635 ctx,
636 Conditions::new().create_coin(
637 alice.puzzle_hash,
638 cat.coin.amount,
639 memos,
640 ),
641 )?,
642 ))
643 })
644 .collect::<anyhow::Result<_>>()?;
645
646 cats = Cat::spend_all(ctx, &cat_spends)?;
647 sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
648 }
649
650 Ok(())
651 }
652
653 #[test]
654 fn test_different_cat_p2_puzzles() -> anyhow::Result<()> {
655 let mut sim = Simulator::new();
656 let ctx = &mut SpendContext::new();
657
658 let alice = sim.bls(2);
659 let alice_p2 = StandardLayer::new(alice.pk);
660
661 let custom_p2 = ctx.alloc(&1)?;
663 let custom_p2_puzzle_hash = ctx.tree_hash(custom_p2).into();
664
665 let memos = ctx.hint(alice.puzzle_hash)?;
666 let custom_memos = ctx.hint(custom_p2_puzzle_hash)?;
667 let (issue_cat, cats) = Cat::issue_with_coin(
668 ctx,
669 alice.coin.coin_id(),
670 2,
671 Conditions::new()
672 .create_coin(alice.puzzle_hash, 1, memos)
673 .create_coin(custom_p2_puzzle_hash, 1, custom_memos),
674 )?;
675 alice_p2.spend(ctx, alice.coin, issue_cat)?;
676 sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
677
678 let spends = [
679 CatSpend::new(
680 cats[0],
681 alice_p2.spend_with_conditions(
682 ctx,
683 Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
684 )?,
685 ),
686 CatSpend::new(
687 cats[1],
688 Spend::new(
689 custom_p2,
690 ctx.alloc(&[CreateCoin::new(custom_p2_puzzle_hash, 1, custom_memos)])?,
691 ),
692 ),
693 ];
694
695 Cat::spend_all(ctx, &spends)?;
696 sim.spend_coins(ctx.take(), &[alice.sk])?;
697
698 Ok(())
699 }
700
701 #[test]
702 fn test_cat_melt() -> anyhow::Result<()> {
703 let mut sim = Simulator::new();
704 let ctx = &mut SpendContext::new();
705
706 let alice = sim.bls(10000);
707 let alice_p2 = StandardLayer::new(alice.pk);
708
709 let memos = ctx.hint(alice.puzzle_hash)?;
710 let conditions = Conditions::new().create_coin(alice.puzzle_hash, 10000, memos);
711 let (issue_cat, cats) =
712 Cat::issue_with_key(ctx, alice.coin.coin_id(), alice.pk, 10000, conditions)?;
713 alice_p2.spend(ctx, alice.coin, issue_cat)?;
714
715 let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
716
717 let cat_spend = CatSpend::new(
718 cats[0],
719 alice_p2.spend_with_conditions(
720 ctx,
721 Conditions::new()
722 .create_coin(alice.puzzle_hash, 7000, memos)
723 .run_cat_tail(tail, NodePtr::NIL),
724 )?,
725 );
726
727 Cat::spend_all(ctx, &[cat_spend])?;
728
729 sim.spend_coins(ctx.take(), &[alice.sk])?;
730
731 Ok(())
732 }
733}