chia_sdk_driver/action_system/
fungible_spends.rs

1use chia_protocol::{Bytes32, Coin};
2use chia_puzzle_types::{
3    Memos,
4    cat::{CatArgs, GenesisByCoinIdTailArgs},
5    offer::NotarizedPayment,
6};
7use chia_puzzles::{SETTLEMENT_PAYMENT_HASH, SINGLETON_LAUNCHER_HASH};
8use chia_sdk_types::conditions::{AssertPuzzleAnnouncement, CreateCoin};
9
10use crate::{
11    Asset, Cat, Delta, DriverError, Launcher, OptionLauncher, OptionLauncherInfo, OptionType,
12    Output, OutputSet, SpendContext, SpendKind,
13};
14
15#[derive(Debug, Clone)]
16pub struct FungibleSpends<A> {
17    pub items: Vec<FungibleSpend<A>>,
18    pub payment_assertions: Vec<AssertPuzzleAnnouncement>,
19}
20
21impl<A> FungibleSpends<A>
22where
23    A: FungibleAsset,
24{
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn selected_amount(&self) -> u64 {
30        self.items
31            .iter()
32            .filter(|item| !item.ephemeral)
33            .map(|item| item.asset.amount())
34            .sum()
35    }
36
37    pub fn output_source(
38        &mut self,
39        ctx: &mut SpendContext,
40        output: &Output,
41    ) -> Result<usize, DriverError> {
42        if let Some(index) = self
43            .items
44            .iter()
45            .position(|item| item.kind.is_allowed(output, &item.asset.constraints()))
46        {
47            return Ok(index);
48        }
49
50        self.intermediate_source(ctx)
51    }
52
53    pub fn notarized_payment_source(
54        &mut self,
55        notarized_payment: &NotarizedPayment,
56    ) -> Result<usize, DriverError> {
57        if let Some(index) = self.items.iter().position(|item| {
58            item.kind.is_settlement()
59                && notarized_payment.payments.iter().all(|payment| {
60                    item.kind.is_allowed(
61                        &Output::new(payment.puzzle_hash, payment.amount),
62                        &item.asset.constraints(),
63                    )
64                })
65        }) {
66            return Ok(index);
67        }
68
69        self.intermediate_settlement_source()?
70            .ok_or(DriverError::NoSourceForOutput)
71    }
72
73    pub fn run_tail_source(&mut self, ctx: &mut SpendContext) -> Result<usize, DriverError> {
74        if let Some(index) = self
75            .items
76            .iter()
77            .position(|item| item.kind.can_run_cat_tail())
78        {
79            return Ok(index);
80        }
81
82        self.intermediate_source(ctx)
83    }
84
85    pub fn cat_issuance_source(
86        &mut self,
87        ctx: &mut SpendContext,
88        asset_id: Option<Bytes32>,
89        amount: u64,
90    ) -> Result<usize, DriverError> {
91        if let Some(index) = self.items.iter().position(|item| {
92            item.kind.is_allowed(
93                &Output::new(
94                    CatArgs::curry_tree_hash(
95                        asset_id.unwrap_or_else(|| {
96                            GenesisByCoinIdTailArgs::curry_tree_hash(item.asset.coin_id()).into()
97                        }),
98                        item.asset.p2_puzzle_hash().into(),
99                    )
100                    .into(),
101                    amount,
102                ),
103                &item.asset.constraints(),
104            )
105        }) {
106            return Ok(index);
107        }
108
109        self.intermediate_source(ctx)
110    }
111
112    pub fn intermediate_source(&mut self, ctx: &mut SpendContext) -> Result<usize, DriverError> {
113        let Some((index, amount)) = self.items.iter().enumerate().find_map(|(index, item)| {
114            item.kind
115                .find_amount(item.asset.p2_puzzle_hash(), &item.asset.constraints())
116                .map(|amount| (index, amount))
117        }) else {
118            return Err(DriverError::NoSourceForOutput);
119        };
120
121        let source = &mut self.items[index];
122
123        source.kind.create_intermediate_coin(CreateCoin::new(
124            source.asset.p2_puzzle_hash(),
125            amount,
126            source
127                .asset
128                .child_memos(ctx, source.asset.p2_puzzle_hash())?,
129        ));
130
131        let child = FungibleSpend::new(
132            source
133                .asset
134                .make_child(source.asset.p2_puzzle_hash(), amount),
135            true,
136        );
137
138        self.items.push(child);
139
140        Ok(self.items.len() - 1)
141    }
142
143    pub fn intermediate_settlement_source(&mut self) -> Result<Option<usize>, DriverError> {
144        let Some((index, amount)) = self.items.iter().enumerate().find_map(|(index, item)| {
145            item.kind
146                .find_amount(SETTLEMENT_PAYMENT_HASH.into(), &item.asset.constraints())
147                .map(|amount| (index, amount))
148        }) else {
149            return Ok(None);
150        };
151
152        let source = &mut self.items[index];
153
154        source.kind.create_intermediate_coin(CreateCoin::new(
155            SETTLEMENT_PAYMENT_HASH.into(),
156            amount,
157            Memos::None,
158        ));
159
160        let child = FungibleSpend::new(
161            source
162                .asset
163                .make_child(SETTLEMENT_PAYMENT_HASH.into(), amount),
164            true,
165        );
166
167        self.items.push(child);
168
169        Ok(Some(self.items.len() - 1))
170    }
171
172    pub fn intermediate_conditions_source(
173        &mut self,
174        ctx: &mut SpendContext,
175        intermediate_puzzle_hash: Bytes32,
176    ) -> Result<Option<usize>, DriverError> {
177        let Some((index, amount)) = self.items.iter().enumerate().find_map(|(index, item)| {
178            item.kind
179                .find_amount(intermediate_puzzle_hash, &item.asset.constraints())
180                .map(|amount| (index, amount))
181        }) else {
182            return Ok(None);
183        };
184
185        let source = &mut self.items[index];
186
187        let hint = ctx.hint(intermediate_puzzle_hash)?;
188
189        source.kind.create_intermediate_coin(CreateCoin::new(
190            intermediate_puzzle_hash,
191            amount,
192            hint,
193        ));
194
195        let child = FungibleSpend::new(
196            source.asset.make_child(intermediate_puzzle_hash, amount),
197            true,
198        );
199
200        self.items.push(child);
201
202        Ok(Some(self.items.len() - 1))
203    }
204
205    pub fn launcher_source(&mut self) -> Result<(usize, u64), DriverError> {
206        let Some((index, amount)) = self.items.iter().enumerate().find_map(|(index, item)| {
207            item.kind
208                .find_amount(SINGLETON_LAUNCHER_HASH.into(), &item.asset.constraints())
209                .map(|amount| (index, amount))
210        }) else {
211            return Err(DriverError::NoSourceForOutput);
212        };
213
214        Ok((index, amount))
215    }
216
217    pub fn create_launcher(
218        &mut self,
219        singleton_amount: u64,
220    ) -> Result<(usize, Launcher), DriverError> {
221        let (index, launcher_amount) = self.launcher_source()?;
222
223        let (create_coin, launcher) =
224            Launcher::create_early(self.items[index].asset.coin_id(), launcher_amount);
225
226        self.items[index].kind.create_intermediate_coin(create_coin);
227
228        Ok((index, launcher.with_singleton_amount(singleton_amount)))
229    }
230
231    pub fn create_option_launcher(
232        &mut self,
233        ctx: &mut SpendContext,
234        singleton_amount: u64,
235        creator_puzzle_hash: Bytes32,
236        seconds: u64,
237        underlying_amount: u64,
238        strike_type: OptionType,
239    ) -> Result<(usize, OptionLauncher), DriverError> {
240        let (index, launcher_amount) = self.launcher_source()?;
241
242        let source = &mut self.items[index];
243
244        let (create_coin, launcher) = OptionLauncher::create_early(
245            ctx,
246            source.asset.coin_id(),
247            launcher_amount,
248            OptionLauncherInfo::new(
249                creator_puzzle_hash,
250                source.asset.p2_puzzle_hash(),
251                seconds,
252                underlying_amount,
253                strike_type,
254            ),
255            singleton_amount,
256        )?;
257
258        source.kind.create_intermediate_coin(create_coin);
259
260        Ok((index, launcher))
261    }
262
263    pub fn create_change(
264        &mut self,
265        ctx: &mut SpendContext,
266        delta: &Delta,
267        change_puzzle_hash: Bytes32,
268    ) -> Result<Option<A>, DriverError> {
269        let change = (self.selected_amount() + delta.input).saturating_sub(delta.output);
270
271        if change == 0 {
272            return Ok(None);
273        }
274
275        let output = Output::new(change_puzzle_hash, change);
276        let source = self.output_source(ctx, &output)?;
277        let item = &mut self.items[source];
278
279        let parent_puzzle_hash = item.asset.full_puzzle_hash();
280        let create_coin = CreateCoin::new(
281            change_puzzle_hash,
282            change,
283            item.asset.child_memos(ctx, change_puzzle_hash)?,
284        );
285        item.kind.create_coin_with_assertion(
286            ctx,
287            parent_puzzle_hash,
288            &mut self.payment_assertions,
289            create_coin,
290        );
291
292        Ok(Some(item.asset.make_child(change_puzzle_hash, change)))
293    }
294}
295
296impl<A> Default for FungibleSpends<A> {
297    fn default() -> Self {
298        Self {
299            items: Vec::new(),
300            payment_assertions: Vec::new(),
301        }
302    }
303}
304
305#[derive(Debug, Clone)]
306pub struct FungibleSpend<T> {
307    pub asset: T,
308    pub kind: SpendKind,
309    pub ephemeral: bool,
310}
311
312impl<T> FungibleSpend<T>
313where
314    T: FungibleAsset,
315{
316    pub fn new(asset: T, ephemeral: bool) -> Self {
317        let kind = if asset.p2_puzzle_hash() == SETTLEMENT_PAYMENT_HASH.into() {
318            SpendKind::settlement()
319        } else {
320            SpendKind::conditions()
321        };
322
323        Self {
324            asset,
325            kind,
326            ephemeral,
327        }
328    }
329}
330
331pub trait FungibleAsset: Clone + Asset {
332    #[must_use]
333    fn make_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self;
334    fn child_memos(
335        &self,
336        ctx: &mut SpendContext,
337        p2_puzzle_hash: Bytes32,
338    ) -> Result<Memos, DriverError>;
339}
340
341impl FungibleAsset for Coin {
342    fn make_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
343        Coin::new(self.coin_id(), p2_puzzle_hash, amount)
344    }
345
346    fn child_memos(
347        &self,
348        _ctx: &mut SpendContext,
349        _p2_puzzle_hash: Bytes32,
350    ) -> Result<Memos, DriverError> {
351        Ok(Memos::None)
352    }
353}
354
355impl FungibleAsset for Cat {
356    fn make_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self {
357        self.child(p2_puzzle_hash, amount)
358    }
359
360    fn child_memos(
361        &self,
362        ctx: &mut SpendContext,
363        p2_puzzle_hash: Bytes32,
364    ) -> Result<Memos, DriverError> {
365        ctx.hint(p2_puzzle_hash)
366    }
367}