chia_sdk_driver/actions/
mint_option.rs

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}