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::puzzles::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;
16use num_bigint::BigInt;
17
18#[derive(Debug, Clone)]
19pub struct DigCollateralCoin {
20 inner: P2ParentCoin,
21 #[allow(dead_code)]
22 morphed_store_id: Option<Bytes32>,
23 #[allow(dead_code)]
24 mirror_urls: Option<Vec<String>>,
25}
26
27impl DigCollateralCoin {
28 pub fn coin(&self) -> Coin {
29 self.inner.coin
30 }
31
32 pub fn proof(&self) -> LineageProof {
33 self.inner.proof
34 }
35
36 pub fn morph_store_launcher_id_for_collateral(store_launcher_id: Bytes32) -> Bytes32 {
38 (store_launcher_id, "DIG_STORE_COLLATERAL")
39 .tree_hash()
40 .into()
41 }
42
43 pub fn morph_store_launcher_id_for_mirror(
45 store_launcher_id: Bytes32,
46 offset: &BigInt,
47 ) -> Bytes32 {
48 let launcher_id_int = BigInt::from_signed_bytes_be(&store_launcher_id);
49 let offset_launcher_id = launcher_id_int + offset;
50
51 (offset_launcher_id, "DIG_STORE_MIRROR_COLLATERAL")
52 .tree_hash()
53 .into()
54 }
55
56 pub async fn from_coin_state(peer: &Peer, coin_state: CoinState) -> Result<Self, WalletError> {
59 let coin = coin_state.coin;
60
61 if matches!(coin_state.spent_height, Some(x) if x != 0) {
63 return Err(WalletError::CoinIsAlreadySpent);
64 }
65
66 let p2_parent_hash = P2ParentCoin::puzzle_hash(Some(DIG_ASSET_ID));
68 if coin.puzzle_hash != p2_parent_hash.into() {
69 return Err(WalletError::PuzzleHashMismatch(format!(
70 "Coin {} is not locked by the $DIG collateral puzzle",
71 coin.coin_id()
72 )));
73 }
74
75 let Some(created_height) = coin_state.created_height else {
76 return Err(WalletError::UnknownCoin);
77 };
78
79 let parent_state = peer
80 .request_coin_state(
81 vec![coin.parent_coin_info],
82 None,
83 MAINNET_CONSTANTS.genesis_challenge,
84 false,
85 )
86 .await?
87 .map_err(|_| WalletError::RejectCoinState)?
88 .coin_states
89 .first()
90 .copied()
91 .ok_or(WalletError::UnknownCoin)?;
92
93 let parent_puzzle_and_solution_response = peer
94 .request_puzzle_and_solution(coin.parent_coin_info, created_height)
95 .await?
96 .map_err(|_| WalletError::RejectPuzzleSolution)?;
97
98 let mut allocator = Allocator::new();
99 let parent_puzzle_ptr = parent_puzzle_and_solution_response
100 .puzzle
101 .to_clvm(&mut allocator)?;
102 let parent_solution_ptr = parent_puzzle_and_solution_response
103 .solution
104 .to_clvm(&mut allocator)?;
105
106 let parent_puzzle = Puzzle::parse(&allocator, parent_puzzle_ptr);
107
108 let (p2_parent, memos) = P2ParentCoin::parse_child(
109 &mut allocator,
110 parent_state.coin,
111 parent_puzzle,
112 parent_solution_ptr,
113 )?
114 .ok_or(WalletError::Parse(
115 "Failed to instantiate from parent state".to_string(),
116 ))?;
117
118 let memos_vec = match memos {
119 Memos::Some(node) => Vec::<Bytes>::from_clvm(&allocator, node)
120 .ok()
121 .unwrap_or_default(),
122 Memos::None => Vec::new(),
123 };
124
125 let morphed_store_id: Option<Bytes32> = if memos_vec.is_empty() {
126 None
127 } else {
128 Bytes32::from_bytes(&memos_vec[0]).ok()
129 };
130
131 let mut mirror_urls_vec = Vec::new();
132 for memo in memos_vec.iter().skip(1) {
133 if let Ok(url_string) = String::from_utf8(memo.to_vec()) {
134 mirror_urls_vec.push(url_string);
135 }
136 }
137
138 let mirror_urls = if mirror_urls_vec.is_empty() {
139 None
140 } else {
141 Some(mirror_urls_vec)
142 };
143
144 Ok(Self {
145 inner: p2_parent,
146 morphed_store_id,
147 mirror_urls,
148 })
149 }
150
151 #[allow(clippy::result_large_err)]
153 pub fn create_collateral(
154 dig_coins: Vec<DigCoin>,
155 amount: u64,
156 store_id: Bytes32,
157 synthetic_key: PublicKey,
158 fee_coins: Vec<Coin>,
159 fee: u64,
160 ) -> Result<Vec<CoinSpend>, WalletError> {
161 let mut ctx = SpendContext::new();
162
163 let morphed_store_id = Self::morph_store_launcher_id_for_collateral(store_id);
164 let hint = ctx.hint(morphed_store_id)?;
165
166 Self::build_coin_spends(
167 &mut ctx,
168 hint,
169 dig_coins,
170 amount,
171 synthetic_key,
172 fee_coins,
173 fee,
174 )
175 }
176
177 #[allow(clippy::result_large_err, clippy::too_many_arguments)]
178 pub fn create_mirror(
179 dig_coins: Vec<DigCoin>,
180 amount: u64,
181 store_id: Bytes32,
182 mirror_urls: Vec<String>,
183 epoch: BigInt,
184 synthetic_key: PublicKey,
185 fee_coins: Vec<Coin>,
186 fee: u64,
187 ) -> Result<Vec<CoinSpend>, WalletError> {
188 let mut ctx = SpendContext::new();
189 let morphed_store_id = Self::morph_store_launcher_id_for_mirror(store_id, &epoch);
190 let mut memos_vec = Vec::with_capacity(mirror_urls.len() + 1);
191 memos_vec.push(morphed_store_id.to_vec());
192
193 for url in &mirror_urls {
194 memos_vec.push(url.as_bytes().to_vec());
195 }
196
197 let memos_node_ptr = ctx.alloc(&memos_vec)?;
198 let memos = Memos::Some(memos_node_ptr);
199
200 Self::build_coin_spends(
201 &mut ctx,
202 memos,
203 dig_coins,
204 amount,
205 synthetic_key,
206 fee_coins,
207 fee,
208 )
209 }
210
211 #[allow(clippy::result_large_err)]
214 pub fn spend(
215 &self,
216 synthetic_key: PublicKey,
217 fee_coins: Vec<Coin>,
218 fee: u64,
219 ) -> Result<Vec<CoinSpend>, WalletError> {
220 let p2_layer = StandardLayer::new(synthetic_key);
221 let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
222
223 if p2_puzzle_hash != self.inner.proof.parent_inner_puzzle_hash {
224 return Err(WalletError::PuzzleHashMismatch(
225 "Collateral coin controlled by another wallet".to_string(),
226 ));
227 }
228
229 let collateral_spend_conditions =
230 Conditions::new().create_coin(p2_puzzle_hash, self.inner.coin.amount, Memos::None);
231
232 let mut ctx = SpendContext::new();
233
234 let p2_delegated_spend =
236 p2_layer.spend_with_conditions(&mut ctx, collateral_spend_conditions)?;
237
238 self.inner.spend(&mut ctx, p2_delegated_spend, ())?;
239
240 let actions = [Action::fee(fee)];
242 let mut fee_spends = Spends::new(p2_puzzle_hash);
243 fee_spends
244 .conditions
245 .required
246 .push(AssertConcurrentSpend::new(self.inner.coin.coin_id()));
247
248 for fee_xch_coin in fee_coins {
250 fee_spends.add(fee_xch_coin);
251 }
252
253 let deltas = fee_spends.apply(&mut ctx, &actions)?;
254 let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
255
256 let _outputs = fee_spends.finish_with_keys(
257 &mut ctx,
258 &deltas,
259 Relation::AssertConcurrent,
260 &index_map,
261 )?;
262
263 Ok(ctx.take())
264 }
265
266 #[allow(clippy::result_large_err)]
267 fn build_coin_spends(
268 ctx: &mut SpendContext,
269 memos: Memos,
270 dig_coins: Vec<DigCoin>,
271 amount: u64,
272 synthetic_key: PublicKey,
273 fee_coins: Vec<Coin>,
274 fee: u64,
275 ) -> Result<Vec<CoinSpend>, WalletError> {
276 let p2_parent_inner_hash = P2ParentCoin::inner_puzzle_hash(Some(DIG_ASSET_ID));
277
278 let actions = [
279 Action::fee(fee),
280 Action::send(
281 Id::Existing(DIG_ASSET_ID),
282 p2_parent_inner_hash.into(),
283 amount,
284 memos,
285 ),
286 ];
287
288 let p2_layer = StandardLayer::new(synthetic_key);
289 let p2_puzzle_hash: Bytes32 = p2_layer.tree_hash().into();
290 let mut spends = Spends::new(p2_puzzle_hash);
291
292 for dig_coin in dig_coins {
294 spends.add(dig_coin.cat());
295 }
296
297 for fee_xch_coin in fee_coins {
299 spends.add(fee_xch_coin);
300 }
301
302 let deltas = spends.apply(ctx, &actions)?;
303 let index_map = indexmap! {p2_puzzle_hash => synthetic_key};
304
305 let _outputs =
306 spends.finish_with_keys(ctx, &deltas, Relation::AssertConcurrent, &index_map)?;
307
308 Ok(ctx.take())
309 }
310}