chia_sdk_driver/primitives/option/
option_contract.rs

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