Skip to main content

chia_sdk_driver/primitives/
clawback_v2.rs

1use chia_protocol::{Bytes32, Coin};
2use chia_sdk_types::{
3    Conditions, MerkleTree, Mod,
4    conditions::{AssertBeforeSecondsAbsolute, AssertSecondsAbsolute, CreateCoin, Memos},
5    puzzles::{AugmentedConditionArgs, AugmentedConditionSolution, P2OneOfManySolution},
6};
7use clvm_traits::{FromClvm, clvm_list, clvm_quote, match_list};
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, Hash)]
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::{Simulator, expect_spend};
318    use clvm_traits::{ToClvm, clvm_list};
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!(
382                sim.coin_state(Coin::new(clawback_coin.coin_id(), alice.puzzle_hash, 1).coin_id())
383                    .is_some()
384            );
385        }
386
387        Ok(())
388    }
389
390    #[rstest]
391    fn test_clawback_v2_force_xch(
392        #[values(false, true)] hinted: bool,
393        #[values(false, true)] after_expiration: bool,
394    ) -> anyhow::Result<()> {
395        let mut sim = Simulator::new();
396        let mut ctx = SpendContext::new();
397
398        if after_expiration {
399            sim.set_next_timestamp(100)?;
400        }
401
402        let alice = sim.bls(1);
403        let p2_alice = StandardLayer::new(alice.pk);
404
405        let bob = sim.bls(0);
406
407        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
408        let clawback_puzzle_hash = clawback.tree_hash().into();
409        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
410
411        p2_alice.spend(
412            &mut ctx,
413            alice.coin,
414            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
415        )?;
416        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
417
418        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
419
420        clawback.force_coin_spend(&mut ctx, clawback_coin, &p2_alice, Conditions::new())?;
421
422        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
423
424        if !after_expiration {
425            assert!(
426                sim.coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
427                    .is_some()
428            );
429        }
430
431        Ok(())
432    }
433
434    #[rstest]
435    fn test_clawback_v2_finish_xch(
436        #[values(false, true)] hinted: bool,
437        #[values(false, true)] after_expiration: bool,
438    ) -> anyhow::Result<()> {
439        let mut sim = Simulator::new();
440        let mut ctx = SpendContext::new();
441
442        if after_expiration {
443            sim.set_next_timestamp(100)?;
444        }
445
446        let alice = sim.bls(1);
447        let p2_alice = StandardLayer::new(alice.pk);
448
449        let bob = sim.bls(0);
450        let p2_bob = StandardLayer::new(bob.pk);
451
452        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
453        let clawback_puzzle_hash = clawback.tree_hash().into();
454        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
455
456        p2_alice.spend(
457            &mut ctx,
458            alice.coin,
459            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
460        )?;
461        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
462
463        sim.spend_coins(ctx.take(), &[alice.sk])?;
464
465        clawback.finish_coin_spend(&mut ctx, clawback_coin, &p2_bob, Conditions::new())?;
466
467        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
468
469        if after_expiration {
470            assert!(
471                sim.coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
472                    .is_some()
473            );
474        }
475
476        Ok(())
477    }
478
479    #[rstest]
480    fn test_clawback_v2_push_through_xch(
481        #[values(false, true)] hinted: bool,
482        #[values(false, true)] after_expiration: bool,
483    ) -> anyhow::Result<()> {
484        let mut sim = Simulator::new();
485        let mut ctx = SpendContext::new();
486
487        if after_expiration {
488            sim.set_next_timestamp(100)?;
489        }
490
491        let alice = sim.bls(1);
492        let p2_alice = StandardLayer::new(alice.pk);
493
494        let bob = sim.bls(0);
495
496        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, hinted);
497        let clawback_puzzle_hash = clawback.tree_hash().into();
498        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
499
500        p2_alice.spend(
501            &mut ctx,
502            alice.coin,
503            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
504        )?;
505        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
506
507        sim.spend_coins(ctx.take(), &[alice.sk])?;
508
509        clawback.push_through_coin_spend(&mut ctx, clawback_coin)?;
510
511        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
512
513        if after_expiration {
514            assert!(
515                sim.coin_state(Coin::new(clawback_coin.coin_id(), bob.puzzle_hash, 1).coin_id())
516                    .is_some()
517            );
518        }
519
520        Ok(())
521    }
522
523    #[rstest]
524    fn test_clawback_v2_recover_cat(
525        #[values(false, true)] after_expiration: bool,
526    ) -> anyhow::Result<()> {
527        let mut sim = Simulator::new();
528        let mut ctx = SpendContext::new();
529
530        if after_expiration {
531            sim.set_next_timestamp(100)?;
532        }
533
534        let alice = sim.bls(1);
535        let p2_alice = StandardLayer::new(alice.pk);
536
537        let bob = sim.bls(0);
538
539        let memos = ctx.hint(alice.puzzle_hash)?;
540        let (issue_cat, cats) = Cat::single_issuance(
541            &mut ctx,
542            alice.coin.coin_id(),
543            None,
544            1,
545            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
546        )?;
547        let cat = cats[0];
548        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
549
550        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
551        let clawback_puzzle_hash = clawback.tree_hash().into();
552        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
553
554        let inner_spend = p2_alice.spend_with_conditions(
555            &mut ctx,
556            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
557        )?;
558        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
559
560        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
561
562        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
563
564        let clawback_spend = clawback.recover_spend(&mut ctx, &p2_alice, Conditions::new())?;
565        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
566
567        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
568
569        if !after_expiration {
570            assert!(
571                sim.coin_state(clawback_cat.child(alice.puzzle_hash, 1).coin.coin_id())
572                    .is_some()
573            );
574        }
575
576        Ok(())
577    }
578
579    #[rstest]
580    fn test_clawback_v2_force_cat(
581        #[values(false, true)] after_expiration: bool,
582    ) -> anyhow::Result<()> {
583        let mut sim = Simulator::new();
584        let mut ctx = SpendContext::new();
585
586        if after_expiration {
587            sim.set_next_timestamp(100)?;
588        }
589
590        let alice = sim.bls(1);
591        let p2_alice = StandardLayer::new(alice.pk);
592
593        let bob = sim.bls(0);
594
595        let memos = ctx.hint(alice.puzzle_hash)?;
596        let (issue_cat, cats) = Cat::single_issuance(
597            &mut ctx,
598            alice.coin.coin_id(),
599            None,
600            1,
601            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
602        )?;
603        let cat = cats[0];
604        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
605
606        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
607        let clawback_puzzle_hash = clawback.tree_hash().into();
608        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
609
610        let inner_spend = p2_alice.spend_with_conditions(
611            &mut ctx,
612            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
613        )?;
614        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
615
616        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
617
618        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
619
620        let clawback_spend = clawback.force_spend(&mut ctx, &p2_alice, Conditions::new())?;
621        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
622
623        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
624
625        if !after_expiration {
626            assert!(
627                sim.coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
628                    .is_some()
629            );
630        }
631
632        Ok(())
633    }
634
635    #[rstest]
636    fn test_clawback_v2_finish_cat(
637        #[values(false, true)] after_expiration: bool,
638    ) -> anyhow::Result<()> {
639        let mut sim = Simulator::new();
640        let mut ctx = SpendContext::new();
641
642        if after_expiration {
643            sim.set_next_timestamp(100)?;
644        }
645
646        let alice = sim.bls(1);
647        let p2_alice = StandardLayer::new(alice.pk);
648
649        let bob = sim.bls(0);
650        let p2_bob = StandardLayer::new(bob.pk);
651
652        let memos = ctx.hint(alice.puzzle_hash)?;
653        let (issue_cat, cats) = Cat::single_issuance(
654            &mut ctx,
655            alice.coin.coin_id(),
656            None,
657            1,
658            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
659        )?;
660        let cat = cats[0];
661        p2_alice.spend(
662            &mut ctx,
663            alice.coin,
664            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
665        )?;
666
667        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
668        let clawback_puzzle_hash = clawback.tree_hash().into();
669        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
670
671        let inner_spend = p2_alice.spend_with_conditions(
672            &mut ctx,
673            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
674        )?;
675        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
676
677        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
678
679        sim.spend_coins(ctx.take(), &[alice.sk])?;
680
681        let clawback_spend = clawback.finish_spend(&mut ctx, &p2_bob, Conditions::new())?;
682        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
683
684        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
685
686        if after_expiration {
687            assert!(
688                sim.coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
689                    .is_some()
690            );
691        }
692
693        Ok(())
694    }
695
696    #[rstest]
697    fn test_clawback_v2_push_through_cat(
698        #[values(false, true)] after_expiration: bool,
699    ) -> anyhow::Result<()> {
700        let mut sim = Simulator::new();
701        let mut ctx = SpendContext::new();
702
703        if after_expiration {
704            sim.set_next_timestamp(100)?;
705        }
706
707        let alice = sim.bls(1);
708        let p2_alice = StandardLayer::new(alice.pk);
709
710        let bob = sim.bls(0);
711
712        let memos = ctx.hint(alice.puzzle_hash)?;
713        let (issue_cat, cats) = Cat::single_issuance(
714            &mut ctx,
715            alice.coin.coin_id(),
716            None,
717            1,
718            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
719        )?;
720        let cat = cats[0];
721        p2_alice.spend(
722            &mut ctx,
723            alice.coin,
724            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
725        )?;
726
727        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
728        let clawback_puzzle_hash = clawback.tree_hash().into();
729        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
730
731        let inner_spend = p2_alice.spend_with_conditions(
732            &mut ctx,
733            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
734        )?;
735        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
736
737        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
738
739        sim.spend_coins(ctx.take(), &[alice.sk])?;
740
741        let clawback_spend = clawback.push_through_spend(&mut ctx)?;
742        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
743
744        expect_spend(sim.spend_coins(ctx.take(), &[]), after_expiration);
745
746        if after_expiration {
747            assert!(
748                sim.coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
749                    .is_some()
750            );
751        }
752
753        Ok(())
754    }
755}