datalayer_driver/
dig_collateral_coin.rs

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    /// Morphs a DIG store launcher ID into the DIG store collateral coin namespace.
37    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    /// Morphs a DIG store launcher ID into the DIG mirror collateral coin namespace.
44    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    /// Instantiates a $DIG collateral coin
57    /// Verifies that coin is unspent and locked by the $DIG P2Parent puzzle
58    pub async fn from_coin_state(peer: &Peer, coin_state: CoinState) -> Result<Self, WalletError> {
59        let coin = coin_state.coin;
60
61        // verify coin is unspent
62        if matches!(coin_state.spent_height, Some(x) if x != 0) {
63            return Err(WalletError::CoinIsAlreadySpent);
64        }
65
66        // verify that the coin is $DIG p2 parent
67        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    /// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID)
152    #[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    /// Builds the spend bundle for spending the $DIG collateral coin to de-collateralize
212    /// the store and return spendable $DIG to the wallet that created the collateral coin.
213    #[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        // add the collateral p2 parent spend to the spend context
235        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        // use actions and spends to attach fee to transaction and generate change
241        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        // add fee coins to spends
249        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        // add collateral coins to spends
293        for dig_coin in dig_coins {
294            spends.add(dig_coin.cat());
295        }
296
297        // add fee coins to spends
298        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}