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(self.underlying_id);
120        deltas.set_needed(Id::Xch);
121    }
122
123    fn spend(
124        &self,
125        ctx: &mut SpendContext,
126        spends: &mut Spends,
127        index: usize,
128    ) -> Result<(), DriverError> {
129        let (source, launcher) = spends.xch.create_option_launcher(
130            ctx,
131            self.amount,
132            self.creator_puzzle_hash,
133            self.seconds,
134            self.underlying_amount,
135            self.strike_type,
136        )?;
137
138        let underlying_p2_puzzle_hash = launcher.underlying().tree_hash().into();
139        let underlying_coin_id = self.lock_underlying(ctx, spends, underlying_p2_puzzle_hash)?;
140
141        let source = &mut spends.xch.items[source];
142
143        let (parent_conditions, eve_option) = launcher
144            .with_underlying(underlying_coin_id)
145            .mint_eve(ctx, source.asset.p2_puzzle_hash())?;
146
147        match &mut source.kind {
148            SpendKind::Conditions(spend) => {
149                spend.add_conditions(parent_conditions);
150            }
151            SpendKind::Settlement(_) => {
152                return Err(DriverError::CannotEmitConditions);
153            }
154        }
155
156        spends
157            .options
158            .insert(Id::New(index), SingletonSpends::new(eve_option, true));
159
160        Ok(())
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use anyhow::Result;
167    use chia_sdk_test::Simulator;
168    use indexmap::indexmap;
169    use rstest::rstest;
170
171    use crate::{Action, Relation};
172
173    use super::*;
174
175    #[rstest]
176    #[case::normal(None)]
177    #[case::revocable(Some(Bytes32::default()))]
178    fn test_action_mint_option(#[case] hidden_puzzle_hash: Option<Bytes32>) -> Result<()> {
179        let mut sim = Simulator::new();
180        let mut ctx = SpendContext::new();
181
182        let alice = sim.bls(6);
183
184        let mut spends = Spends::new(alice.puzzle_hash);
185        spends.add(alice.coin);
186
187        let deltas = spends.apply(
188            &mut ctx,
189            &[
190                Action::single_issue_cat(hidden_puzzle_hash, 5),
191                Action::mint_option(
192                    alice.puzzle_hash,
193                    100,
194                    Id::New(0),
195                    5,
196                    OptionType::Xch { amount: 5 },
197                    1,
198                ),
199            ],
200        )?;
201
202        let outputs = spends.finish_with_keys(
203            &mut ctx,
204            &deltas,
205            Relation::None,
206            &indexmap! { alice.puzzle_hash => alice.pk },
207        )?;
208
209        sim.spend_coins(ctx.take(), &[alice.sk])?;
210
211        let option = outputs.options[&Id::New(1)];
212        assert_ne!(sim.coin_state(option.coin.coin_id()), None);
213        assert_eq!(option.info.p2_puzzle_hash, alice.puzzle_hash);
214        assert_eq!(option.coin.amount, 1);
215
216        Ok(())
217    }
218}