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