1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::Memos;
3use chia_sdk_types::conditions::CreateCoin;
4
5use crate::{
6 Asset, Deltas, DriverError, Id, Output, SingletonDestination, SpendAction, SpendContext, Spends,
7};
8
9#[derive(Debug, Clone, Copy)]
10pub struct SendAction {
11 pub id: Id,
12 pub puzzle_hash: Bytes32,
13 pub amount: u64,
14 pub memos: Memos,
15}
16
17impl SendAction {
18 pub fn new(id: Id, puzzle_hash: Bytes32, amount: u64, memos: Memos) -> Self {
19 Self {
20 id,
21 puzzle_hash,
22 amount,
23 memos,
24 }
25 }
26}
27
28impl SpendAction for SendAction {
29 fn calculate_delta(&self, deltas: &mut Deltas, _index: usize) {
30 deltas.update(self.id).output += self.amount;
31 deltas.set_needed(self.id);
32 }
33
34 fn spend(
35 &self,
36 ctx: &mut SpendContext,
37 spends: &mut Spends,
38 _index: usize,
39 ) -> Result<(), DriverError> {
40 let output = Output::new(self.puzzle_hash, self.amount);
41 let create_coin = CreateCoin::new(self.puzzle_hash, self.amount, self.memos);
42
43 if matches!(self.id, Id::Xch) {
44 let source = spends.xch.output_source(ctx, &output)?;
45 let parent = &mut spends.xch.items[source];
46 let parent_puzzle_hash = parent.asset.full_puzzle_hash();
47
48 parent.kind.create_coin_with_assertion(
49 ctx,
50 parent_puzzle_hash,
51 &mut spends.xch.payment_assertions,
52 create_coin,
53 );
54
55 let coin = Coin::new(
56 parent.asset.coin_id(),
57 create_coin.puzzle_hash,
58 create_coin.amount,
59 );
60
61 spends.outputs.xch.push(coin);
62 } else if let Some(cat) = spends.cats.get_mut(&self.id) {
63 let source = cat.output_source(ctx, &output)?;
64 let parent = &mut cat.items[source];
65 let parent_puzzle_hash = parent.asset.full_puzzle_hash();
66
67 parent.kind.create_coin_with_assertion(
68 ctx,
69 parent_puzzle_hash,
70 &mut cat.payment_assertions,
71 create_coin,
72 );
73
74 let cat = parent
75 .asset
76 .child(create_coin.puzzle_hash, create_coin.amount);
77
78 spends.outputs.cats.entry(self.id).or_default().push(cat);
79 } else if let Some(did) = spends.dids.get_mut(&self.id) {
80 let source = did.last_mut()?;
81 source.child_info.destination = Some(SingletonDestination::CreateCoin(create_coin));
82 } else if let Some(nft) = spends.nfts.get_mut(&self.id) {
83 let source = nft.last_mut()?;
84 source.child_info.destination = Some(create_coin);
85 } else if let Some(option) = spends.options.get_mut(&self.id) {
86 let source = option.last_mut()?;
87 source.child_info.destination = Some(SingletonDestination::CreateCoin(create_coin));
88 } else {
89 return Err(DriverError::InvalidAssetId);
90 }
91
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use anyhow::Result;
99 use chia_protocol::Coin;
100 use chia_puzzle_types::standard::StandardArgs;
101 use chia_sdk_test::{BlsPair, Simulator};
102 use indexmap::indexmap;
103 use rstest::rstest;
104
105 use crate::{Action, Cat, Relation};
106
107 use super::*;
108
109 #[test]
110 fn test_action_send_xch() -> Result<()> {
111 let mut sim = Simulator::new();
112 let mut ctx = SpendContext::new();
113
114 let alice = sim.bls(1);
115
116 let mut spends = Spends::new(alice.puzzle_hash);
117 spends.add(alice.coin);
118
119 let deltas = spends.apply(
120 &mut ctx,
121 &[Action::send(Id::Xch, alice.puzzle_hash, 1, Memos::None)],
122 )?;
123
124 let outputs = spends.finish_with_keys(
125 &mut ctx,
126 &deltas,
127 Relation::None,
128 &indexmap! { alice.puzzle_hash => alice.pk },
129 )?;
130
131 sim.spend_coins(ctx.take(), &[alice.sk])?;
132
133 let coin = outputs.xch[0];
134 assert_eq!(outputs.xch.len(), 1);
135 assert_ne!(sim.coin_state(coin.coin_id()), None);
136 assert_eq!(coin.amount, 1);
137
138 Ok(())
139 }
140
141 #[test]
142 fn test_action_send_xch_with_change() -> Result<()> {
143 let mut sim = Simulator::new();
144 let mut ctx = SpendContext::new();
145
146 let alice = sim.bls(5);
147 let bob = BlsPair::new(0);
148 let bob_puzzle_hash = StandardArgs::curry_tree_hash(bob.pk).into();
149
150 let mut spends = Spends::new(alice.puzzle_hash);
151 spends.add(alice.coin);
152
153 let deltas = spends.apply(
154 &mut ctx,
155 &[Action::send(Id::Xch, bob_puzzle_hash, 2, Memos::None)],
156 )?;
157
158 let outputs = spends.finish_with_keys(
159 &mut ctx,
160 &deltas,
161 Relation::None,
162 &indexmap! { alice.puzzle_hash => alice.pk },
163 )?;
164
165 sim.spend_coins(ctx.take(), &[alice.sk])?;
166
167 assert_eq!(outputs.xch.len(), 2);
168
169 let change = outputs.xch[0];
170 assert_ne!(sim.coin_state(change.coin_id()), None);
171 assert_eq!(change.amount, 2);
172 assert_eq!(change.puzzle_hash, bob_puzzle_hash);
173
174 let coin = outputs.xch[1];
175 assert_ne!(sim.coin_state(coin.coin_id()), None);
176 assert_eq!(coin.amount, 3);
177 assert_eq!(coin.puzzle_hash, alice.puzzle_hash);
178
179 Ok(())
180 }
181
182 #[test]
183 fn test_action_send_xch_split() -> Result<()> {
184 let mut sim = Simulator::new();
185 let mut ctx = SpendContext::new();
186
187 let alice = sim.bls(3);
188
189 let mut spends = Spends::new(alice.puzzle_hash);
190 spends.add(alice.coin);
191
192 let deltas = spends.apply(
193 &mut ctx,
194 &[
195 Action::send(Id::Xch, alice.puzzle_hash, 1, Memos::None),
196 Action::send(Id::Xch, alice.puzzle_hash, 1, Memos::None),
197 Action::send(Id::Xch, alice.puzzle_hash, 1, Memos::None),
198 ],
199 )?;
200
201 let outputs = spends.finish_with_keys(
202 &mut ctx,
203 &deltas,
204 Relation::None,
205 &indexmap! { alice.puzzle_hash => alice.pk },
206 )?;
207
208 sim.spend_coins(ctx.take(), &[alice.sk])?;
209
210 assert_eq!(outputs.xch.len(), 3);
211
212 let coins: Vec<Coin> = outputs
213 .xch
214 .iter()
215 .copied()
216 .filter(|coin| {
217 sim.coin_state(coin.coin_id())
218 .expect("missing coin")
219 .spent_height
220 .is_none()
221 })
222 .collect();
223
224 assert_eq!(coins.len(), 3);
225
226 for coin in coins {
227 assert_eq!(coin.puzzle_hash, alice.puzzle_hash);
228 assert_eq!(coin.amount, 1);
229 }
230
231 Ok(())
232 }
233
234 #[rstest]
235 #[case::normal(None)]
236 #[case::revocable(Some(Bytes32::default()))]
237 fn test_action_send_cat(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
238 let mut sim = Simulator::new();
239 let mut ctx = SpendContext::new();
240
241 let alice = sim.bls(1);
242 let hint = ctx.hint(alice.puzzle_hash)?;
243
244 let mut spends = Spends::new(alice.puzzle_hash);
245 spends.add(alice.coin);
246
247 let deltas = spends.apply(
248 &mut ctx,
249 &[
250 Action::single_issue_cat(hidden_puzzle_hash, 1),
251 Action::send(Id::New(0), alice.puzzle_hash, 1, hint),
252 ],
253 )?;
254
255 let outputs = spends.finish_with_keys(
256 &mut ctx,
257 &deltas,
258 Relation::None,
259 &indexmap! { alice.puzzle_hash => alice.pk },
260 )?;
261
262 sim.spend_coins(ctx.take(), &[alice.sk])?;
263
264 let cat = outputs.cats[&Id::New(0)][0];
265 assert_ne!(sim.coin_state(cat.coin.coin_id()), None);
266 assert_eq!(cat.coin.amount, 1);
267
268 Ok(())
269 }
270
271 #[rstest]
272 #[case::normal(None)]
273 #[case::revocable(Some(Bytes32::default()))]
274 fn test_action_send_cat_with_change(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
275 let mut sim = Simulator::new();
276 let mut ctx = SpendContext::new();
277
278 let alice = sim.bls(5);
279 let bob = BlsPair::new(0);
280 let bob_puzzle_hash = StandardArgs::curry_tree_hash(bob.pk).into();
281 let bob_hint = ctx.hint(bob_puzzle_hash)?;
282
283 let mut spends = Spends::new(alice.puzzle_hash);
284 spends.add(alice.coin);
285
286 let deltas = spends.apply(
287 &mut ctx,
288 &[
289 Action::single_issue_cat(hidden_puzzle_hash, 5),
290 Action::send(Id::New(0), bob_puzzle_hash, 2, bob_hint),
291 ],
292 )?;
293
294 let outputs = spends.finish_with_keys(
295 &mut ctx,
296 &deltas,
297 Relation::None,
298 &indexmap! { alice.puzzle_hash => alice.pk },
299 )?;
300
301 sim.spend_coins(ctx.take(), &[alice.sk])?;
302
303 let cats = &outputs.cats[&Id::New(0)];
304 assert_eq!(cats.len(), 2);
305
306 let change = cats[0];
307 assert_ne!(sim.coin_state(change.coin.coin_id()), None);
308 assert_eq!(change.coin.amount, 2);
309 assert_eq!(change.info.p2_puzzle_hash, bob_puzzle_hash);
310
311 let cat = cats[1];
312 assert_ne!(sim.coin_state(cat.coin.coin_id()), None);
313 assert_eq!(cat.coin.amount, 3);
314 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
315
316 Ok(())
317 }
318
319 #[rstest]
320 #[case::normal(None)]
321 #[case::revocable(Some(Bytes32::default()))]
322 fn test_action_send_cat_split(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
323 let mut sim = Simulator::new();
324 let mut ctx = SpendContext::new();
325
326 let alice = sim.bls(3);
327 let hint = ctx.hint(alice.puzzle_hash)?;
328
329 let mut spends = Spends::new(alice.puzzle_hash);
330 spends.add(alice.coin);
331
332 let deltas = spends.apply(
333 &mut ctx,
334 &[
335 Action::single_issue_cat(hidden_puzzle_hash, 3),
336 Action::send(Id::New(0), alice.puzzle_hash, 1, hint),
337 Action::send(Id::New(0), alice.puzzle_hash, 1, hint),
338 Action::send(Id::New(0), alice.puzzle_hash, 1, hint),
339 ],
340 )?;
341
342 let outputs = spends.finish_with_keys(
343 &mut ctx,
344 &deltas,
345 Relation::None,
346 &indexmap! { alice.puzzle_hash => alice.pk },
347 )?;
348
349 sim.spend_coins(ctx.take(), &[alice.sk])?;
350
351 let cats = &outputs.cats[&Id::New(0)];
352 assert_eq!(cats.len(), 3);
353
354 let cats: Vec<Cat> = cats
355 .iter()
356 .copied()
357 .filter(|cat| {
358 sim.coin_state(cat.coin.coin_id())
359 .expect("missing coin")
360 .spent_height
361 .is_none()
362 })
363 .collect();
364
365 assert_eq!(cats.len(), 3);
366
367 for cat in cats {
368 assert_eq!(cat.info.p2_puzzle_hash, alice.puzzle_hash);
369 assert_eq!(cat.coin.amount, 1);
370 }
371
372 Ok(())
373 }
374}