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