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)]
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::issue_with_coin(
541            &mut ctx,
542            alice.coin.coin_id(),
543            1,
544            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
545        )?;
546        let cat = cats[0];
547        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
548
549        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
550        let clawback_puzzle_hash = clawback.tree_hash().into();
551        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
552
553        let inner_spend = p2_alice.spend_with_conditions(
554            &mut ctx,
555            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
556        )?;
557        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
558
559        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
560
561        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
562
563        let clawback_spend = clawback.recover_spend(&mut ctx, &p2_alice, Conditions::new())?;
564        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
565
566        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
567
568        if !after_expiration {
569            assert!(
570                sim.coin_state(clawback_cat.child(alice.puzzle_hash, 1).coin.coin_id())
571                    .is_some()
572            );
573        }
574
575        Ok(())
576    }
577
578    #[rstest]
579    fn test_clawback_v2_force_cat(
580        #[values(false, true)] after_expiration: bool,
581    ) -> anyhow::Result<()> {
582        let mut sim = Simulator::new();
583        let mut ctx = SpendContext::new();
584
585        if after_expiration {
586            sim.set_next_timestamp(100)?;
587        }
588
589        let alice = sim.bls(1);
590        let p2_alice = StandardLayer::new(alice.pk);
591
592        let bob = sim.bls(0);
593
594        let memos = ctx.hint(alice.puzzle_hash)?;
595        let (issue_cat, cats) = Cat::issue_with_coin(
596            &mut ctx,
597            alice.coin.coin_id(),
598            1,
599            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
600        )?;
601        let cat = cats[0];
602        p2_alice.spend(&mut ctx, alice.coin, issue_cat)?;
603
604        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
605        let clawback_puzzle_hash = clawback.tree_hash().into();
606        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
607
608        let inner_spend = p2_alice.spend_with_conditions(
609            &mut ctx,
610            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
611        )?;
612        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
613
614        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
615
616        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
617
618        let clawback_spend = clawback.force_spend(&mut ctx, &p2_alice, Conditions::new())?;
619        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
620
621        expect_spend(sim.spend_coins(ctx.take(), &[alice.sk]), !after_expiration);
622
623        if !after_expiration {
624            assert!(
625                sim.coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
626                    .is_some()
627            );
628        }
629
630        Ok(())
631    }
632
633    #[rstest]
634    fn test_clawback_v2_finish_cat(
635        #[values(false, true)] after_expiration: bool,
636    ) -> anyhow::Result<()> {
637        let mut sim = Simulator::new();
638        let mut ctx = SpendContext::new();
639
640        if after_expiration {
641            sim.set_next_timestamp(100)?;
642        }
643
644        let alice = sim.bls(1);
645        let p2_alice = StandardLayer::new(alice.pk);
646
647        let bob = sim.bls(0);
648        let p2_bob = StandardLayer::new(bob.pk);
649
650        let memos = ctx.hint(alice.puzzle_hash)?;
651        let (issue_cat, cats) = Cat::issue_with_coin(
652            &mut ctx,
653            alice.coin.coin_id(),
654            1,
655            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
656        )?;
657        let cat = cats[0];
658        p2_alice.spend(
659            &mut ctx,
660            alice.coin,
661            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
662        )?;
663
664        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
665        let clawback_puzzle_hash = clawback.tree_hash().into();
666        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
667
668        let inner_spend = p2_alice.spend_with_conditions(
669            &mut ctx,
670            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
671        )?;
672        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
673
674        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
675
676        sim.spend_coins(ctx.take(), &[alice.sk])?;
677
678        let clawback_spend = clawback.finish_spend(&mut ctx, &p2_bob, Conditions::new())?;
679        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
680
681        expect_spend(sim.spend_coins(ctx.take(), &[bob.sk]), after_expiration);
682
683        if after_expiration {
684            assert!(
685                sim.coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
686                    .is_some()
687            );
688        }
689
690        Ok(())
691    }
692
693    #[rstest]
694    fn test_clawback_v2_push_through_cat(
695        #[values(false, true)] after_expiration: bool,
696    ) -> anyhow::Result<()> {
697        let mut sim = Simulator::new();
698        let mut ctx = SpendContext::new();
699
700        if after_expiration {
701            sim.set_next_timestamp(100)?;
702        }
703
704        let alice = sim.bls(1);
705        let p2_alice = StandardLayer::new(alice.pk);
706
707        let bob = sim.bls(0);
708
709        let memos = ctx.hint(alice.puzzle_hash)?;
710        let (issue_cat, cats) = Cat::issue_with_coin(
711            &mut ctx,
712            alice.coin.coin_id(),
713            1,
714            Conditions::new().create_coin(alice.puzzle_hash, 1, memos),
715        )?;
716        let cat = cats[0];
717        p2_alice.spend(
718            &mut ctx,
719            alice.coin,
720            issue_cat.create_coin(alice.puzzle_hash, 0, Memos::None),
721        )?;
722
723        let clawback = ClawbackV2::new(alice.puzzle_hash, bob.puzzle_hash, 100, 1, true);
724        let clawback_puzzle_hash = clawback.tree_hash().into();
725        let memos = ctx.memos(&clvm_list!(bob.puzzle_hash, clawback.memo()))?;
726
727        let inner_spend = p2_alice.spend_with_conditions(
728            &mut ctx,
729            Conditions::new().create_coin(clawback_puzzle_hash, 1, memos),
730        )?;
731        Cat::spend_all(&mut ctx, &[CatSpend::new(cat, inner_spend)])?;
732
733        let clawback_cat = cat.child(clawback_puzzle_hash, 1);
734
735        sim.spend_coins(ctx.take(), &[alice.sk])?;
736
737        let clawback_spend = clawback.push_through_spend(&mut ctx)?;
738        Cat::spend_all(&mut ctx, &[CatSpend::new(clawback_cat, clawback_spend)])?;
739
740        expect_spend(sim.spend_coins(ctx.take(), &[]), after_expiration);
741
742        if after_expiration {
743            assert!(
744                sim.coin_state(clawback_cat.child(bob.puzzle_hash, 1).coin.coin_id())
745                    .is_some()
746            );
747        }
748
749        Ok(())
750    }
751}