Skip to main content

chia_sdk_driver/action_system/
spends.rs

1use std::{collections::HashMap, mem};
2
3use chia_bls::PublicKey;
4use chia_protocol::{Bytes32, Coin};
5use chia_puzzle_types::offer::SettlementPaymentsSolution;
6use chia_sdk_types::{Conditions, conditions::AssertPuzzleAnnouncement};
7use indexmap::IndexMap;
8
9use crate::{
10    Action, Asset, Cat, CatSpend, ConditionsSpend, Delta, Deltas, Did, DriverError, FungibleSpend,
11    FungibleSpends, Id, Layer, Nft, OptionContract, Relation, SettlementLayer, SingletonSpends,
12    Spend, SpendAction, SpendContext, SpendKind, SpendWithConditions, SpendableAsset,
13    StandardLayer,
14};
15
16#[derive(Debug, Clone)]
17#[must_use]
18pub struct Spends<S = Unfinished> {
19    pub xch: FungibleSpends<Coin>,
20    pub cats: IndexMap<Id, FungibleSpends<Cat>>,
21    pub dids: IndexMap<Id, SingletonSpends<Did>>,
22    pub nfts: IndexMap<Id, SingletonSpends<Nft>>,
23    pub options: IndexMap<Id, SingletonSpends<OptionContract>>,
24    pub intermediate_puzzle_hash: Bytes32,
25    pub change_puzzle_hash: Bytes32,
26    pub outputs: Outputs,
27    pub conditions: ConditionConfig,
28    _state: S,
29}
30
31#[derive(Debug, Default, Clone)]
32pub struct ConditionConfig {
33    pub optional: Conditions,
34    pub required: Conditions,
35    pub disable_settlement_assertions: bool,
36}
37
38#[derive(Debug, Default, Clone)]
39pub struct Outputs {
40    pub xch: Vec<Coin>,
41    pub cats: IndexMap<Id, Vec<Cat>>,
42    pub dids: IndexMap<Id, Did>,
43    pub nfts: IndexMap<Id, Nft>,
44    pub options: IndexMap<Id, OptionContract>,
45    pub fee: u64,
46    pub reserved_fee: u64,
47}
48
49#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
50pub struct Unfinished;
51
52#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct Finished;
54
55impl Spends<Unfinished> {
56    pub fn new(change_puzzle_hash: Bytes32) -> Self {
57        Self::with_separate_change_puzzle_hash(change_puzzle_hash, change_puzzle_hash)
58    }
59
60    pub fn with_separate_change_puzzle_hash(
61        intermediate_puzzle_hash: Bytes32,
62        change_puzzle_hash: Bytes32,
63    ) -> Self {
64        Self {
65            xch: FungibleSpends::new(),
66            cats: IndexMap::new(),
67            dids: IndexMap::new(),
68            nfts: IndexMap::new(),
69            options: IndexMap::new(),
70            intermediate_puzzle_hash,
71            change_puzzle_hash,
72            outputs: Outputs::default(),
73            conditions: ConditionConfig::default(),
74            _state: Unfinished,
75        }
76    }
77
78    pub fn add(&mut self, asset: impl AddAsset) {
79        asset.add(self);
80    }
81
82    pub fn apply(
83        &mut self,
84        ctx: &mut SpendContext,
85        actions: &[Action],
86    ) -> Result<Deltas, DriverError> {
87        let deltas = Deltas::from_actions(actions);
88        for (index, action) in actions.iter().enumerate() {
89            action.spend(ctx, self, index)?;
90        }
91        Ok(deltas)
92    }
93
94    fn create_change(
95        &mut self,
96        ctx: &mut SpendContext,
97        deltas: &Deltas,
98    ) -> Result<(), DriverError> {
99        if let Some(change) = self.xch.create_change(
100            ctx,
101            deltas.get(&Id::Xch).unwrap_or(&Delta::default()),
102            self.change_puzzle_hash,
103        )? {
104            self.outputs.xch.push(change);
105        }
106
107        for (&id, cat) in &mut self.cats {
108            if let Some(change) = cat.create_change(
109                ctx,
110                deltas.get(&id).unwrap_or(&Delta::default()),
111                self.change_puzzle_hash,
112            )? {
113                self.outputs.cats.entry(id).or_default().push(change);
114            }
115        }
116
117        for (&id, did) in &mut self.dids {
118            if let Some(change) =
119                did.finalize(ctx, self.intermediate_puzzle_hash, self.change_puzzle_hash)?
120            {
121                self.outputs.dids.insert(id, change);
122            }
123        }
124
125        for (&id, nft) in &mut self.nfts {
126            if let Some(change) =
127                nft.finalize(ctx, self.intermediate_puzzle_hash, self.change_puzzle_hash)?
128            {
129                self.outputs.nfts.insert(id, change);
130            }
131        }
132
133        for (&id, option) in &mut self.options {
134            if let Some(change) =
135                option.finalize(ctx, self.intermediate_puzzle_hash, self.change_puzzle_hash)?
136            {
137                self.outputs.options.insert(id, change);
138            }
139        }
140
141        Ok(())
142    }
143
144    fn payment_assertions(&self) -> Vec<AssertPuzzleAnnouncement> {
145        let mut payment_assertions = self.xch.payment_assertions.clone();
146
147        for cat in self.cats.values() {
148            payment_assertions.extend_from_slice(&cat.payment_assertions);
149        }
150
151        for did in self.dids.values() {
152            for item in &did.lineage {
153                payment_assertions.extend_from_slice(&item.payment_assertions);
154            }
155        }
156
157        for nft in self.nfts.values() {
158            for item in &nft.lineage {
159                payment_assertions.extend_from_slice(&item.payment_assertions);
160            }
161        }
162
163        for option in self.options.values() {
164            for item in &option.lineage {
165                payment_assertions.extend_from_slice(&item.payment_assertions);
166            }
167        }
168
169        payment_assertions
170    }
171
172    fn iter_conditions_spends(&mut self) -> impl Iterator<Item = (Coin, &mut ConditionsSpend)> {
173        self.xch
174            .items
175            .iter_mut()
176            .filter_map(|item| {
177                if let SpendKind::Conditions(spend) = &mut item.kind {
178                    Some((item.asset, spend))
179                } else {
180                    None
181                }
182            })
183            .chain(self.cats.values_mut().filter_map(|cat| {
184                cat.items.iter_mut().find_map(|item| {
185                    if let SpendKind::Conditions(spend) = &mut item.kind {
186                        Some((item.asset.coin, spend))
187                    } else {
188                        None
189                    }
190                })
191            }))
192            .chain(self.dids.values_mut().filter_map(|did| {
193                did.lineage
194                    .iter_mut()
195                    .filter_map(|item| {
196                        if let SpendKind::Conditions(spend) = &mut item.kind {
197                            Some((item.asset.coin, spend))
198                        } else {
199                            None
200                        }
201                    })
202                    .last()
203            }))
204            .chain(self.nfts.values_mut().filter_map(|nft| {
205                nft.lineage
206                    .iter_mut()
207                    .filter_map(|item| {
208                        if let SpendKind::Conditions(spend) = &mut item.kind {
209                            Some((item.asset.coin, spend))
210                        } else {
211                            None
212                        }
213                    })
214                    .last()
215            }))
216            .chain(self.options.values_mut().filter_map(|option| {
217                option
218                    .lineage
219                    .iter_mut()
220                    .filter_map(|item| {
221                        if let SpendKind::Conditions(spend) = &mut item.kind {
222                            Some((item.asset.coin, spend))
223                        } else {
224                            None
225                        }
226                    })
227                    .last()
228            }))
229    }
230
231    fn emit_conditions(&mut self, ctx: &mut SpendContext) -> Result<(), DriverError> {
232        let mut conditions = self.conditions.required.clone().extend(
233            if self.conditions.disable_settlement_assertions {
234                vec![]
235            } else {
236                self.payment_assertions()
237            },
238        );
239
240        let required = !conditions.is_empty();
241
242        conditions = conditions.extend(self.conditions.optional.clone());
243
244        if self.outputs.reserved_fee > 0 {
245            conditions = conditions.reserve_fee(self.outputs.reserved_fee);
246        }
247
248        for (_, spend) in self.iter_conditions_spends() {
249            spend.add_conditions(mem::take(&mut conditions));
250        }
251
252        if conditions.is_empty() || !required {
253            return Ok(());
254        }
255
256        if let Some(index) = self
257            .xch
258            .intermediate_conditions_source(ctx, self.intermediate_puzzle_hash)?
259        {
260            match &mut self.xch.items[index].kind {
261                SpendKind::Conditions(spend) => {
262                    spend.add_conditions(mem::take(&mut conditions));
263                }
264                SpendKind::Settlement(_) => {}
265            }
266        }
267
268        for cat in self.cats.values_mut() {
269            if let Some(index) =
270                cat.intermediate_conditions_source(ctx, self.intermediate_puzzle_hash)?
271            {
272                match &mut cat.items[index].kind {
273                    SpendKind::Conditions(spend) => {
274                        spend.add_conditions(mem::take(&mut conditions));
275                    }
276                    SpendKind::Settlement(_) => {}
277                }
278            }
279        }
280
281        for did in self.dids.values_mut() {
282            if let Some(mut item) =
283                did.intermediate_fungible_xch_spend(ctx, self.intermediate_puzzle_hash)?
284            {
285                match &mut item.kind {
286                    SpendKind::Conditions(spend) => {
287                        spend.add_conditions(mem::take(&mut conditions));
288                    }
289                    SpendKind::Settlement(_) => {}
290                }
291                self.xch.items.push(item);
292            }
293        }
294
295        for nft in self.nfts.values_mut() {
296            if let Some(mut item) =
297                nft.intermediate_fungible_xch_spend(ctx, self.intermediate_puzzle_hash)?
298            {
299                match &mut item.kind {
300                    SpendKind::Conditions(spend) => {
301                        spend.add_conditions(mem::take(&mut conditions));
302                    }
303                    SpendKind::Settlement(_) => {}
304                }
305                self.xch.items.push(item);
306            }
307        }
308
309        for option in self.options.values_mut() {
310            if let Some(mut item) =
311                option.intermediate_fungible_xch_spend(ctx, self.intermediate_puzzle_hash)?
312            {
313                match &mut item.kind {
314                    SpendKind::Conditions(spend) => {
315                        spend.add_conditions(mem::take(&mut conditions));
316                    }
317                    SpendKind::Settlement(_) => {}
318                }
319                self.xch.items.push(item);
320            }
321        }
322
323        if conditions.is_empty() {
324            Ok(())
325        } else {
326            Err(DriverError::CannotEmitConditions)
327        }
328    }
329
330    fn emit_relation(&mut self, relation: Relation) {
331        match relation {
332            Relation::None => {}
333            Relation::AssertConcurrent => {
334                let coin_ids: Vec<Bytes32> = self
335                    .iter_conditions_spends()
336                    .map(|(coin, _)| coin.coin_id())
337                    .collect();
338
339                if coin_ids.len() <= 1 {
340                    return;
341                }
342
343                self.iter_conditions_spends()
344                    .enumerate()
345                    .for_each(|(i, (_, spend))| {
346                        spend.add_conditions(Conditions::new().assert_concurrent_spend(
347                            if i == 0 {
348                                coin_ids[coin_ids.len() - 1]
349                            } else {
350                                coin_ids[i - 1]
351                            },
352                        ));
353                    });
354            }
355        }
356    }
357
358    pub fn p2_puzzle_hashes(&self) -> Vec<Bytes32> {
359        let mut p2_puzzle_hashes = vec![self.intermediate_puzzle_hash];
360
361        for item in &self.xch.items {
362            p2_puzzle_hashes.push(item.asset.p2_puzzle_hash());
363        }
364
365        for (_, cat) in &self.cats {
366            for item in &cat.items {
367                p2_puzzle_hashes.push(item.asset.p2_puzzle_hash());
368            }
369        }
370
371        for (_, did) in &self.dids {
372            for item in &did.lineage {
373                p2_puzzle_hashes.push(item.asset.p2_puzzle_hash());
374            }
375        }
376
377        for (_, nft) in &self.nfts {
378            for item in &nft.lineage {
379                p2_puzzle_hashes.push(item.asset.p2_puzzle_hash());
380            }
381        }
382
383        for (_, option) in &self.options {
384            for item in &option.lineage {
385                p2_puzzle_hashes.push(item.asset.p2_puzzle_hash());
386            }
387        }
388
389        p2_puzzle_hashes
390    }
391
392    pub fn non_settlement_coin_ids(&self) -> Vec<Bytes32> {
393        let mut coin_ids = Vec::new();
394
395        for item in &self.xch.items {
396            if item.kind.is_conditions() {
397                coin_ids.push(item.asset.coin_id());
398            }
399        }
400
401        for (_, cat) in &self.cats {
402            for item in &cat.items {
403                if item.kind.is_conditions() {
404                    coin_ids.push(item.asset.coin_id());
405                }
406            }
407        }
408
409        for (_, did) in &self.dids {
410            for item in &did.lineage {
411                if item.kind.is_conditions() {
412                    coin_ids.push(item.asset.coin_id());
413                }
414            }
415        }
416
417        for (_, nft) in &self.nfts {
418            for item in &nft.lineage {
419                if item.kind.is_conditions() {
420                    coin_ids.push(item.asset.coin_id());
421                }
422            }
423        }
424
425        for (_, option) in &self.options {
426            for item in &option.lineage {
427                if item.kind.is_conditions() {
428                    coin_ids.push(item.asset.coin_id());
429                }
430            }
431        }
432
433        coin_ids
434    }
435
436    pub fn prepare(
437        mut self,
438        ctx: &mut SpendContext,
439        deltas: &Deltas,
440        relation: Relation,
441    ) -> Result<Spends<Finished>, DriverError> {
442        self.create_change(ctx, deltas)?;
443        self.emit_conditions(ctx)?;
444        self.emit_relation(relation);
445
446        Ok(Spends {
447            xch: self.xch,
448            cats: self.cats,
449            dids: self.dids,
450            nfts: self.nfts,
451            options: self.options,
452            intermediate_puzzle_hash: self.intermediate_puzzle_hash,
453            change_puzzle_hash: self.change_puzzle_hash,
454            outputs: self.outputs,
455            conditions: self.conditions,
456            _state: Finished,
457        })
458    }
459
460    pub fn finish_with_keys(
461        self,
462        ctx: &mut SpendContext,
463        deltas: &Deltas,
464        relation: Relation,
465        synthetic_keys: &IndexMap<Bytes32, PublicKey>,
466    ) -> Result<Outputs, DriverError> {
467        let spends = self.prepare(ctx, deltas, relation)?;
468        let mut coin_spends = HashMap::new();
469
470        for (asset, kind) in spends.unspent() {
471            match kind {
472                SpendKind::Conditions(spend) => {
473                    let Some(&synthetic_key) = synthetic_keys.get(&asset.p2_puzzle_hash()) else {
474                        return Err(DriverError::MissingKey);
475                    };
476                    coin_spends.insert(
477                        asset.coin().coin_id(),
478                        StandardLayer::new(synthetic_key)
479                            .spend_with_conditions(ctx, spend.finish())?,
480                    );
481                }
482                SpendKind::Settlement(spend) => {
483                    coin_spends.insert(
484                        asset.coin().coin_id(),
485                        SettlementLayer.construct_spend(
486                            ctx,
487                            SettlementPaymentsSolution::new(spend.finish()),
488                        )?,
489                    );
490                }
491            }
492        }
493
494        spends.spend(ctx, coin_spends)
495    }
496}
497
498impl Spends<Finished> {
499    pub fn unspent(&self) -> Vec<(SpendableAsset, SpendKind)> {
500        let mut result = Vec::new();
501
502        for item in &self.xch.items {
503            result.push((SpendableAsset::Xch(item.asset), item.kind.clone()));
504        }
505
506        for cat in self.cats.values() {
507            for item in &cat.items {
508                result.push((SpendableAsset::Cat(item.asset), item.kind.clone()));
509            }
510        }
511
512        for did in self.dids.values() {
513            for item in &did.lineage {
514                result.push((SpendableAsset::Did(item.asset), item.kind.clone()));
515            }
516        }
517
518        for nft in self.nfts.values() {
519            for item in &nft.lineage {
520                result.push((SpendableAsset::Nft(item.asset), item.kind.clone()));
521            }
522        }
523
524        for option in self.options.values() {
525            for item in &option.lineage {
526                result.push((SpendableAsset::Option(item.asset), item.kind.clone()));
527            }
528        }
529
530        result
531    }
532
533    pub fn spend(
534        self,
535        ctx: &mut SpendContext,
536        mut coin_spends: HashMap<Bytes32, Spend>,
537    ) -> Result<Outputs, DriverError> {
538        for item in self.xch.items {
539            let spend = coin_spends
540                .remove(&item.asset.coin_id())
541                .ok_or(DriverError::MissingSpend)?;
542            ctx.spend(item.asset, spend)?;
543        }
544
545        for cat in self.cats.into_values() {
546            let mut cat_spends = Vec::new();
547            for item in cat.items {
548                let spend = coin_spends
549                    .remove(&item.asset.coin_id())
550                    .ok_or(DriverError::MissingSpend)?;
551                cat_spends.push(CatSpend::new(item.asset, spend));
552            }
553            Cat::spend_all(ctx, &cat_spends)?;
554        }
555
556        for did in self.dids.into_values() {
557            for item in did.lineage {
558                let spend = coin_spends
559                    .remove(&item.asset.coin_id())
560                    .ok_or(DriverError::MissingSpend)?;
561                item.asset.spend(ctx, spend)?;
562            }
563        }
564
565        for nft in self.nfts.into_values() {
566            for item in nft.lineage {
567                let spend = coin_spends
568                    .remove(&item.asset.coin_id())
569                    .ok_or(DriverError::MissingSpend)?;
570                let _nft = item.asset.spend(ctx, spend)?;
571            }
572        }
573
574        for option in self.options.into_values() {
575            for item in option.lineage {
576                let spend = coin_spends
577                    .remove(&item.asset.coin_id())
578                    .ok_or(DriverError::MissingSpend)?;
579                let _option = item.asset.spend(ctx, spend)?;
580            }
581        }
582
583        Ok(self.outputs)
584    }
585}
586
587pub trait AddAsset {
588    fn add(self, spends: &mut Spends);
589}
590
591impl AddAsset for Coin {
592    fn add(self, spends: &mut Spends) {
593        spends.xch.items.push(FungibleSpend::new(self, false));
594    }
595}
596
597impl AddAsset for Cat {
598    fn add(self, spends: &mut Spends) {
599        spends
600            .cats
601            .entry(Id::Existing(self.info.asset_id))
602            .or_default()
603            .items
604            .push(FungibleSpend::new(self, false));
605    }
606}
607
608impl AddAsset for Did {
609    fn add(self, spends: &mut Spends) {
610        spends.dids.insert(
611            Id::Existing(self.info.launcher_id),
612            SingletonSpends::new(self, false),
613        );
614    }
615}
616
617impl AddAsset for Nft {
618    fn add(self, spends: &mut Spends) {
619        spends.nfts.insert(
620            Id::Existing(self.info.launcher_id),
621            SingletonSpends::new(self, false),
622        );
623    }
624}
625
626impl AddAsset for OptionContract {
627    fn add(self, spends: &mut Spends) {
628        spends.options.insert(
629            Id::Existing(self.info.launcher_id),
630            SingletonSpends::new(self, false),
631        );
632    }
633}