Skip to main content

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::single_issuance(
345                    ctx,
346                    strike_parent_coin.coin_id(),
347                    None,
348                    strike_amount,
349                    Conditions::new().create_coin(
350                        SETTLEMENT_PAYMENT_HASH.into(),
351                        strike_amount,
352                        hint,
353                    ),
354                )?;
355                alice_p2.spend(ctx, strike_parent_coin, issue_cat)?;
356                let coin = OptionCoin::Cat(cats[0]);
357                (
358                    coin,
359                    OptionType::Cat {
360                        asset_id: cats[0].info.asset_id,
361                        amount: strike_amount,
362                    },
363                )
364            }
365            Type::RevocableCat => {
366                let hint = ctx.hint(SETTLEMENT_PAYMENT_HASH.into())?;
367                let revocation_settlement_hash =
368                    RevocationArgs::new(Bytes32::default(), SETTLEMENT_PAYMENT_HASH.into())
369                        .curry_tree_hash()
370                        .into();
371                let (issue_cat, cats) = Cat::single_issuance(
372                    ctx,
373                    strike_parent_coin.coin_id(),
374                    None,
375                    strike_amount,
376                    Conditions::new().create_coin(revocation_settlement_hash, strike_amount, hint),
377                )?;
378                alice_p2.spend(ctx, strike_parent_coin, issue_cat)?;
379                let coin = OptionCoin::RevocableCat(cats[0]);
380                (
381                    coin,
382                    OptionType::RevocableCat {
383                        asset_id: cats[0].info.asset_id,
384                        hidden_puzzle_hash: Bytes32::default(),
385                        amount: strike_amount,
386                    },
387                )
388            }
389            Type::Nft => {
390                let (create_did, did) = Launcher::new(strike_parent_coin.coin_id(), 1)
391                    .create_simple_did(ctx, &alice_p2)?;
392
393                let (mint_nft, nft) = Launcher::new(did.coin.coin_id(), 0)
394                    .with_singleton_amount(strike_amount)
395                    .mint_nft(
396                        ctx,
397                        &NftMint::new(
398                            HashedPtr::NIL,
399                            SETTLEMENT_PAYMENT_HASH.into(),
400                            0,
401                            Some(TransferNft::new(
402                                Some(did.info.launcher_id),
403                                Vec::new(),
404                                Some(did.info.inner_puzzle_hash().into()),
405                            )),
406                        ),
407                    )?;
408
409                alice_p2.spend(ctx, strike_parent_coin, create_did)?;
410                let _did = did.update(ctx, &alice_p2, mint_nft)?;
411
412                let launcher_id = nft.info.launcher_id;
413
414                (
415                    OptionCoin::Nft(nft),
416                    OptionType::Nft {
417                        launcher_id,
418                        settlement_puzzle_hash: nft.coin.puzzle_hash,
419                        amount: strike_amount,
420                    },
421                )
422            }
423        };
424
425        let launcher = OptionLauncher::new(
426            ctx,
427            alice.coin.coin_id(),
428            OptionLauncherInfo::new(
429                alice.puzzle_hash,
430                alice.puzzle_hash,
431                10,
432                underlying_amount,
433                strike_type,
434            ),
435            1,
436        )?;
437        let underlying = launcher.underlying();
438        let p2_option = launcher.p2_puzzle_hash();
439
440        let underlying_parent_coin = sim.new_coin(
441            alice.puzzle_hash,
442            if matches!(underlying_type, Type::Nft) {
443                underlying_amount + 1
444            } else {
445                underlying_amount
446            },
447        );
448        let underlying_coin = match underlying_type {
449            Type::Xch => {
450                alice_p2.spend(
451                    ctx,
452                    underlying_parent_coin,
453                    Conditions::new().create_coin(p2_option, underlying_amount, Memos::None),
454                )?;
455                OptionCoin::Xch(Coin::new(
456                    underlying_parent_coin.coin_id(),
457                    p2_option,
458                    underlying_amount,
459                ))
460            }
461            Type::Cat => {
462                let hint = ctx.hint(p2_option)?;
463                let (issue_cat, cats) = Cat::single_issuance(
464                    ctx,
465                    underlying_parent_coin.coin_id(),
466                    None,
467                    underlying_amount,
468                    Conditions::new().create_coin(p2_option, underlying_amount, hint),
469                )?;
470                alice_p2.spend(ctx, underlying_parent_coin, issue_cat)?;
471                OptionCoin::Cat(cats[0])
472            }
473            Type::RevocableCat => {
474                let hint = ctx.hint(p2_option)?;
475                let revocation_p2_option = RevocationArgs::new(Bytes32::default(), p2_option)
476                    .curry_tree_hash()
477                    .into();
478                let (issue_cat, cats) = Cat::single_issuance(
479                    ctx,
480                    underlying_parent_coin.coin_id(),
481                    None,
482                    underlying_amount,
483                    Conditions::new().create_coin(revocation_p2_option, underlying_amount, hint),
484                )?;
485                alice_p2.spend(ctx, underlying_parent_coin, issue_cat)?;
486                OptionCoin::RevocableCat(cats[0])
487            }
488            Type::Nft => {
489                let (create_did, did) = Launcher::new(underlying_parent_coin.coin_id(), 1)
490                    .create_simple_did(ctx, &alice_p2)?;
491
492                let (mint_nft, nft) = Launcher::new(did.coin.coin_id(), 0)
493                    .with_singleton_amount(underlying_amount)
494                    .mint_nft(
495                        ctx,
496                        &NftMint::new(
497                            HashedPtr::NIL,
498                            p2_option,
499                            0,
500                            Some(TransferNft::new(
501                                Some(did.info.launcher_id),
502                                Vec::new(),
503                                Some(did.info.inner_puzzle_hash().into()),
504                            )),
505                        ),
506                    )?;
507
508                alice_p2.spend(ctx, underlying_parent_coin, create_did)?;
509                let _did = did.update(ctx, &alice_p2, mint_nft)?;
510
511                OptionCoin::Nft(nft)
512            }
513        };
514
515        let launcher = launcher.with_underlying(underlying_coin.coin_id());
516
517        let (mint_option, option) = launcher.mint(ctx)?;
518        alice_p2.spend(ctx, alice.coin, mint_option)?;
519
520        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
521
522        match action {
523            Action::Exercise | Action::ExerciseWithoutPayment => {
524                option.exercise(ctx, &alice_p2, Conditions::new())?;
525
526                match underlying_coin {
527                    OptionCoin::Xch(coin) => {
528                        underlying.exercise_coin_spend(
529                            ctx,
530                            coin,
531                            option.info.inner_puzzle_hash().into(),
532                            option.coin.amount,
533                        )?;
534                    }
535                    OptionCoin::Cat(cat) => {
536                        let exercise_spend = underlying.exercise_spend(
537                            ctx,
538                            option.info.inner_puzzle_hash().into(),
539                            option.coin.amount,
540                        )?;
541                        Cat::spend_all(ctx, &[CatSpend::new(cat, exercise_spend)])?;
542                    }
543                    OptionCoin::RevocableCat(cat) => {
544                        let exercise_spend = underlying.exercise_spend(
545                            ctx,
546                            option.info.inner_puzzle_hash().into(),
547                            option.coin.amount,
548                        )?;
549                        let puzzle =
550                            ctx.curry(RevocationArgs::new(Bytes32::default(), p2_option))?;
551                        let solution = ctx.alloc(&RevocationSolution::new(
552                            false,
553                            exercise_spend.puzzle,
554                            exercise_spend.solution,
555                        ))?;
556                        let exercise_spend = Spend::new(puzzle, solution);
557                        Cat::spend_all(ctx, &[CatSpend::new(cat, exercise_spend)])?;
558                    }
559                    OptionCoin::Nft(nft) => {
560                        let exercise_spend = underlying.exercise_spend(
561                            ctx,
562                            option.info.inner_puzzle_hash().into(),
563                            option.coin.amount,
564                        )?;
565                        let _nft = nft.spend(ctx, exercise_spend)?;
566                    }
567                }
568            }
569            Action::Clawback => match underlying_coin {
570                OptionCoin::Xch(coin) => {
571                    let clawback_spend = alice_p2.spend_with_conditions(
572                        ctx,
573                        Conditions::new().create_coin(
574                            alice.puzzle_hash,
575                            underlying_amount,
576                            Memos::None,
577                        ),
578                    )?;
579                    underlying.clawback_coin_spend(ctx, coin, clawback_spend)?;
580                }
581                OptionCoin::Cat(cat) => {
582                    let hint = ctx.hint(alice.puzzle_hash)?;
583                    let clawback_spend = alice_p2.spend_with_conditions(
584                        ctx,
585                        Conditions::new().create_coin(alice.puzzle_hash, underlying_amount, hint),
586                    )?;
587                    let clawback_spend = underlying.clawback_spend(ctx, clawback_spend)?;
588                    Cat::spend_all(ctx, &[CatSpend::new(cat, clawback_spend)])?;
589                }
590                OptionCoin::RevocableCat(cat) => {
591                    let hint = ctx.hint(alice.puzzle_hash)?;
592                    let clawback_spend = alice_p2.spend_with_conditions(
593                        ctx,
594                        Conditions::new().create_coin(alice.puzzle_hash, underlying_amount, hint),
595                    )?;
596                    let clawback_spend = underlying.clawback_spend(ctx, clawback_spend)?;
597                    let puzzle = ctx.curry(RevocationArgs::new(Bytes32::default(), p2_option))?;
598                    let solution = ctx.alloc(&RevocationSolution::new(
599                        false,
600                        clawback_spend.puzzle,
601                        clawback_spend.solution,
602                    ))?;
603                    let clawback_spend = Spend::new(puzzle, solution);
604                    Cat::spend_all(ctx, &[CatSpend::new(cat, clawback_spend)])?;
605                }
606                OptionCoin::Nft(nft) => {
607                    let hint = ctx.hint(alice.puzzle_hash)?;
608                    let clawback_spend = alice_p2.spend_with_conditions(
609                        ctx,
610                        Conditions::new().create_coin(alice.puzzle_hash, underlying_amount, hint),
611                    )?;
612                    let clawback_spend = underlying.clawback_spend(ctx, clawback_spend)?;
613                    let _nft = nft.spend(ctx, clawback_spend)?;
614                }
615            },
616        }
617
618        if matches!(action, Action::Exercise) {
619            match strike_coin {
620                OptionCoin::Xch(coin) => {
621                    let payment = underlying.requested_payment(&mut **ctx)?;
622                    let coin_spend = SettlementLayer.construct_coin_spend(
623                        ctx,
624                        coin,
625                        SettlementPaymentsSolution::new(vec![payment]),
626                    )?;
627                    ctx.insert(coin_spend);
628                }
629                OptionCoin::Cat(cat) => {
630                    let payment = underlying.requested_payment(&mut **ctx)?;
631                    let spend = SettlementLayer
632                        .construct_spend(ctx, SettlementPaymentsSolution::new(vec![payment]))?;
633                    Cat::spend_all(ctx, &[CatSpend::new(cat, spend)])?;
634                }
635                OptionCoin::RevocableCat(cat) => {
636                    let payment = underlying.requested_payment(&mut **ctx)?;
637                    let spend = SettlementLayer
638                        .construct_spend(ctx, SettlementPaymentsSolution::new(vec![payment]))?;
639                    let puzzle = ctx.curry(RevocationArgs::new(
640                        Bytes32::default(),
641                        SETTLEMENT_PAYMENT_HASH.into(),
642                    ))?;
643                    let solution = ctx.alloc(&RevocationSolution::new(
644                        false,
645                        spend.puzzle,
646                        spend.solution,
647                    ))?;
648                    Cat::spend_all(ctx, &[CatSpend::new(cat, Spend::new(puzzle, solution))])?;
649                }
650                OptionCoin::Nft(nft) => {
651                    let payment = underlying.requested_payment(&mut **ctx)?;
652                    let spend = SettlementLayer
653                        .construct_spend(ctx, SettlementPaymentsSolution::new(vec![payment]))?;
654                    let _nft = nft.spend(ctx, spend)?;
655                }
656            }
657        }
658
659        expect_spend(
660            sim.spend_coins(ctx.take(), &[alice.sk]),
661            match action {
662                Action::Exercise => !expired,
663                Action::ExerciseWithoutPayment => false,
664                Action::Clawback => expired,
665            },
666        );
667
668        Ok(())
669    }
670
671    #[test]
672    fn test_transfer_option() -> anyhow::Result<()> {
673        let mut sim = Simulator::new();
674        let ctx = &mut SpendContext::new();
675
676        let alice = sim.bls(1);
677        let alice_p2 = StandardLayer::new(alice.pk);
678
679        let parent_coin = sim.new_coin(alice.puzzle_hash, 1);
680
681        let launcher = OptionLauncher::new(
682            ctx,
683            alice.coin.coin_id(),
684            OptionLauncherInfo::new(
685                alice.puzzle_hash,
686                alice.puzzle_hash,
687                10,
688                1,
689                OptionType::Xch { amount: 1 },
690            ),
691            1,
692        )?;
693        let p2_option = launcher.p2_puzzle_hash();
694
695        alice_p2.spend(
696            ctx,
697            parent_coin,
698            Conditions::new().create_coin(p2_option, 1, Memos::None),
699        )?;
700        let underlying_coin = Coin::new(parent_coin.coin_id(), p2_option, 1);
701        let launcher = launcher.with_underlying(underlying_coin.coin_id());
702
703        let (mint_option, mut option) = launcher.mint(ctx)?;
704        alice_p2.spend(ctx, alice.coin, mint_option)?;
705
706        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
707
708        for _ in 0..5 {
709            option = option.transfer(ctx, &alice_p2, alice.puzzle_hash, Conditions::new())?;
710        }
711
712        sim.spend_coins(ctx.take(), &[alice.sk])?;
713
714        Ok(())
715    }
716
717    #[rstest]
718    fn test_incomplete_exercise(#[values(true, false)] melt: bool) -> anyhow::Result<()> {
719        let mut sim = Simulator::new();
720        let ctx = &mut SpendContext::new();
721
722        let alice = sim.bls(1);
723        let alice_p2 = StandardLayer::new(alice.pk);
724
725        let parent_coin = sim.new_coin(alice.puzzle_hash, 1);
726
727        let launcher = OptionLauncher::new(
728            ctx,
729            alice.coin.coin_id(),
730            OptionLauncherInfo::new(
731                alice.puzzle_hash,
732                alice.puzzle_hash,
733                10,
734                1,
735                OptionType::Xch { amount: 1 },
736            ),
737            1,
738        )?;
739        let p2_option = launcher.p2_puzzle_hash();
740
741        alice_p2.spend(
742            ctx,
743            parent_coin,
744            Conditions::new().create_coin(p2_option, 1, Memos::None),
745        )?;
746        let underlying_coin = Coin::new(parent_coin.coin_id(), p2_option, 1);
747        let launcher = launcher.with_underlying(underlying_coin.coin_id());
748
749        let (mint_option, option) = launcher.mint(ctx)?;
750        alice_p2.spend(ctx, alice.coin, mint_option)?;
751
752        sim.spend_coins(ctx.take(), slice::from_ref(&alice.sk))?;
753
754        let data = ctx.alloc(&option.info.underlying_coin_id)?;
755
756        option.spend_with(
757            ctx,
758            &alice_p2,
759            if melt {
760                Conditions::new().melt_singleton()
761            } else {
762                Conditions::new().send_message(
763                    23,
764                    option.info.underlying_delegated_puzzle_hash.into(),
765                    vec![data],
766                )
767            },
768        )?;
769
770        assert!(sim.spend_coins(ctx.take(), &[alice.sk]).is_err());
771
772        Ok(())
773    }
774}