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#[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(&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 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 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 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 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 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 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 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 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 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 pub fn child_from_p2_create_coin(
364 &self,
365 allocator: &Allocator,
366 create_coin: CreateCoin<NodePtr>,
367 revoke: bool,
368 ) -> Self {
369 let child = self.child(create_coin.puzzle_hash, create_coin.amount);
371
372 let Some(hidden_puzzle_hash) = self.info.hidden_puzzle_hash else {
374 return child;
375 };
376
377 if !revoke {
379 return child;
380 }
381
382 let unrevocable_child = self.unrevocable_child(create_coin.puzzle_hash, create_coin.amount);
384
385 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 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 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 let mut amounts = Vec::with_capacity(coins);
578
579 for amount in 0..coins {
580 amounts.push(amount as u64);
581 }
582
583 let sum = amounts.iter().sum::<u64>();
585
586 let alice = sim.bls(sum);
587 let alice_p2 = StandardLayer::new(alice.pk);
588
589 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 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 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}