chia_sdk_driver/primitives/option/
option_contract.rs

1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::{
3    singleton::{LauncherSolution, SingletonArgs, SingletonSolution},
4    LineageProof, Proof,
5};
6use chia_sdk_types::{
7    puzzles::{OptionContractArgs, OptionContractSolution},
8    run_puzzle, Condition, Conditions, Mod,
9};
10use clvm_traits::FromClvm;
11use clvm_utils::{ToTreeHash, TreeHash};
12use clvmr::{Allocator, NodePtr};
13
14use crate::{
15    DriverError, Layer, Puzzle, Singleton, SingletonInfo, Spend, SpendContext, SpendWithConditions,
16};
17
18use super::{OptionContractLayers, OptionInfo, OptionMetadata};
19
20pub type OptionContract = Singleton<OptionInfo>;
21
22impl OptionContract {
23    pub fn parse_child(
24        allocator: &mut Allocator,
25        parent_coin: Coin,
26        parent_puzzle: Puzzle,
27        parent_solution: NodePtr,
28    ) -> Result<Option<Self>, DriverError> {
29        let Some(singleton) =
30            OptionContractLayers::<Puzzle>::parse_puzzle(allocator, parent_puzzle)?
31        else {
32            return Ok(None);
33        };
34
35        let solution = OptionContractLayers::<Puzzle>::parse_solution(allocator, parent_solution)?;
36        let output = run_puzzle(
37            allocator,
38            singleton.inner_puzzle.inner_puzzle.ptr(),
39            solution.inner_solution.inner_solution,
40        )?;
41        let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
42
43        let Some(create_coin) = conditions
44            .into_iter()
45            .filter_map(Condition::into_create_coin)
46            .find(|cond| cond.amount % 2 == 1)
47        else {
48            return Err(DriverError::MissingChild);
49        };
50
51        let puzzle_hash = SingletonArgs::curry_tree_hash(
52            singleton.launcher_id,
53            OptionContractArgs::new(
54                singleton.inner_puzzle.underlying_coin_id,
55                singleton.inner_puzzle.underlying_delegated_puzzle_hash,
56                TreeHash::from(create_coin.puzzle_hash),
57            )
58            .curry_tree_hash(),
59        );
60
61        let option = Self {
62            coin: Coin::new(
63                parent_coin.coin_id(),
64                puzzle_hash.into(),
65                create_coin.amount,
66            ),
67            proof: Proof::Lineage(LineageProof {
68                parent_parent_coin_info: parent_coin.parent_coin_info,
69                parent_inner_puzzle_hash: singleton.inner_puzzle.tree_hash().into(),
70                parent_amount: parent_coin.amount,
71            }),
72            info: OptionInfo {
73                launcher_id: singleton.launcher_id,
74                underlying_coin_id: singleton.inner_puzzle.underlying_coin_id,
75                underlying_delegated_puzzle_hash: singleton
76                    .inner_puzzle
77                    .underlying_delegated_puzzle_hash,
78                p2_puzzle_hash: create_coin.puzzle_hash,
79            },
80        };
81
82        Ok(Some(option))
83    }
84
85    /// Parses an [`OptionContract`] and its p2 spend from a coin spend.
86    ///
87    /// If the puzzle is not an option contract, this will return [`None`] instead of an error.
88    /// However, if the puzzle should have been an option contract but had a parsing error, this will return an error.
89    pub fn parse(
90        allocator: &Allocator,
91        coin: Coin,
92        puzzle: Puzzle,
93        solution: NodePtr,
94    ) -> Result<Option<(Self, Puzzle, NodePtr)>, DriverError> {
95        let Some((option_info, p2_puzzle)) = OptionInfo::parse(allocator, puzzle)? else {
96            return Ok(None);
97        };
98
99        let solution = OptionContractLayers::<Puzzle>::parse_solution(allocator, solution)?;
100
101        let p2_solution = solution.inner_solution.inner_solution;
102
103        Ok(Some((
104            Self::new(coin, solution.lineage_proof, option_info),
105            p2_puzzle,
106            p2_solution,
107        )))
108    }
109
110    pub fn parse_metadata(
111        allocator: &mut Allocator,
112        launcher_solution: NodePtr,
113    ) -> Result<OptionMetadata, DriverError> {
114        let solution = LauncherSolution::<OptionMetadata>::from_clvm(allocator, launcher_solution)?;
115        Ok(solution.key_value_list)
116    }
117
118    pub fn spend(
119        &self,
120        ctx: &mut SpendContext,
121        inner_spend: Spend,
122    ) -> Result<Option<Self>, DriverError> {
123        let layers = self.info.into_layers(inner_spend.puzzle);
124
125        let spend = layers.construct_spend(
126            ctx,
127            SingletonSolution {
128                lineage_proof: self.proof,
129                amount: self.coin.amount,
130                inner_solution: OptionContractSolution::new(inner_spend.solution),
131            },
132        )?;
133
134        ctx.spend(self.coin, spend)?;
135
136        let output = ctx.run(inner_spend.puzzle, inner_spend.solution)?;
137        let conditions = Vec::<Condition>::from_clvm(ctx, output)?;
138
139        for condition in conditions {
140            if let Some(create_coin) = condition.into_create_coin() {
141                if create_coin.amount % 2 == 1 {
142                    return Ok(Some(
143                        self.child(create_coin.puzzle_hash, create_coin.amount),
144                    ));
145                }
146            }
147        }
148
149        Ok(None)
150    }
151
152    pub fn spend_with<I>(
153        &self,
154        ctx: &mut SpendContext,
155        inner: &I,
156        conditions: Conditions,
157    ) -> Result<Option<Self>, DriverError>
158    where
159        I: SpendWithConditions,
160    {
161        let inner_spend = inner.spend_with_conditions(ctx, conditions)?;
162        self.spend(ctx, inner_spend)
163    }
164
165    pub fn transfer<I>(
166        self,
167        ctx: &mut SpendContext,
168        inner: &I,
169        p2_puzzle_hash: Bytes32,
170        extra_conditions: Conditions,
171    ) -> Result<Self, DriverError>
172    where
173        I: SpendWithConditions,
174    {
175        let memos = ctx.hint(p2_puzzle_hash)?;
176
177        self.spend_with(
178            ctx,
179            inner,
180            extra_conditions.create_coin(p2_puzzle_hash, self.coin.amount, memos),
181        )?;
182
183        Ok(self.child(p2_puzzle_hash, self.coin.amount))
184    }
185
186    pub fn exercise<I>(
187        self,
188        ctx: &mut SpendContext,
189        inner: &I,
190        extra_conditions: Conditions,
191    ) -> Result<(), DriverError>
192    where
193        I: SpendWithConditions,
194    {
195        let data = ctx.alloc(&self.info.underlying_coin_id)?;
196
197        self.spend_with(
198            ctx,
199            inner,
200            extra_conditions
201                .send_message(
202                    23,
203                    self.info.underlying_delegated_puzzle_hash.into(),
204                    vec![data],
205                )
206                .melt_singleton(),
207        )?;
208
209        Ok(())
210    }
211
212    pub fn child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
213        let info = OptionInfo {
214            p2_puzzle_hash,
215            ..self.info
216        };
217
218        let inner_puzzle_hash = info.inner_puzzle_hash();
219
220        Self::new(
221            Coin::new(
222                self.coin.coin_id(),
223                SingletonArgs::curry_tree_hash(info.launcher_id, inner_puzzle_hash).into(),
224                amount,
225            ),
226            Proof::Lineage(self.child_lineage_proof()),
227            info,
228        )
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use std::slice;
235
236    use chia_puzzle_types::{offer::SettlementPaymentsSolution, Memos};
237    use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
238    use chia_sdk_test::{expect_spend, Simulator};
239    use chia_sdk_types::{
240        conditions::TransferNft,
241        puzzles::{RevocationArgs, RevocationSolution},
242    };
243    use rstest::rstest;
244
245    use crate::{
246        Cat, CatSpend, HashedPtr, Launcher, Nft, NftMint, OptionLauncher, OptionLauncherInfo,
247        OptionType, SettlementLayer, SingletonInfo, StandardLayer,
248    };
249
250    use super::*;
251
252    enum Action {
253        Exercise,
254        ExerciseWithoutPayment,
255        Clawback,
256    }
257
258    enum Type {
259        Xch,
260        Cat,
261        RevocableCat,
262        Nft,
263    }
264
265    enum OptionCoin {
266        Xch(Coin),
267        Cat(Cat),
268        RevocableCat(Cat),
269        Nft(Nft),
270    }
271
272    impl OptionCoin {
273        fn coin_id(&self) -> Bytes32 {
274            match self {
275                Self::Xch(coin) => coin.coin_id(),
276                Self::Cat(cat) | Self::RevocableCat(cat) => cat.coin.coin_id(),
277                Self::Nft(nft) => nft.coin.coin_id(),
278            }
279        }
280    }
281
282    #[rstest]
283    fn test_option_actions(
284        #[values(true, false)] expired: bool,
285        #[values(Action::Exercise, Action::ExerciseWithoutPayment, Action::Clawback)]
286        action: Action,
287        #[values(Type::Xch, Type::Cat, Type::RevocableCat, Type::Nft)] underlying_type: Type,
288        #[values(1, 1000, u64::MAX)] underlying_amount: u64,
289        #[values(Type::Xch, Type::Cat, Type::RevocableCat, Type::Nft)] strike_type: Type,
290        #[values(1, 1000, u64::MAX)] strike_amount: u64,
291    ) -> anyhow::Result<()> {
292        if matches!(underlying_type, Type::Nft) && underlying_amount != 1 {
293            return Ok(());
294        }
295
296        if matches!(strike_type, Type::Nft) && strike_amount != 1 {
297            return Ok(());
298        }
299
300        let mut sim = Simulator::new();
301        let ctx = &mut SpendContext::new();
302
303        if expired {
304            sim.set_next_timestamp(100)?;
305        }
306
307        let alice = sim.bls(1);
308        let alice_p2 = StandardLayer::new(alice.pk);
309
310        let strike_parent_coin = sim.new_coin(
311            alice.puzzle_hash,
312            if matches!(strike_type, Type::Nft) {
313                strike_amount + 1
314            } else {
315                strike_amount
316            },
317        );
318        let (strike_coin, strike_type) = match strike_type {
319            Type::Xch => {
320                alice_p2.spend(
321                    ctx,
322                    strike_parent_coin,
323                    Conditions::new().create_coin(
324                        SETTLEMENT_PAYMENT_HASH.into(),
325                        strike_amount,
326                        Memos::None,
327                    ),
328                )?;
329                let coin = OptionCoin::Xch(Coin::new(
330                    strike_parent_coin.coin_id(),
331                    SETTLEMENT_PAYMENT_HASH.into(),
332                    strike_amount,
333                ));
334                (
335                    coin,
336                    OptionType::Xch {
337                        amount: strike_amount,
338                    },
339                )
340            }
341            Type::Cat => {
342                let hint = ctx.hint(SETTLEMENT_PAYMENT_HASH.into())?;
343                let (issue_cat, cats) = Cat::issue_with_coin(
344                    ctx,
345                    strike_parent_coin.coin_id(),
346                    strike_amount,
347                    Conditions::new().create_coin(
348                        SETTLEMENT_PAYMENT_HASH.into(),
349                        strike_amount,
350                        hint,
351                    ),
352                )?;
353                alice_p2.spend(ctx, strike_parent_coin, issue_cat)?;
354                let coin = OptionCoin::Cat(cats[0]);
355                (
356                    coin,
357                    OptionType::Cat {
358                        asset_id: cats[0].info.asset_id,
359                        amount: strike_amount,
360                    },
361                )
362            }
363            Type::RevocableCat => {
364                let hint = ctx.hint(SETTLEMENT_PAYMENT_HASH.into())?;
365                let revocation_settlement_hash =
366                    RevocationArgs::new(Bytes32::default(), SETTLEMENT_PAYMENT_HASH.into())
367                        .curry_tree_hash()
368                        .into();
369                let (issue_cat, cats) = Cat::issue_with_coin(
370                    ctx,
371                    strike_parent_coin.coin_id(),
372                    strike_amount,
373                    Conditions::new().create_coin(revocation_settlement_hash, strike_amount, hint),
374                )?;
375                alice_p2.spend(ctx, strike_parent_coin, issue_cat)?;
376                let coin = OptionCoin::RevocableCat(cats[0]);
377                (
378                    coin,
379                    OptionType::RevocableCat {
380                        asset_id: cats[0].info.asset_id,
381                        hidden_puzzle_hash: Bytes32::default(),
382                        amount: strike_amount,
383                    },
384                )
385            }
386            Type::Nft => {
387                let (create_did, did) = Launcher::new(strike_parent_coin.coin_id(), 1)
388                    .create_simple_did(ctx, &alice_p2)?;
389
390                let (mint_nft, nft) = Launcher::new(did.coin.coin_id(), 0)
391                    .with_singleton_amount(strike_amount)
392                    .mint_nft(
393                        ctx,
394                        &NftMint::new(
395                            HashedPtr::NIL,
396                            SETTLEMENT_PAYMENT_HASH.into(),
397                            0,
398                            Some(TransferNft::new(
399                                Some(did.info.launcher_id),
400                                Vec::new(),
401                                Some(did.info.inner_puzzle_hash().into()),
402                            )),
403                        ),
404                    )?;
405
406                alice_p2.spend(ctx, strike_parent_coin, create_did)?;
407                let _did = did.update(ctx, &alice_p2, mint_nft)?;
408
409                let launcher_id = nft.info.launcher_id;
410
411                (
412                    OptionCoin::Nft(nft),
413                    OptionType::Nft {
414                        launcher_id,
415                        settlement_puzzle_hash: nft.coin.puzzle_hash,
416                        amount: strike_amount,
417                    },
418                )
419            }
420        };
421
422        let launcher = OptionLauncher::new(
423            ctx,
424            alice.coin.coin_id(),
425            OptionLauncherInfo::new(
426                alice.puzzle_hash,
427                alice.puzzle_hash,
428                10,
429                underlying_amount,
430                strike_type,
431            ),
432            1,
433        )?;
434        let underlying = launcher.underlying();
435        let p2_option = launcher.p2_puzzle_hash();
436
437        let underlying_parent_coin = sim.new_coin(
438            alice.puzzle_hash,
439            if matches!(underlying_type, Type::Nft) {
440                underlying_amount + 1
441            } else {
442                underlying_amount
443            },
444        );
445        let underlying_coin = match underlying_type {
446            Type::Xch => {
447                alice_p2.spend(
448                    ctx,
449                    underlying_parent_coin,
450                    Conditions::new().create_coin(p2_option, underlying_amount, Memos::None),
451                )?;
452                OptionCoin::Xch(Coin::new(
453                    underlying_parent_coin.coin_id(),
454                    p2_option,
455                    underlying_amount,
456                ))
457            }
458            Type::Cat => {
459                let hint = ctx.hint(p2_option)?;
460                let (issue_cat, cats) = Cat::issue_with_coin(
461                    ctx,
462                    underlying_parent_coin.coin_id(),
463                    underlying_amount,
464                    Conditions::new().create_coin(p2_option, underlying_amount, hint),
465                )?;
466                alice_p2.spend(ctx, underlying_parent_coin, issue_cat)?;
467                OptionCoin::Cat(cats[0])
468            }
469            Type::RevocableCat => {
470                let hint = ctx.hint(p2_option)?;
471                let revocation_p2_option = RevocationArgs::new(Bytes32::default(), p2_option)
472                    .curry_tree_hash()
473                    .into();
474                let (issue_cat, cats) = Cat::issue_with_coin(
475                    ctx,
476                    underlying_parent_coin.coin_id(),
477                    underlying_amount,
478                    Conditions::new().create_coin(revocation_p2_option, underlying_amount, hint),
479                )?;
480                alice_p2.spend(ctx, underlying_parent_coin, issue_cat)?;
481                OptionCoin::RevocableCat(cats[0])
482            }
483            Type::Nft => {
484                let (create_did, did) = Launcher::new(underlying_parent_coin.coin_id(), 1)
485                    .create_simple_did(ctx, &alice_p2)?;
486
487                let (mint_nft, nft) = Launcher::new(did.coin.coin_id(), 0)
488                    .with_singleton_amount(underlying_amount)
489                    .mint_nft(
490                        ctx,
491                        &NftMint::new(
492                            HashedPtr::NIL,
493                            p2_option,
494                            0,
495                            Some(TransferNft::new(
496                                Some(did.info.launcher_id),
497                                Vec::new(),
498                                Some(did.info.inner_puzzle_hash().into()),
499                            )),
500                        ),
501                    )?;
502
503                alice_p2.spend(ctx, underlying_parent_coin, create_did)?;
504                let _did = did.update(ctx, &alice_p2, mint_nft)?;
505
506                OptionCoin::Nft(nft)
507            }
508        };
509
510        let launcher = launcher.with_underlying(underlying_coin.coin_id());
511
512        let (mint_option, option) = launcher.mint(ctx)?;
513        alice_p2.spend(ctx, alice.coin, mint_option)?;
514
515        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
516
517        match action {
518            Action::Exercise | Action::ExerciseWithoutPayment => {
519                option.exercise(ctx, &alice_p2, Conditions::new())?;
520
521                match underlying_coin {
522                    OptionCoin::Xch(coin) => {
523                        underlying.exercise_coin_spend(
524                            ctx,
525                            coin,
526                            option.info.inner_puzzle_hash().into(),
527                            option.coin.amount,
528                        )?;
529                    }
530                    OptionCoin::Cat(cat) => {
531                        let exercise_spend = underlying.exercise_spend(
532                            ctx,
533                            option.info.inner_puzzle_hash().into(),
534                            option.coin.amount,
535                        )?;
536                        Cat::spend_all(ctx, &[CatSpend::new(cat, exercise_spend)])?;
537                    }
538                    OptionCoin::RevocableCat(cat) => {
539                        let exercise_spend = underlying.exercise_spend(
540                            ctx,
541                            option.info.inner_puzzle_hash().into(),
542                            option.coin.amount,
543                        )?;
544                        let puzzle =
545                            ctx.curry(RevocationArgs::new(Bytes32::default(), p2_option))?;
546                        let solution = ctx.alloc(&RevocationSolution::new(
547                            false,
548                            exercise_spend.puzzle,
549                            exercise_spend.solution,
550                        ))?;
551                        let exercise_spend = Spend::new(puzzle, solution);
552                        Cat::spend_all(ctx, &[CatSpend::new(cat, exercise_spend)])?;
553                    }
554                    OptionCoin::Nft(nft) => {
555                        let exercise_spend = underlying.exercise_spend(
556                            ctx,
557                            option.info.inner_puzzle_hash().into(),
558                            option.coin.amount,
559                        )?;
560                        let _nft = nft.spend(ctx, exercise_spend)?;
561                    }
562                }
563            }
564            Action::Clawback => match underlying_coin {
565                OptionCoin::Xch(coin) => {
566                    let clawback_spend = alice_p2.spend_with_conditions(
567                        ctx,
568                        Conditions::new().create_coin(
569                            alice.puzzle_hash,
570                            underlying_amount,
571                            Memos::None,
572                        ),
573                    )?;
574                    underlying.clawback_coin_spend(ctx, coin, clawback_spend)?;
575                }
576                OptionCoin::Cat(cat) => {
577                    let hint = ctx.hint(alice.puzzle_hash)?;
578                    let clawback_spend = alice_p2.spend_with_conditions(
579                        ctx,
580                        Conditions::new().create_coin(alice.puzzle_hash, underlying_amount, hint),
581                    )?;
582                    let clawback_spend = underlying.clawback_spend(ctx, clawback_spend)?;
583                    Cat::spend_all(ctx, &[CatSpend::new(cat, clawback_spend)])?;
584                }
585                OptionCoin::RevocableCat(cat) => {
586                    let hint = ctx.hint(alice.puzzle_hash)?;
587                    let clawback_spend = alice_p2.spend_with_conditions(
588                        ctx,
589                        Conditions::new().create_coin(alice.puzzle_hash, underlying_amount, hint),
590                    )?;
591                    let clawback_spend = underlying.clawback_spend(ctx, clawback_spend)?;
592                    let puzzle = ctx.curry(RevocationArgs::new(Bytes32::default(), p2_option))?;
593                    let solution = ctx.alloc(&RevocationSolution::new(
594                        false,
595                        clawback_spend.puzzle,
596                        clawback_spend.solution,
597                    ))?;
598                    let clawback_spend = Spend::new(puzzle, solution);
599                    Cat::spend_all(ctx, &[CatSpend::new(cat, clawback_spend)])?;
600                }
601                OptionCoin::Nft(nft) => {
602                    let hint = ctx.hint(alice.puzzle_hash)?;
603                    let clawback_spend = alice_p2.spend_with_conditions(
604                        ctx,
605                        Conditions::new().create_coin(alice.puzzle_hash, underlying_amount, hint),
606                    )?;
607                    let clawback_spend = underlying.clawback_spend(ctx, clawback_spend)?;
608                    let _nft = nft.spend(ctx, clawback_spend)?;
609                }
610            },
611        }
612
613        if matches!(action, Action::Exercise) {
614            match strike_coin {
615                OptionCoin::Xch(coin) => {
616                    let payment = underlying.requested_payment(&mut **ctx)?;
617                    let coin_spend = SettlementLayer.construct_coin_spend(
618                        ctx,
619                        coin,
620                        SettlementPaymentsSolution::new(vec![payment]),
621                    )?;
622                    ctx.insert(coin_spend);
623                }
624                OptionCoin::Cat(cat) => {
625                    let payment = underlying.requested_payment(&mut **ctx)?;
626                    let spend = SettlementLayer
627                        .construct_spend(ctx, SettlementPaymentsSolution::new(vec![payment]))?;
628                    Cat::spend_all(ctx, &[CatSpend::new(cat, spend)])?;
629                }
630                OptionCoin::RevocableCat(cat) => {
631                    let payment = underlying.requested_payment(&mut **ctx)?;
632                    let spend = SettlementLayer
633                        .construct_spend(ctx, SettlementPaymentsSolution::new(vec![payment]))?;
634                    let puzzle = ctx.curry(RevocationArgs::new(
635                        Bytes32::default(),
636                        SETTLEMENT_PAYMENT_HASH.into(),
637                    ))?;
638                    let solution = ctx.alloc(&RevocationSolution::new(
639                        false,
640                        spend.puzzle,
641                        spend.solution,
642                    ))?;
643                    Cat::spend_all(ctx, &[CatSpend::new(cat, Spend::new(puzzle, solution))])?;
644                }
645                OptionCoin::Nft(nft) => {
646                    let payment = underlying.requested_payment(&mut **ctx)?;
647                    let spend = SettlementLayer
648                        .construct_spend(ctx, SettlementPaymentsSolution::new(vec![payment]))?;
649                    let _nft = nft.spend(ctx, spend)?;
650                }
651            }
652        }
653
654        expect_spend(
655            sim.spend_coins(ctx.take(), &[alice.sk]),
656            match action {
657                Action::Exercise => !expired,
658                Action::ExerciseWithoutPayment => false,
659                Action::Clawback => expired,
660            },
661        );
662
663        Ok(())
664    }
665
666    #[test]
667    fn test_transfer_option() -> anyhow::Result<()> {
668        let mut sim = Simulator::new();
669        let ctx = &mut SpendContext::new();
670
671        let alice = sim.bls(1);
672        let alice_p2 = StandardLayer::new(alice.pk);
673
674        let parent_coin = sim.new_coin(alice.puzzle_hash, 1);
675
676        let launcher = OptionLauncher::new(
677            ctx,
678            alice.coin.coin_id(),
679            OptionLauncherInfo::new(
680                alice.puzzle_hash,
681                alice.puzzle_hash,
682                10,
683                1,
684                OptionType::Xch { amount: 1 },
685            ),
686            1,
687        )?;
688        let p2_option = launcher.p2_puzzle_hash();
689
690        alice_p2.spend(
691            ctx,
692            parent_coin,
693            Conditions::new().create_coin(p2_option, 1, Memos::None),
694        )?;
695        let underlying_coin = Coin::new(parent_coin.coin_id(), p2_option, 1);
696        let launcher = launcher.with_underlying(underlying_coin.coin_id());
697
698        let (mint_option, mut option) = launcher.mint(ctx)?;
699        alice_p2.spend(ctx, alice.coin, mint_option)?;
700
701        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
702
703        for _ in 0..5 {
704            option = option.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
705        }
706
707        sim.spend_coins(ctx.take(), &[alice.sk])?;
708
709        Ok(())
710    }
711
712    #[rstest]
713    fn test_incomplete_exercise(#[values(true, false)] melt: bool) -> anyhow::Result<()> {
714        let mut sim = Simulator::new();
715        let ctx = &mut SpendContext::new();
716
717        let alice = sim.bls(1);
718        let alice_p2 = StandardLayer::new(alice.pk);
719
720        let parent_coin = sim.new_coin(alice.puzzle_hash, 1);
721
722        let launcher = OptionLauncher::new(
723            ctx,
724            alice.coin.coin_id(),
725            OptionLauncherInfo::new(
726                alice.puzzle_hash,
727                alice.puzzle_hash,
728                10,
729                1,
730                OptionType::Xch { amount: 1 },
731            ),
732            1,
733        )?;
734        let p2_option = launcher.p2_puzzle_hash();
735
736        alice_p2.spend(
737            ctx,
738            parent_coin,
739            Conditions::new().create_coin(p2_option, 1, Memos::None),
740        )?;
741        let underlying_coin = Coin::new(parent_coin.coin_id(), p2_option, 1);
742        let launcher = launcher.with_underlying(underlying_coin.coin_id());
743
744        let (mint_option, option) = launcher.mint(ctx)?;
745        alice_p2.spend(ctx, alice.coin, mint_option)?;
746
747        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
748
749        let data = ctx.alloc(&option.info.underlying_coin_id)?;
750
751        option.spend_with(
752            ctx,
753            &alice_p2,
754            if melt {
755                Conditions::new().melt_singleton()
756            } else {
757                Conditions::new().send_message(
758                    23,
759                    option.info.underlying_delegated_puzzle_hash.into(),
760                    vec![data],
761                )
762            },
763        )?;
764
765        assert!(sim.spend_coins(ctx.take(), &[alice.sk]).is_err());
766
767        Ok(())
768    }
769}