1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::{cat::GenesisByCoinIdTailArgs, Memos};
3use chia_sdk_types::{conditions::CreateCoin, Conditions};
4use clvmr::NodePtr;
5
6use crate::{
7 Asset, Cat, CatInfo, Deltas, DriverError, FungibleSpend, Id, Spend, SpendAction, SpendContext,
8 SpendKind, Spends,
9};
10
11#[derive(Debug, Clone, Copy)]
12pub enum TailIssuance {
13 Single,
14 Multiple(Spend),
15}
16
17#[derive(Debug, Clone, Copy)]
18pub struct IssueCatAction {
19 pub issuance: TailIssuance,
20 pub hidden_puzzle_hash: Option<Bytes32>,
21 pub amount: u64,
22}
23
24impl IssueCatAction {
25 pub fn new(issuance: TailIssuance, hidden_puzzle_hash: Option<Bytes32>, amount: u64) -> Self {
26 Self {
27 issuance,
28 hidden_puzzle_hash,
29 amount,
30 }
31 }
32}
33
34impl SpendAction for IssueCatAction {
35 fn calculate_delta(&self, deltas: &mut Deltas, index: usize) {
36 deltas.update(Id::New(index)).input += self.amount;
37 deltas.update(Id::Xch).output += self.amount;
38 deltas.set_needed(Id::Xch);
39 }
40
41 fn spend(
42 &self,
43 ctx: &mut SpendContext,
44 spends: &mut Spends,
45 index: usize,
46 ) -> Result<(), DriverError> {
47 let asset_id = match self.issuance {
48 TailIssuance::Single => None,
49 TailIssuance::Multiple(spend) => Some(ctx.tree_hash(spend.puzzle).into()),
50 };
51
52 let source_index = spends.xch.cat_issuance_source(ctx, asset_id, self.amount)?;
53 let source = &mut spends.xch.items[source_index];
54
55 let asset_id = asset_id.unwrap_or_else(|| {
56 GenesisByCoinIdTailArgs::curry_tree_hash(source.asset.coin_id()).into()
57 });
58
59 let cat_info = CatInfo::new(
60 asset_id,
61 self.hidden_puzzle_hash,
62 source.asset.p2_puzzle_hash(),
63 );
64
65 let create_coin = CreateCoin::new(cat_info.puzzle_hash().into(), self.amount, Memos::None);
66 let parent_puzzle_hash = source.asset.full_puzzle_hash();
67
68 source.kind.create_coin_with_assertion(
69 ctx,
70 parent_puzzle_hash,
71 &mut spends.xch.payment_assertions,
72 create_coin,
73 );
74
75 let eve_cat = Cat::new(
76 Coin::new(
77 source.asset.coin_id(),
78 cat_info.puzzle_hash().into(),
79 self.amount,
80 ),
81 None,
82 cat_info,
83 );
84
85 let id = if spends.cats.contains_key(&Id::Existing(asset_id)) {
86 Id::Existing(asset_id)
87 } else {
88 Id::New(index)
89 };
90
91 let mut cat_spend = FungibleSpend::new(eve_cat, true);
92
93 let tail_spend = match self.issuance {
94 TailIssuance::Single => {
95 let puzzle = ctx.curry(GenesisByCoinIdTailArgs::new(source.asset.coin_id()))?;
96 Spend::new(puzzle, NodePtr::NIL)
97 }
98 TailIssuance::Multiple(spend) => spend,
99 };
100
101 match &mut cat_spend.kind {
102 SpendKind::Conditions(spend) => {
103 spend.add_conditions(
104 Conditions::new().run_cat_tail(tail_spend.puzzle, tail_spend.solution),
105 );
106 }
107 SpendKind::Settlement(_) => {
108 return Err(DriverError::CannotEmitConditions);
109 }
110 }
111
112 spends.cats.entry(id).or_default().items.push(cat_spend);
113
114 Ok(())
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use anyhow::Result;
121 use chia_puzzle_types::cat::EverythingWithSignatureTailArgs;
122 use chia_sdk_test::Simulator;
123 use indexmap::indexmap;
124 use rstest::rstest;
125
126 use crate::{Action, Relation};
127
128 use super::*;
129
130 #[rstest]
131 #[case::normal(None)]
132 #[case::revocable(Some(Bytes32::default()))]
133 fn test_action_single_issuance_cat(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
134 let mut sim = Simulator::new();
135 let mut ctx = SpendContext::new();
136
137 let alice = sim.bls(1);
138
139 let mut spends = Spends::new(alice.puzzle_hash);
140 spends.add(alice.coin);
141
142 let deltas = spends.apply(&mut ctx, &[Action::single_issue_cat(hidden_puzzle_hash, 1)])?;
143
144 let outputs = spends.finish_with_keys(
145 &mut ctx,
146 &deltas,
147 Relation::None,
148 &indexmap! { alice.puzzle_hash => alice.pk },
149 )?;
150
151 sim.spend_coins(ctx.take(), &[alice.sk])?;
152
153 let cat = outputs.cats[&Id::New(0)][0];
154 assert_ne!(sim.coin_state(cat.coin.coin_id()), None);
155 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
156 assert_eq!(cat.coin.amount, 1);
157
158 Ok(())
159 }
160
161 #[rstest]
162 #[case::normal(None)]
163 #[case::revocable(Some(Bytes32::default()))]
164 fn test_action_multiple_issuance_cat(
165 #[case] hidden_puzzle_hash: Option<Bytes32>,
166 ) -> Result<()> {
167 let mut sim = Simulator::new();
168 let mut ctx = SpendContext::new();
169
170 let alice = sim.bls(1);
171
172 let tail = ctx.curry(EverythingWithSignatureTailArgs::new(alice.pk))?;
173
174 let mut spends = Spends::new(alice.puzzle_hash);
175 spends.add(alice.coin);
176
177 let deltas = spends.apply(
178 &mut ctx,
179 &[Action::issue_cat(
180 Spend::new(tail, NodePtr::NIL),
181 hidden_puzzle_hash,
182 1,
183 )],
184 )?;
185
186 let outputs = spends.finish_with_keys(
187 &mut ctx,
188 &deltas,
189 Relation::None,
190 &indexmap! { alice.puzzle_hash => alice.pk },
191 )?;
192
193 sim.spend_coins(ctx.take(), &[alice.sk])?;
194
195 let cat = outputs.cats[&Id::New(0)][0];
196 assert_ne!(sim.coin_state(cat.coin.coin_id()), None);
197 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
198 assert_eq!(cat.coin.amount, 1);
199
200 Ok(())
201 }
202}