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}