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 chia_protocol::Coin;
315    use chia_sdk_test::{expect_spend, Simulator};
316    use clvm_traits::{clvm_list, ToClvm};
317    use rstest::rstest;
318
319    use crate::{Cat, CatSpend, SpendWithConditions, StandardLayer};
320
321    use super::*;
322
323    #[rstest]
324    fn test_clawback_memo(#[values(false, true)] hinted: bool) -> anyhow::Result<()> {
325        let mut allocator = Allocator::new();
326
327        let clawback =
328            ClawbackV2::new(Bytes32::new([1; 32]), Bytes32::new([2; 32]), 100, 1, hinted);
329        let memo = clawback.memo().to_clvm(&mut allocator)?;
330
331        let roundtrip = ClawbackV2::from_memo(
332            &allocator,
333            memo,
334            Bytes32::new([2; 32]),
335            1,
336            hinted,
337            clawback.tree_hash().into(),
338        );
339        assert_eq!(roundtrip, Some(clawback));
340
341        Ok(())
342    }
343
344    #[rstest]
345    fn test_clawback_v2_recover_xch(
346        #[values(false, true)] hinted: bool,
347        #[values(false, true)] after_expiration: bool,
348    ) -> anyhow::Result<()> {
349        let mut sim = Simulator::new();
350        let mut ctx = SpendContext::new();
351
352        if after_expiration {
353            sim.set_next_timestamp(100)?;
354        }
355
356        let alice = sim.bls(1);
357        let p2_alice = StandardLayer::new(alice.pk);
358
359        let bob = sim.bls(0);
360
361        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
362        let clawback_puzzle_hash = clawback.tree_hash().into();
363        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
364
365        p2_alice.spend(
366            &mut ctx,
367            alice.coin,
368            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
369        )?;
370        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
371
372        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
373
374        clawback.recover_coin_spend(&mut ctx, clawback_coin, &p2_alice, Conditions::new())?;
375
376        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
377
378        if !after_expiration {
379            assert!(sim
380                .coin_state(Coin::new(clawback_coin.coin_id(), alice.puzzle_hash, 1).coin_id())
381                .is_some());
382        }
383
384        Ok(())
385    }
386
387    #[rstest]
388    fn test_clawback_v2_force_xch(
389        #[values(false, true)] hinted: bool,
390        #[values(false, true)] after_expiration: bool,
391    ) -> anyhow::Result<()> {
392        let mut sim = Simulator::new();
393        let mut ctx = SpendContext::new();
394
395        if after_expiration {
396            sim.set_next_timestamp(100)?;
397        }
398
399        let alice = sim.bls(1);
400        let p2_alice = StandardLayer::new(alice.pk);
401
402        let bob = sim.bls(0);
403
404        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
405        let clawback_puzzle_hash = clawback.tree_hash().into();
406        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
407
408        p2_alice.spend(
409            &mut ctx,
410            alice.coin,
411            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
412        )?;
413        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
414
415        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
416
417        clawback.force_coin_spend(&mut ctx, clawback_coin, &p2_alice, Conditions::new())?;
418
419        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
420
421        if !after_expiration {
422            assert!(sim
423                .coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
424                .is_some());
425        }
426
427        Ok(())
428    }
429
430    #[rstest]
431    fn test_clawback_v2_finish_xch(
432        #[values(false, true)] hinted: bool,
433        #[values(false, true)] after_expiration: bool,
434    ) -> anyhow::Result<()> {
435        let mut sim = Simulator::new();
436        let mut ctx = SpendContext::new();
437
438        if after_expiration {
439            sim.set_next_timestamp(100)?;
440        }
441
442        let alice = sim.bls(1);
443        let p2_alice = StandardLayer::new(alice.pk);
444
445        let bob = sim.bls(0);
446        let p2_bob = StandardLayer::new(bob.pk);
447
448        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
449        let clawback_puzzle_hash = clawback.tree_hash().into();
450        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
451
452        p2_alice.spend(
453            &mut ctx,
454            alice.coin,
455            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
456        )?;
457        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
458
459        sim.spend_coins(ctx.take(), &[alice.sk])?;
460
461        clawback.finish_coin_spend(&mut ctx, clawback_coin, &p2_bob, Conditions::new())?;
462
463        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
464
465        if after_expiration {
466            assert!(sim
467                .coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
468                .is_some());
469        }
470
471        Ok(())
472    }
473
474    #[rstest]
475    fn test_clawback_v2_push_through_xch(
476        #[values(false, true)] hinted: bool,
477        #[values(false, true)] after_expiration: bool,
478    ) -> anyhow::Result<()> {
479        let mut sim = Simulator::new();
480        let mut ctx = SpendContext::new();
481
482        if after_expiration {
483            sim.set_next_timestamp(100)?;
484        }
485
486        let alice = sim.bls(1);
487        let p2_alice = StandardLayer::new(alice.pk);
488
489        let bob = sim.bls(0);
490
491        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
492        let clawback_puzzle_hash = clawback.tree_hash().into();
493        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
494
495        p2_alice.spend(
496            &mut ctx,
497            alice.coin,
498            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
499        )?;
500        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
501
502        sim.spend_coins(ctx.take(), &[alice.sk])?;
503
504        clawback.push_through_coin_spend(&mut ctx, clawback_coin)?;
505
506        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
507
508        if after_expiration {
509            assert!(sim
510                .coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
511                .is_some());
512        }
513
514        Ok(())
515    }
516
517    #[rstest]
518    fn test_clawback_v2_recover_cat(
519        #[values(false, true)] after_expiration: bool,
520    ) -> anyhow::Result<()> {
521        let mut sim = Simulator::new();
522        let mut ctx = SpendContext::new();
523
524        if after_expiration {
525            sim.set_next_timestamp(100)?;
526        }
527
528        let alice = sim.bls(1);
529        let p2_alice = StandardLayer::new(alice.pk);
530
531        let bob = sim.bls(0);
532
533        let memos = ctx.hint(alice.puzzle_hash)?;
534        let (issue_cat, cats) = Cat::issue_with_coin(
535            &mut ctx,
536            alice.coin.coin_id(),
537            1,
538            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
539        )?;
540        let cat = cats[0];
541        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
542
543        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
544        let clawback_puzzle_hash = clawback.tree_hash().into();
545        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
546
547        let inner_spend = p2_alice.spend_with_conditions(
548            &mut ctx,
549            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
550        )?;
551        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
552
553        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
554
555        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
556
557        let clawback_spend = clawback.recover_spend(&mut ctx, &p2_alice, Conditions::new())?;
558        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
559
560        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
561
562        if !after_expiration {
563            assert!(sim
564                .coin_state(clawback_cat.child(alice.puzzle_hash, 1).coin.coin_id())
565                .is_some());
566        }
567
568        Ok(())
569    }
570
571    #[rstest]
572    fn test_clawback_v2_force_cat(
573        #[values(false, true)] after_expiration: bool,
574    ) -> anyhow::Result<()> {
575        let mut sim = Simulator::new();
576        let mut ctx = SpendContext::new();
577
578        if after_expiration {
579            sim.set_next_timestamp(100)?;
580        }
581
582        let alice = sim.bls(1);
583        let p2_alice = StandardLayer::new(alice.pk);
584
585        let bob = sim.bls(0);
586
587        let memos = ctx.hint(alice.puzzle_hash)?;
588        let (issue_cat, cats) = Cat::issue_with_coin(
589            &mut ctx,
590            alice.coin.coin_id(),
591            1,
592            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
593        )?;
594        let cat = cats[0];
595        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
596
597        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
598        let clawback_puzzle_hash = clawback.tree_hash().into();
599        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
600
601        let inner_spend = p2_alice.spend_with_conditions(
602            &mut ctx,
603            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
604        )?;
605        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
606
607        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
608
609        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
610
611        let clawback_spend = clawback.force_spend(&mut ctx, &p2_alice, Conditions::new())?;
612        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
613
614        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
615
616        if !after_expiration {
617            assert!(sim
618                .coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
619                .is_some());
620        }
621
622        Ok(())
623    }
624
625    #[rstest]
626    fn test_clawback_v2_finish_cat(
627        #[values(false, true)] after_expiration: bool,
628    ) -> anyhow::Result<()> {
629        let mut sim = Simulator::new();
630        let mut ctx = SpendContext::new();
631
632        if after_expiration {
633            sim.set_next_timestamp(100)?;
634        }
635
636        let alice = sim.bls(1);
637        let p2_alice = StandardLayer::new(alice.pk);
638
639        let bob = sim.bls(0);
640        let p2_bob = StandardLayer::new(bob.pk);
641
642        let memos = ctx.hint(alice.puzzle_hash)?;
643        let (issue_cat, cats) = Cat::issue_with_coin(
644            &mut ctx,
645            alice.coin.coin_id(),
646            1,
647            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
648        )?;
649        let cat = cats[0];
650        p2_alice.spend(
651            &mut ctx,
652            alice.coin,
653            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
654        )?;
655
656        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
657        let clawback_puzzle_hash = clawback.tree_hash().into();
658        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
659
660        let inner_spend = p2_alice.spend_with_conditions(
661            &mut ctx,
662            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
663        )?;
664        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
665
666        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
667
668        sim.spend_coins(ctx.take(), &[alice.sk])?;
669
670        let clawback_spend = clawback.finish_spend(&mut ctx, &p2_bob, Conditions::new())?;
671        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
672
673        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
674
675        if after_expiration {
676            assert!(sim
677                .coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
678                .is_some());
679        }
680
681        Ok(())
682    }
683
684    #[rstest]
685    fn test_clawback_v2_push_through_cat(
686        #[values(false, true)] after_expiration: bool,
687    ) -> anyhow::Result<()> {
688        let mut sim = Simulator::new();
689        let mut ctx = SpendContext::new();
690
691        if after_expiration {
692            sim.set_next_timestamp(100)?;
693        }
694
695        let alice = sim.bls(1);
696        let p2_alice = StandardLayer::new(alice.pk);
697
698        let bob = sim.bls(0);
699
700        let memos = ctx.hint(alice.puzzle_hash)?;
701        let (issue_cat, cats) = Cat::issue_with_coin(
702            &mut ctx,
703            alice.coin.coin_id(),
704            1,
705            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
706        )?;
707        let cat = cats[0];
708        p2_alice.spend(
709            &mut ctx,
710            alice.coin,
711            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
712        )?;
713
714        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
715        let clawback_puzzle_hash = clawback.tree_hash().into();
716        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
717
718        let inner_spend = p2_alice.spend_with_conditions(
719            &mut ctx,
720            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
721        )?;
722        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
723
724        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
725
726        sim.spend_coins(ctx.take(), &[alice.sk])?;
727
728        let clawback_spend = clawback.push_through_spend(&mut ctx)?;
729        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
730
731        expect_spend(sim.spend_coins(ctx.take(), &[]), after_expiration);
732
733        if after_expiration {
734            assert!(sim
735                .coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
736                .is_some());
737        }
738
739        Ok(())
740    }
741}