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 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}