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