1use crate::dig_coin::DigCoin;
2use crate::error::WalletError;
3use crate::wallet::DIG_ASSET_ID;
4use crate::{
5 Bytes, Bytes32, Coin, CoinSpend, CoinState, LineageProof, P2ParentCoin, Peer, PublicKey,
6};
7use chia_puzzle_types::Memos;
8use chia_traits::Streamable;
9use chia_wallet_sdk::driver::{
10 Action, Id, Puzzle, Relation, SpendContext, SpendWithConditions, Spends, StandardLayer,
11};
12use chia_wallet_sdk::prelude::{AssertConcurrentSpend, Conditions, ToTreeHash, MAINNET_CONSTANTS};
13use clvm_traits::{FromClvm, ToClvm};
14use clvmr::Allocator;
15use indexmap::indexmap;
16
17#[derive(Debug, Clone)]
39pub struct DigCollateralCoin {
40 inner: P2ParentCoin,
41 #[allow(dead_code)]
42 morphed_store_id: Option<Bytes32>,
43 #[allow(dead_code)]
44 mirror_urls: Option<Vec<String>>,
45}
46
47impl DigCollateralCoin {
48 pub fn coin(&self) -> Coin {
49 self.inner.coin
50 }
51
52 pub fn proof(&self) -> LineageProof {
53 self.inner.proof
54 }
55
56 pub fn morph_store_launcher_id_for_collateral(store_launcher_id: Bytes32) -> Bytes32 {
58 (store_launcher_id, "DIG_STORE_COLLATERAL")
59 .tree_hash()
60 .into()
61 }
62
63 pub async fn from_coin_state(peer: &Peer, coin_state: CoinState) -> Result<Self, WalletError> {
66 let coin = coin_state.coin;
67
68 if matches!(coin_state.spent_height, Some(x) if x != 0) {
70 return Err(WalletError::CoinIsAlreadySpent);
71 }
72
73 let p2_parent_hash = P2ParentCoin::puzzle_hash(Some(DIG_ASSET_ID));
75 if coin.puzzle_hash != p2_parent_hash.into() {
76 return Err(WalletError::PuzzleHashMismatch(format!(
77 "Coin {} is not locked by the $DIG collateral puzzle",
78 coin.coin_id()
79 )));
80 }
81
82 let Some(created_height) = coin_state.created_height else {
83 return Err(WalletError::UnknownCoin);
84 };
85
86 let parent_state = peer
87 .request_coin_state(
88 vec![coin.parent_coin_info],
89 None,
90 MAINNET_CONSTANTS.genesis_challenge,
91 false,
92 )
93 .await?
94 .map_err(|_| WalletError::RejectCoinState)?
95 .coin_states
96 .first()
97 .copied()
98 .ok_or(WalletError::UnknownCoin)?;
99
100 let parent_puzzle_and_solution_response = peer
101 .request_puzzle_and_solution(coin.parent_coin_info, created_height)
102 .await?
103 .map_err(|_| WalletError::RejectPuzzleSolution)?;
104
105 let mut allocator = Allocator::new();
106 let parent_puzzle_ptr = parent_puzzle_and_solution_response
107 .puzzle
108 .to_clvm(&mut allocator)?;
109 let parent_solution_ptr = parent_puzzle_and_solution_response
110 .solution
111 .to_clvm(&mut allocator)?;
112
113 let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr);
114
115 let (p2_parent, memos) = P2ParentCoin::parse_child(
116 &mut allocator,
117 parent_state.coin,
118 parent_puzzle,
119 parent_solution_ptr,
120 )?
121 .ok_or(WalletError::Parse(
122 "Failed to instantiate from parent state".to_string(),
123 ))?;
124
125 let memos_vec = match memos {
126 Memos::Some(node) => Vec::<Bytes>::from_clvm(&allocator, node)
127 .ok()
128 .unwrap_or_default(),
129 Memos::None => Vec::new(),
130 };
131
132 let morphed_store_id: Option<Bytes32> = if memos_vec.is_empty() {
133 None
134 } else {
135 Bytes32::from_bytes(&memos_vec[0]).ok()
136 };
137
138 let mut mirror_urls_vec = Vec::new();
139 for memo in memos_vec.iter().skip(1) {
140 if let Ok(url_string) = String::from_utf8(memo.to_vec()) {
141 mirror_urls_vec.push(url_string);
142 }
143 }
144
145 let mirror_urls = if mirror_urls_vec.is_empty() {
146 None
147 } else {
148 Some(mirror_urls_vec)
149 };
150
151 Ok(Self {
152 inner: p2_parent,
153 morphed_store_id,
154 mirror_urls,
155 })
156 }
157
158 #[allow(clippy::result_large_err)]
160 pub fn create_collateral(
161 dig_coins: Vec<DigCoin>,
162 amount: u64,
163 store_id: Bytes32,
164 synthetic_key: PublicKey,
165 fee_coins: Vec<Coin>,
166 fee: u64,
167 ) -> Result<Vec<CoinSpend>, WalletError> {
168 let mut ctx = SpendContext::new();
169
170 let morphed_store_id = Self::morph_store_launcher_id_for_collateral(store_id);
171 let hint = ctx.hint(morphed_store_id)?;
172
173 Self::build_coin_spends(
174 &mut ctx,
175 hint,
176 dig_coins,
177 amount,
178 synthetic_key,
179 fee_coins,
180 fee,
181 )
182 }
183
184 #[allow(clippy::result_large_err)]
187 pub fn spend(
188 &self,
189 synthetic_key: PublicKey,
190 fee_coins: Vec<Coin>,
191 fee: u64,
192 ) -> Result<Vec<CoinSpend>, WalletError> {
193 let p2_layer = StandardLayer::new(synthetic_key);
194 let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
195
196 if p2_puzzle_hash != self.inner.proof.parent_inner_puzzle_hash {
197 return Err(WalletError::PuzzleHashMismatch(
198 "Collateral coin controlled by another wallet".to_string(),
199 ));
200 }
201
202 let collateral_spend_conditions =
203 Conditions::new().create_coin(p2_puzzle_hash, self.inner.coin.amount, Memos::None);
204
205 let mut ctx = SpendContext::new();
206
207 let p2_delegated_spend =
209 p2_layer.spend_with_conditions(&mut ctx, collateral_spend_conditions)?;
210
211 self.inner.spend(&mut ctx, p2_delegated_spend, ())?;
212
213 let actions = [Action::fee(fee)];
215 let mut fee_spends = Spends::new(p2_puzzle_hash);
216 fee_spends
217 .conditions
218 .required
219 .push(AssertConcurrentSpend::new(self.inner.coin.coin_id()));
220
221 for fee_xch_coin in fee_coins {
223 fee_spends.add(fee_xch_coin);
224 }
225
226 let deltas = fee_spends.apply(&mut ctx, &actions)?;
227 let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
228
229 let _outputs = fee_spends.finish_with_keys(
230 &mut ctx,
231 &deltas,
232 Relation::AssertConcurrent,
233 &index_map,
234 )?;
235
236 Ok(ctx.take())
237 }
238
239 #[allow(clippy::result_large_err)]
240 fn build_coin_spends(
241 ctx: &mut SpendContext,
242 memos: Memos,
243 dig_coins: Vec<DigCoin>,
244 amount: u64,
245 synthetic_key: PublicKey,
246 fee_coins: Vec<Coin>,
247 fee: u64,
248 ) -> Result<Vec<CoinSpend>, WalletError> {
249 let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID));
250
251 let actions = [
252 Action::fee(fee),
253 Action::send(
254 Id::Existing(DIG_ASSET_ID),
255 p2_parent_inner_hash.into(),
256 amount,
257 memos,
258 ),
259 ];
260
261 let p2_layer = StandardLayer::new(synthetic_key);
262 let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
263 let mut spends = Spends::new(p2_puzzle_hash);
264
265 for dig_coin in dig_coins {
267 spends.add(dig_coin.cat());
268 }
269
270 for fee_xch_coin in fee_coins {
272 spends.add(fee_xch_coin);
273 }
274
275 let deltas = spends.apply(ctx, &actions)?;
276 let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
277
278 let _outputs =
279 spends.finish_with_keys(ctx, &deltas, Relation::AssertConcurrent, &index_map)?;
280
281 Ok(ctx.take())
282 }
283}