chia_sdk_driver/offers/
offer_coins.rs1use 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}