Skip to main content

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_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/// A `$DIG` collateral coin.
18///
19/// # The mirror-collateral namespace is not served here
20///
21/// This type once also derived and minted **mirror** collateral coins, under the namespace tag
22/// `DIG_STORE_MIRROR_COLLATERAL`, from `morph(store_launcher_id + epoch)`. Both the morph and its
23/// mint path (`morph_store_launcher_id_for_mirror`, `create_mirror`) were removed in 6.0.0.
24///
25/// They were removed rather than renamed because they were not a second namespace that merely
26/// shared a tag — they were the *same* namespace as the canonical one. `dig-mirror-coin` hints a
27/// mirror coin under `morph(store + root + owner + epoch)` beneath that identical tag, and because
28/// both morphs hash an additive sum, the extra terms are absorbed rather than separating: an author
29/// who freely chooses the epoch can solve `e' = store + epoch - store' - root' - owner'` and land a
30/// coin bonding their own store and root exactly on a hint derived here. `dig-mirror-coin` closes
31/// that one level up, by having the coin *declare* its four terms and checking the declaration as
32/// well as the recompute; the two-term form had no such check and no way to add one, since the
33/// epoch it was built with is not recoverable from the hint.
34///
35/// Mirror collateral therefore has a single owner: use the `dig-mirror-coin` crate. Coins already
36/// minted through the removed path are unaffected — [`Self::from_coin_state`] reads the morphed id
37/// out of the coin's memos and never recomputes it, and [`Self::spend`] does not use the hint.
38#[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    /// Morphs a DIG store launcher ID into the DIG store collateral coin namespace.
57    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    /// Instantiates a $DIG collateral coin
64    /// Verifies that coin is unspent and locked by the $DIG P2Parent puzzle
65    pub async fn from_coin_state(peer: &Peer, coin_state: CoinState) -> Result<Self, WalletError> {
66        let coin = coin_state.coin;
67
68        // verify coin is unspent
69        if matches!(coin_state.spent_height, Some(x) if x != 0) {
70            return Err(WalletError::CoinIsAlreadySpent);
71        }
72
73        // verify that the coin is $DIG p2 parent
74        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    /// Uses the specified $DIG to create a collateral coin for the provided DIG store ID (launcher ID)
159    #[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    /// Builds the spend bundle for spending the $DIG collateral coin to de-collateralize
185    /// the store and return spendable $DIG to the wallet that created the collateral coin.
186    #[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        // add the collateral p2 parent spend to the spend context
208        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        // use actions and spends to attach fee to transaction and generate change
214        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        // add fee coins to spends
222        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        // add collateral coins to spends
266        for dig_coin in dig_coins {
267            spends.add(dig_coin.cat());
268        }
269
270        // add fee coins to spends
271        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}