chia_sdk_driver/primitives/
clawback_v2.rs

1use chia_protocol::{Bytes32, Coin};
2use chia_sdk_types::{
3    conditions::{AssertBeforeSecondsAbsolute, AssertSecondsAbsolute, CreateCoin, Memos},
4    puzzles::{AugmentedConditionArgs, AugmentedConditionSolution, P2OneOfManySolution},
5    Conditions, MerkleTree, Mod,
6};
7use clvm_traits::{clvm_list, clvm_quote, match_list, FromClvm};
8use clvm_utils::{ToTreeHash, TreeHash};
9use clvmr::{Allocator, NodePtr};
10
11use crate::{DriverError, Layer, P2OneOfManyLayer, Spend, SpendContext, SpendWithConditions};
12
13pub type PushThroughPath = (
14    u8,
15    match_list!(AssertSecondsAbsolute, CreateCoin<[Bytes32; 1]>),
16);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct ClawbackV2 {
20    pub sender_puzzle_hash: Bytes32,
21    pub receiver_puzzle_hash: Bytes32,
22    pub seconds: u64,
23    pub amount: u64,
24    pub hinted: bool,
25}
26
27impl ClawbackV2 {
28    pub fn new(
29        sender_puzzle_hash: Bytes32,
30        receiver_puzzle_hash: Bytes32,
31        seconds: u64,
32        amount: u64,
33        hinted: bool,
34    ) -> Self {
35        Self {
36            sender_puzzle_hash,
37            receiver_puzzle_hash,
38            seconds,
39            amount,
40            hinted,
41        }
42    }
43
44    pub fn from_memo(
45        allocator: &Allocator,
46        memo: NodePtr,
47        receiver_puzzle_hash: Bytes32,
48        amount: u64,
49        hinted: bool,
50        expected_puzzle_hash: Bytes32,
51    ) -> Option<Self> {
52        let (sender_puzzle_hash, (seconds, ())) =
53            <(Bytes32, (u64, ()))>::from_clvm(allocator, memo).ok()?;
54
55        let clawback = Self {
56            sender_puzzle_hash,
57            receiver_puzzle_hash,
58            seconds,
59            amount,
60            hinted,
61        };
62
63        if clawback.tree_hash() != expected_puzzle_hash.into() {
64            return None;
65        }
66
67        Some(clawback)
68    }
69
70    pub fn memo(&self) -> (Bytes32, (u64, ())) {
71        (self.sender_puzzle_hash, (self.seconds, ()))
72    }
73
74    pub fn merkle_tree(&self) -> MerkleTree {
75        MerkleTree::new(&[
76            self.sender_path_hash(),
77            self.receiver_path_hash(),
78            self.push_through_path_hash(),
79        ])
80    }
81
82    pub fn sender_path_hash(&self) -> Bytes32 {
83        AugmentedConditionArgs::<TreeHash, TreeHash>::new(
84            AssertBeforeSecondsAbsolute::new(self.seconds).into(),
85            self.sender_puzzle_hash.into(),
86        )
87        .curry_tree_hash()
88        .into()
89    }
90
91    pub fn receiver_path_hash(&self) -> Bytes32 {
92        AugmentedConditionArgs::<TreeHash, TreeHash>::new(
93            AssertSecondsAbsolute::new(self.seconds).into(),
94            self.receiver_puzzle_hash.into(),
95        )
96        .curry_tree_hash()
97        .into()
98    }
99
100    pub fn push_through_path(&self) -> PushThroughPath {
101        clvm_quote!(clvm_list!(
102            AssertSecondsAbsolute::new(self.seconds),
103            CreateCoin::new(
104                self.receiver_puzzle_hash,
105                self.amount,
106                if self.hinted {
107                    Memos::Some([self.receiver_puzzle_hash])
108                } else {
109                    Memos::None
110                }
111            )
112        ))
113    }
114
115    pub fn push_through_path_hash(&self) -> Bytes32 {
116        self.push_through_path().tree_hash().into()
117    }
118
119    pub fn into_1_of_n(&self) -> P2OneOfManyLayer {
120        P2OneOfManyLayer::new(self.merkle_tree().root())
121    }
122
123    pub fn sender_spend(&self, ctx: &mut SpendContext, spend: Spend) -> Result<Spend, DriverError> {
124        let merkle_tree = self.merkle_tree();
125
126        let puzzle_hash = self.sender_path_hash();
127        let merkle_proof = merkle_tree
128            .proof(puzzle_hash)
129            .ok_or(DriverError::InvalidMerkleProof)?;
130
131        let puzzle = ctx.curry(AugmentedConditionArgs::<NodePtr, NodePtr>::new(
132            AssertBeforeSecondsAbsolute::new(self.seconds).into(),
133            spend.puzzle,
134        ))?;
135
136        let solution = ctx.alloc(&AugmentedConditionSolution::new(spend.solution))?;
137
138        P2OneOfManyLayer::new(merkle_tree.root()).construct_spend(
139            ctx,
140            P2OneOfManySolution::new(merkle_proof, puzzle, solution),
141        )
142    }
143
144    pub fn receiver_spend(
145        &self,
146        ctx: &mut SpendContext,
147        spend: Spend,
148    ) -> Result<Spend, DriverError> {
149        let merkle_tree = self.merkle_tree();
150
151        let puzzle_hash = self.receiver_path_hash();
152        let merkle_proof = merkle_tree
153            .proof(puzzle_hash)
154            .ok_or(DriverError::InvalidMerkleProof)?;
155
156        let puzzle = ctx.curry(AugmentedConditionArgs::<NodePtr, NodePtr>::new(
157            AssertSecondsAbsolute::new(self.seconds).into(),
158            spend.puzzle,
159        ))?;
160
161        let solution = ctx.alloc(&AugmentedConditionSolution::new(spend.solution))?;
162
163        P2OneOfManyLayer::new(merkle_tree.root()).construct_spend(
164            ctx,
165            P2OneOfManySolution::new(merkle_proof, puzzle, solution),
166        )
167    }
168
169    pub fn push_through_spend(&self, ctx: &mut SpendContext) -> Result<Spend, DriverError> {
170        let merkle_tree = self.merkle_tree();
171
172        let puzzle_hash = self.push_through_path_hash();
173        let merkle_proof = merkle_tree
174            .proof(puzzle_hash)
175            .ok_or(DriverError::InvalidMerkleProof)?;
176
177        let puzzle = ctx.alloc(&self.push_through_path())?;
178
179        P2OneOfManyLayer::new(merkle_tree.root()).construct_spend(
180            ctx,
181            P2OneOfManySolution::new(merkle_proof, puzzle, NodePtr::NIL),
182        )
183    }
184
185    pub fn recover_spend<I>(
186        &self,
187        ctx: &mut SpendContext,
188        inner: &I,
189        conditions: Conditions,
190    ) -> Result<Spend, DriverError>
191    where
192        I: SpendWithConditions,
193    {
194        let hint = ctx.hint(self.sender_puzzle_hash)?;
195
196        let inner_spend = inner.spend_with_conditions(
197            ctx,
198            conditions.create_coin(
199                self.sender_puzzle_hash,
200                self.amount,
201                if self.hinted { hint } else { Memos::None },
202            ),
203        )?;
204
205        self.sender_spend(ctx, inner_spend)
206    }
207
208    pub fn recover_coin_spend<I>(
209        &self,
210        ctx: &mut SpendContext,
211        coin: Coin,
212        inner: &I,
213        conditions: Conditions,
214    ) -> Result<(), DriverError>
215    where
216        I: SpendWithConditions,
217    {
218        let spend = self.recover_spend(ctx, inner, conditions)?;
219        ctx.spend(coin, spend)
220    }
221
222    pub fn force_spend<I>(
223        &self,
224        ctx: &mut SpendContext,
225        inner: &I,
226        conditions: Conditions,
227    ) -> Result<Spend, DriverError>
228    where
229        I: SpendWithConditions,
230    {
231        let hint = ctx.hint(self.receiver_puzzle_hash)?;
232
233        let inner_spend = inner.spend_with_conditions(
234            ctx,
235            conditions.create_coin(
236                self.receiver_puzzle_hash,
237                self.amount,
238                if self.hinted { hint } else { Memos::None },
239            ),
240        )?;
241
242        self.sender_spend(ctx, inner_spend)
243    }
244
245    pub fn force_coin_spend<I>(
246        &self,
247        ctx: &mut SpendContext,
248        coin: Coin,
249        inner: &I,
250        conditions: Conditions,
251    ) -> Result<(), DriverError>
252    where
253        I: SpendWithConditions,
254    {
255        let spend = self.force_spend(ctx, inner, conditions)?;
256        ctx.spend(coin, spend)
257    }
258
259    pub fn finish_spend<I>(
260        &self,
261        ctx: &mut SpendContext,
262        inner: &I,
263        conditions: Conditions,
264    ) -> Result<Spend, DriverError>
265    where
266        I: SpendWithConditions,
267    {
268        let hint = ctx.hint(self.receiver_puzzle_hash)?;
269
270        let inner_spend = inner.spend_with_conditions(
271            ctx,
272            conditions.create_coin(
273                self.receiver_puzzle_hash,
274                self.amount,
275                if self.hinted { hint } else { Memos::None },
276            ),
277        )?;
278
279        self.receiver_spend(ctx, inner_spend)
280    }
281
282    pub fn finish_coin_spend<I>(
283        &self,
284        ctx: &mut SpendContext,
285        coin: Coin,
286        inner: &I,
287        conditions: Conditions,
288    ) -> Result<(), DriverError>
289    where
290        I: SpendWithConditions,
291    {
292        let spend = self.finish_spend(ctx, inner, conditions)?;
293        ctx.spend(coin, spend)
294    }
295
296    pub fn push_through_coin_spend(
297        &self,
298        ctx: &mut SpendContext,
299        coin: Coin,
300    ) -> Result<(), DriverError> {
301        let spend = self.push_through_spend(ctx)?;
302        ctx.spend(coin, spend)
303    }
304}
305
306impl ToTreeHash for ClawbackV2 {
307    fn tree_hash(&self) -> TreeHash {
308        self.into_1_of_n().tree_hash()
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use std::slice;
315
316    use chia_protocol::Coin;
317    use chia_sdk_test::{expect_spend, Simulator};
318    use clvm_traits::{clvm_list, ToClvm};
319    use rstest::rstest;
320
321    use crate::{Cat, CatSpend, SpendWithConditions, StandardLayer};
322
323    use super::*;
324
325    #[rstest]
326    fn test_clawback_memo(#[values(false, true)] hinted: bool) -> anyhow::Result<()> {
327        let mut allocator = Allocator::new();
328
329        let clawback =
330            ClawbackV2::new(Bytes32::new([1; 32]), Bytes32::new([2; 32]), 100, 1, hinted);
331        let memo = clawback.memo().to_clvm(&mut allocator)?;
332
333        let roundtrip = ClawbackV2::from_memo(
334            &allocator,
335            memo,
336            Bytes32::new([2; 32]),
337            1,
338            hinted,
339            clawback.tree_hash().into(),
340        );
341        assert_eq!(roundtrip, Some(clawback));
342
343        Ok(())
344    }
345
346    #[rstest]
347    fn test_clawback_v2_recover_xch(
348        #[values(false, true)] hinted: bool,
349        #[values(false, true)] after_expiration: bool,
350    ) -> anyhow::Result<()> {
351        let mut sim = Simulator::new();
352        let mut ctx = SpendContext::new();
353
354        if after_expiration {
355            sim.set_next_timestamp(100)?;
356        }
357
358        let alice = sim.bls(1);
359        let p2_alice = StandardLayer::new(alice.pk);
360
361        let bob = sim.bls(0);
362
363        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
364        let clawback_puzzle_hash = clawback.tree_hash().into();
365        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
366
367        p2_alice.spend(
368            &mut ctx,
369            alice.coin,
370            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
371        )?;
372        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
373
374        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
375
376        clawback.recover_coin_spend(&mut ctx, clawback_coin, &p2_alice, Conditions::new())?;
377
378        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
379
380        if !after_expiration {
381            assert!(sim
382                .coin_state(Coin::new(clawback_coin.coin_id(), alice.puzzle_hash, 1).coin_id())
383                .is_some());
384        }
385
386        Ok(())
387    }
388
389    #[rstest]
390    fn test_clawback_v2_force_xch(
391        #[values(false, true)] hinted: bool,
392        #[values(false, true)] after_expiration: bool,
393    ) -> anyhow::Result<()> {
394        let mut sim = Simulator::new();
395        let mut ctx = SpendContext::new();
396
397        if after_expiration {
398            sim.set_next_timestamp(100)?;
399        }
400
401        let alice = sim.bls(1);
402        let p2_alice = StandardLayer::new(alice.pk);
403
404        let bob = sim.bls(0);
405
406        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
407        let clawback_puzzle_hash = clawback.tree_hash().into();
408        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
409
410        p2_alice.spend(
411            &mut ctx,
412            alice.coin,
413            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
414        )?;
415        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
416
417        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
418
419        clawback.force_coin_spend(&mut ctx, clawback_coin, &p2_alice, Conditions::new())?;
420
421        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
422
423        if !after_expiration {
424            assert!(sim
425                .coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
426                .is_some());
427        }
428
429        Ok(())
430    }
431
432    #[rstest]
433    fn test_clawback_v2_finish_xch(
434        #[values(false, true)] hinted: bool,
435        #[values(false, true)] after_expiration: bool,
436    ) -> anyhow::Result<()> {
437        let mut sim = Simulator::new();
438        let mut ctx = SpendContext::new();
439
440        if after_expiration {
441            sim.set_next_timestamp(100)?;
442        }
443
444        let alice = sim.bls(1);
445        let p2_alice = StandardLayer::new(alice.pk);
446
447        let bob = sim.bls(0);
448        let p2_bob = StandardLayer::new(bob.pk);
449
450        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
451        let clawback_puzzle_hash = clawback.tree_hash().into();
452        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
453
454        p2_alice.spend(
455            &mut ctx,
456            alice.coin,
457            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
458        )?;
459        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
460
461        sim.spend_coins(ctx.take(), &[alice.sk])?;
462
463        clawback.finish_coin_spend(&mut ctx, clawback_coin, &p2_bob, Conditions::new())?;
464
465        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
466
467        if after_expiration {
468            assert!(sim
469                .coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
470                .is_some());
471        }
472
473        Ok(())
474    }
475
476    #[rstest]
477    fn test_clawback_v2_push_through_xch(
478        #[values(false, true)] hinted: bool,
479        #[values(false, true)] after_expiration: bool,
480    ) -> anyhow::Result<()> {
481        let mut sim = Simulator::new();
482        let mut ctx = SpendContext::new();
483
484        if after_expiration {
485            sim.set_next_timestamp(100)?;
486        }
487
488        let alice = sim.bls(1);
489        let p2_alice = StandardLayer::new(alice.pk);
490
491        let bob = sim.bls(0);
492
493        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
494        let clawback_puzzle_hash = clawback.tree_hash().into();
495        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
496
497        p2_alice.spend(
498            &mut ctx,
499            alice.coin,
500            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
501        )?;
502        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
503
504        sim.spend_coins(ctx.take(), &[alice.sk])?;
505
506        clawback.push_through_coin_spend(&mut ctx, clawback_coin)?;
507
508        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
509
510        if after_expiration {
511            assert!(sim
512                .coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
513                .is_some());
514        }
515
516        Ok(())
517    }
518
519    #[rstest]
520    fn test_clawback_v2_recover_cat(
521        #[values(false, true)] after_expiration: bool,
522    ) -> anyhow::Result<()> {
523        let mut sim = Simulator::new();
524        let mut ctx = SpendContext::new();
525
526        if after_expiration {
527            sim.set_next_timestamp(100)?;
528        }
529
530        let alice = sim.bls(1);
531        let p2_alice = StandardLayer::new(alice.pk);
532
533        let bob = sim.bls(0);
534
535        let memos = ctx.hint(alice.puzzle_hash)?;
536        let (issue_cat, cats) = Cat::issue_with_coin(
537            &mut ctx,
538            alice.coin.coin_id(),
539            1,
540            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
541        )?;
542        let cat = cats[0];
543        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
544
545        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
546        let clawback_puzzle_hash = clawback.tree_hash().into();
547        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
548
549        let inner_spend = p2_alice.spend_with_conditions(
550            &mut ctx,
551            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
552        )?;
553        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
554
555        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
556
557        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
558
559        let clawback_spend = clawback.recover_spend(&mut ctx, &p2_alice, Conditions::new())?;
560        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
561
562        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
563
564        if !after_expiration {
565            assert!(sim
566                .coin_state(clawback_cat.child(alice.puzzle_hash, 1).coin.coin_id())
567                .is_some());
568        }
569
570        Ok(())
571    }
572
573    #[rstest]
574    fn test_clawback_v2_force_cat(
575        #[values(false, true)] after_expiration: bool,
576    ) -> anyhow::Result<()> {
577        let mut sim = Simulator::new();
578        let mut ctx = SpendContext::new();
579
580        if after_expiration {
581            sim.set_next_timestamp(100)?;
582        }
583
584        let alice = sim.bls(1);
585        let p2_alice = StandardLayer::new(alice.pk);
586
587        let bob = sim.bls(0);
588
589        let memos = ctx.hint(alice.puzzle_hash)?;
590        let (issue_cat, cats) = Cat::issue_with_coin(
591            &mut ctx,
592            alice.coin.coin_id(),
593            1,
594            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
595        )?;
596        let cat = cats[0];
597        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
598
599        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
600        let clawback_puzzle_hash = clawback.tree_hash().into();
601        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
602
603        let inner_spend = p2_alice.spend_with_conditions(
604            &mut ctx,
605            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
606        )?;
607        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
608
609        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
610
611        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
612
613        let clawback_spend = clawback.force_spend(&mut ctx, &p2_alice, Conditions::new())?;
614        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
615
616        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
617
618        if !after_expiration {
619            assert!(sim
620                .coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
621                .is_some());
622        }
623
624        Ok(())
625    }
626
627    #[rstest]
628    fn test_clawback_v2_finish_cat(
629        #[values(false, true)] after_expiration: bool,
630    ) -> anyhow::Result<()> {
631        let mut sim = Simulator::new();
632        let mut ctx = SpendContext::new();
633
634        if after_expiration {
635            sim.set_next_timestamp(100)?;
636        }
637
638        let alice = sim.bls(1);
639        let p2_alice = StandardLayer::new(alice.pk);
640
641        let bob = sim.bls(0);
642        let p2_bob = StandardLayer::new(bob.pk);
643
644        let memos = ctx.hint(alice.puzzle_hash)?;
645        let (issue_cat, cats) = Cat::issue_with_coin(
646            &mut ctx,
647            alice.coin.coin_id(),
648            1,
649            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
650        )?;
651        let cat = cats[0];
652        p2_alice.spend(
653            &mut ctx,
654            alice.coin,
655            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
656        )?;
657
658        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
659        let clawback_puzzle_hash = clawback.tree_hash().into();
660        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
661
662        let inner_spend = p2_alice.spend_with_conditions(
663            &mut ctx,
664            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
665        )?;
666        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
667
668        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
669
670        sim.spend_coins(ctx.take(), &[alice.sk])?;
671
672        let clawback_spend = clawback.finish_spend(&mut ctx, &p2_bob, Conditions::new())?;
673        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
674
675        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
676
677        if after_expiration {
678            assert!(sim
679                .coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
680                .is_some());
681        }
682
683        Ok(())
684    }
685
686    #[rstest]
687    fn test_clawback_v2_push_through_cat(
688        #[values(false, true)] after_expiration: bool,
689    ) -> anyhow::Result<()> {
690        let mut sim = Simulator::new();
691        let mut ctx = SpendContext::new();
692
693        if after_expiration {
694            sim.set_next_timestamp(100)?;
695        }
696
697        let alice = sim.bls(1);
698        let p2_alice = StandardLayer::new(alice.pk);
699
700        let bob = sim.bls(0);
701
702        let memos = ctx.hint(alice.puzzle_hash)?;
703        let (issue_cat, cats) = Cat::issue_with_coin(
704            &mut ctx,
705            alice.coin.coin_id(),
706            1,
707            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
708        )?;
709        let cat = cats[0];
710        p2_alice.spend(
711            &mut ctx,
712            alice.coin,
713            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
714        )?;
715
716        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
717        let clawback_puzzle_hash = clawback.tree_hash().into();
718        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
719
720        let inner_spend = p2_alice.spend_with_conditions(
721            &mut ctx,
722            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
723        )?;
724        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
725
726        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
727
728        sim.spend_coins(ctx.take(), &[alice.sk])?;
729
730        let clawback_spend = clawback.push_through_spend(&mut ctx)?;
731        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
732
733        expect_spend(sim.spend_coins(ctx.take(), &[]), after_expiration);
734
735        if after_expiration {
736            assert!(sim
737                .coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
738                .is_some());
739        }
740
741        Ok(())
742    }
743}