1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::Memos;
3use chia_sdk_types::conditions::CreateCoin;
4use clvm_utils::ToTreeHash;
5
6use crate::{
7 Asset, Deltas, DriverError, Id, OptionType, Output, SingletonSpends, SpendAction, SpendContext,
8 SpendKind, Spends,
9};
10
11#[derive(Debug, Clone, Copy)]
12pub struct MintOptionAction {
13 pub creator_puzzle_hash: Bytes32,
14 pub seconds: u64,
15 pub underlying_id: Id,
16 pub underlying_amount: u64,
17 pub strike_type: OptionType,
18 pub amount: u64,
19}
20
21impl MintOptionAction {
22 pub fn new(
23 creator_puzzle_hash: Bytes32,
24 seconds: u64,
25 underlying_id: Id,
26 underlying_amount: u64,
27 strike_type: OptionType,
28 amount: u64,
29 ) -> Self {
30 Self {
31 creator_puzzle_hash,
32 seconds,
33 underlying_id,
34 underlying_amount,
35 strike_type,
36 amount,
37 }
38 }
39
40 fn lock_underlying(
41 &self,
42 ctx: &mut SpendContext,
43 spends: &mut Spends,
44 p2_puzzle_hash: Bytes32,
45 ) -> Result<Bytes32, DriverError> {
46 let output = Output::new(p2_puzzle_hash, self.underlying_amount);
47 let create_coin = CreateCoin::new(p2_puzzle_hash, self.underlying_amount, Memos::None);
48
49 if matches!(self.underlying_id, Id::Xch) {
50 let source = spends.xch.output_source(ctx, &output)?;
51 let parent = &mut spends.xch.items[source];
52 let parent_puzzle_hash = parent.asset.full_puzzle_hash();
53
54 parent.kind.create_coin_with_assertion(
55 ctx,
56 parent_puzzle_hash,
57 &mut spends.xch.payment_assertions,
58 create_coin,
59 );
60
61 let coin = Coin::new(
62 parent.asset.coin_id(),
63 p2_puzzle_hash,
64 self.underlying_amount,
65 );
66
67 spends.outputs.xch.push(coin);
68
69 return Ok(coin.coin_id());
70 } else if let Some(cat) = spends.cats.get_mut(&self.underlying_id) {
71 let source = cat.output_source(ctx, &output)?;
72 let parent = &mut cat.items[source];
73 let parent_puzzle_hash = parent.asset.full_puzzle_hash();
74
75 parent.kind.create_coin_with_assertion(
76 ctx,
77 parent_puzzle_hash,
78 &mut cat.payment_assertions,
79 create_coin,
80 );
81
82 let cat = parent.asset.child(p2_puzzle_hash, self.underlying_amount);
83
84 spends
85 .outputs
86 .cats
87 .entry(self.underlying_id)
88 .or_default()
89 .push(cat);
90
91 return Ok(cat.coin_id());
92 } else if let Some(nft) = spends.nfts.get_mut(&self.underlying_id) {
93 let source = nft.last_mut()?;
94 source.child_info.destination = Some(create_coin);
95
96 let Some(nft) = nft.finalize(
97 ctx,
98 spends.intermediate_puzzle_hash,
99 spends.change_puzzle_hash,
100 )?
101 else {
102 return Err(DriverError::NoSourceForOutput);
103 };
104
105 spends.outputs.nfts.insert(self.underlying_id, nft);
106
107 return Ok(nft.coin_id());
108 }
109
110 Err(DriverError::InvalidAssetId)
111 }
112}
113
114impl SpendAction for MintOptionAction {
115 fn calculate_delta(&self, deltas: &mut Deltas, index: usize) {
116 deltas.update(Id::Xch).output += self.amount;
117 deltas.update(Id::New(index)).input += self.amount;
118 deltas.update(self.underlying_id).output += self.underlying_amount;
119 deltas.set_needed(Id::Xch);
120 }
121
122 fn spend(
123 &self,
124 ctx: &mut SpendContext,
125 spends: &mut Spends,
126 index: usize,
127 ) -> Result<(), DriverError> {
128 let (source, launcher) = spends.xch.create_option_launcher(
129 ctx,
130 self.amount,
131 self.creator_puzzle_hash,
132 self.seconds,
133 self.underlying_amount,
134 self.strike_type,
135 )?;
136
137 let underlying_p2_puzzle_hash = launcher.underlying().tree_hash().into();
138 let underlying_coin_id = self.lock_underlying(ctx, spends, underlying_p2_puzzle_hash)?;
139
140 let source = &mut spends.xch.items[source];
141
142 let (parent_conditions, eve_option) = launcher
143 .with_underlying(underlying_coin_id)
144 .mint_eve(ctx, source.asset.p2_puzzle_hash())?;
145
146 match &mut source.kind {
147 SpendKind::Conditions(spend) => {
148 spend.add_conditions(parent_conditions);
149 }
150 SpendKind::Settlement(_) => {
151 return Err(DriverError::CannotEmitConditions);
152 }
153 }
154
155 spends
156 .options
157 .insert(Id::New(index), SingletonSpends::new(eve_option, true));
158
159 Ok(())
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use anyhow::Result;
166 use chia_sdk_test::Simulator;
167 use indexmap::indexmap;
168 use rstest::rstest;
169
170 use crate::{Action, Relation};
171
172 use super::*;
173
174 #[rstest]
175 #[case::normal(None)]
176 #[case::revocable(Some(Bytes32::default()))]
177 fn test_action_mint_option(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
178 let mut sim = Simulator::new();
179 let mut ctx = SpendContext::new();
180
181 let alice = sim.bls(6);
182
183 let mut spends = Spends::new(alice.puzzle_hash);
184 spends.add(alice.coin);
185
186 let deltas = spends.apply(
187 &mut ctx,
188 &[
189 Action::single_issue_cat(hidden_puzzle_hash, 5),
190 Action::mint_option(
191 alice.puzzle_hash,
192 100,
193 Id::New(0),
194 5,
195 OptionType::Xch { amount: 5 },
196 1,
197 ),
198 ],
199 )?;
200
201 let outputs = spends.finish_with_keys(
202 &mut ctx,
203 &deltas,
204 Relation::None,
205 &indexmap! { alice.puzzle_hash => alice.pk },
206 )?;
207
208 sim.spend_coins(ctx.take(), &[alice.sk])?;
209
210 let option = outputs.options[&Id::New(1)];
211 assert_ne!(sim.coin_state(option.coin.coin_id()), None);
212 assert_eq!(option.info.p2_puzzle_hash, alice.puzzle_hash);
213 assert_eq!(option.coin.amount, 1);
214
215 Ok(())
216 }
217}