chia_sdk_driver/offers/
offer_coins.rs

1use std::collections::HashSet;
2
3use chia_protocol::{Bytes32, Coin};
4use chia_puzzles::SETTLEMENT_PAYMENT_HASH;
5use chia_sdk_types::{Condition, run_puzzle};
6use clvm_traits::FromClvm;
7use clvmr::{Allocator, NodePtr};
8use indexmap::IndexMap;
9
10use crate::{
11    AddAsset, AssetInfo, Cat, CatAssetInfo, DriverError, Nft, NftAssetInfo, OfferAmounts,
12    OptionAssetInfo, OptionContract, Outputs, Puzzle, Spends,
13};
14
15#[derive(Debug, Default, Clone)]
16pub struct OfferCoins {
17    pub xch: Vec<Coin>,
18    pub cats: IndexMap<Bytes32, Vec<Cat>>,
19    pub nfts: IndexMap<Bytes32, Nft>,
20    pub options: IndexMap<Bytes32, OptionContract>,
21    pub fee: u64,
22}
23
24impl OfferCoins {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn from_outputs(outputs: &Outputs) -> Self {
30        let mut coins = Self::default();
31
32        for coin in &outputs.xch {
33            if coin.puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
34                coins.xch.push(*coin);
35            }
36        }
37
38        for cats in outputs.cats.values() {
39            for cat in cats {
40                if cat.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
41                    coins
42                        .cats
43                        .entry(cat.info.asset_id)
44                        .or_insert_with(Vec::new)
45                        .push(*cat);
46                }
47            }
48        }
49
50        for nft in outputs.nfts.values() {
51            if nft.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
52                coins.nfts.insert(nft.info.launcher_id, *nft);
53            }
54        }
55
56        for option in outputs.options.values() {
57            if option.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into() {
58                coins.options.insert(option.info.launcher_id, *option);
59            }
60        }
61
62        coins.fee = outputs.fee;
63
64        coins
65    }
66
67    pub fn amounts(&self) -> OfferAmounts {
68        OfferAmounts {
69            xch: self.xch.iter().map(|c| c.amount).sum(),
70            cats: self
71                .cats
72                .iter()
73                .map(|(&launcher_id, cats)| (launcher_id, cats.iter().map(|c| c.coin.amount).sum()))
74                .collect(),
75        }
76    }
77
78    pub fn flatten(&self) -> Vec<Coin> {
79        let mut coins = self.xch.clone();
80
81        for cats in self.cats.values() {
82            for cat in cats {
83                coins.push(cat.coin);
84            }
85        }
86
87        for nft in self.nfts.values() {
88            coins.push(nft.coin);
89        }
90
91        for option in self.options.values() {
92            coins.push(option.coin);
93        }
94
95        coins
96    }
97
98    pub fn extend(&mut self, other: Self) -> Result<(), DriverError> {
99        for coin in other.xch {
100            if self.xch.iter().any(|c| c.coin_id() == coin.coin_id()) {
101                return Err(DriverError::ConflictingOfferInputs);
102            }
103
104            self.xch.push(coin);
105        }
106
107        for (asset_id, cats) in other.cats {
108            let existing = self.cats.entry(asset_id).or_default();
109
110            for cat in cats {
111                if existing
112                    .iter()
113                    .any(|c| c.coin.coin_id() == cat.coin.coin_id())
114                {
115                    return Err(DriverError::ConflictingOfferInputs);
116                }
117
118                existing.push(cat);
119            }
120        }
121
122        for (launcher_id, nft) in other.nfts {
123            if self.nfts.insert(launcher_id, nft).is_some() {
124                return Err(DriverError::ConflictingOfferInputs);
125            }
126        }
127
128        for (launcher_id, option) in other.options {
129            if self.options.insert(launcher_id, option).is_some() {
130                return Err(DriverError::ConflictingOfferInputs);
131            }
132        }
133
134        self.fee += other.fee;
135
136        Ok(())
137    }
138
139    pub fn parse(
140        &mut self,
141        allocator: &mut Allocator,
142        asset_info: &mut AssetInfo,
143        spent_coin_ids: &HashSet<Bytes32>,
144        parent_coin: Coin,
145        parent_puzzle: Puzzle,
146        parent_solution: NodePtr,
147    ) -> Result<(), DriverError> {
148        if let Some(cats) =
149            Cat::parse_children(allocator, parent_coin, parent_puzzle, parent_solution)?
150        {
151            for cat in cats {
152                if !spent_coin_ids.contains(&cat.coin.coin_id())
153                    && cat.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
154                {
155                    self.cats.entry(cat.info.asset_id).or_default().push(cat);
156                }
157
158                let info = CatAssetInfo::new(cat.info.hidden_puzzle_hash);
159                asset_info.insert_cat(cat.info.asset_id, info)?;
160            }
161        }
162
163        if let Some(nft) = Nft::parse_child(allocator, parent_coin, parent_puzzle, parent_solution)?
164            && !spent_coin_ids.contains(&nft.coin.coin_id())
165            && nft.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
166        {
167            self.nfts.insert(nft.info.launcher_id, nft);
168
169            let info = NftAssetInfo::new(
170                nft.info.metadata,
171                nft.info.metadata_updater_puzzle_hash,
172                nft.info.royalty_puzzle_hash,
173                nft.info.royalty_basis_points,
174            );
175            asset_info.insert_nft(nft.info.launcher_id, info)?;
176        }
177
178        if let Some(option) =
179            OptionContract::parse_child(allocator, parent_coin, parent_puzzle, parent_solution)?
180            && !spent_coin_ids.contains(&option.coin.coin_id())
181            && option.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
182        {
183            self.options.insert(option.info.launcher_id, option);
184
185            let info = OptionAssetInfo::new(
186                option.info.underlying_coin_id,
187                option.info.underlying_delegated_puzzle_hash,
188            );
189            asset_info.insert_option(option.info.launcher_id, info)?;
190        }
191
192        let output = run_puzzle(allocator, parent_puzzle.ptr(), parent_solution)?;
193        let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
194
195        for condition in conditions {
196            if let Some(reserve_fee) = condition.as_reserve_fee() {
197                self.fee += reserve_fee.amount;
198            }
199
200            let Some(create_coin) = condition.into_create_coin() else {
201                continue;
202            };
203
204            let coin = Coin::new(
205                parent_coin.coin_id(),
206                create_coin.puzzle_hash,
207                create_coin.amount,
208            );
209
210            if !spent_coin_ids.contains(&coin.coin_id())
211                && coin.puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
212            {
213                self.xch.push(coin);
214            }
215        }
216
217        Ok(())
218    }
219}
220
221impl AddAsset for OfferCoins {
222    fn add(self, spends: &mut Spends) {
223        for coin in self.xch {
224            spends.add(coin);
225        }
226
227        for cats in self.cats.into_values() {
228            for cat in cats {
229                spends.add(cat);
230            }
231        }
232
233        for nft in self.nfts.into_values() {
234            spends.add(nft);
235        }
236
237        for option in self.options.into_values() {
238            spends.add(option);
239        }
240    }
241}