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::{run_puzzle, Condition};
6use clvm_traits::FromClvm;
7use clvmr::{Allocator, NodePtr};
8use indexmap::IndexMap;
9
10use crate::{
11    AddAsset, AssetInfo, Cat, CatAssetInfo, DriverError, HashedPtr, Nft, NftAssetInfo,
12    OfferAmounts, 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<HashedPtr>>,
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) =
164            Nft::<HashedPtr>::parse_child(allocator, parent_coin, parent_puzzle, parent_solution)?
165        {
166            if !spent_coin_ids.contains(&nft.coin.coin_id())
167                && nft.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
168            {
169                self.nfts.insert(nft.info.launcher_id, nft);
170
171                let info = NftAssetInfo::new(
172                    nft.info.metadata,
173                    nft.info.metadata_updater_puzzle_hash,
174                    nft.info.royalty_puzzle_hash,
175                    nft.info.royalty_basis_points,
176                );
177                asset_info.insert_nft(nft.info.launcher_id, info)?;
178            }
179        }
180
181        if let Some(option) =
182            OptionContract::parse_child(allocator, parent_coin, parent_puzzle, parent_solution)?
183        {
184            if !spent_coin_ids.contains(&option.coin.coin_id())
185                && option.info.p2_puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
186            {
187                self.options.insert(option.info.launcher_id, option);
188
189                let info = OptionAssetInfo::new(
190                    option.info.underlying_coin_id,
191                    option.info.underlying_delegated_puzzle_hash,
192                );
193                asset_info.insert_option(option.info.launcher_id, info)?;
194            }
195        }
196
197        let output = run_puzzle(allocator, parent_puzzle.ptr(), parent_solution)?;
198        let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
199
200        for condition in conditions {
201            if let Some(reserve_fee) = condition.as_reserve_fee() {
202                self.fee += reserve_fee.amount;
203            }
204
205            let Some(create_coin) = condition.into_create_coin() else {
206                continue;
207            };
208
209            let coin = Coin::new(
210                parent_coin.coin_id(),
211                create_coin.puzzle_hash,
212                create_coin.amount,
213            );
214
215            if !spent_coin_ids.contains(&coin.coin_id())
216                && coin.puzzle_hash == SETTLEMENT_PAYMENT_HASH.into()
217            {
218                self.xch.push(coin);
219            }
220        }
221
222        Ok(())
223    }
224}
225
226impl AddAsset for OfferCoins {
227    fn add(self, spends: &mut Spends) {
228        for coin in self.xch {
229            spends.add(coin);
230        }
231
232        for cats in self.cats.into_values() {
233            for cat in cats {
234                spends.add(cat);
235            }
236        }
237
238        for nft in self.nfts.into_values() {
239            spends.add(nft);
240        }
241
242        for option in self.options.into_values() {
243            spends.add(option);
244        }
245    }
246}