chia_sdk_driver/actions/
send.rs

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}