dig_store_coin/
lib.rs

1use datalayer_driver::async_api::get_all_unspent_coins;
2use datalayer_driver::types::TransactionAck;
3use datalayer_driver::wallet::{broadcast_spend_bundle, get_unspent_coin_states_by_hint};
4use datalayer_driver::{
5    Bytes32, Coin, DIG_MIN_HEIGHT, DataStore, DataStoreInnerSpend, DigCollateralCoin, NetworkType,
6    PublicKey, SpendBundle, SuccessResponse, add_fee, admin_delegated_puzzle_from_key,
7    connect_random, get_fee_estimate, get_header_hash, mint_store, oracle_delegated_puzzle,
8    secret_key_to_public_key, select_coins, sign_coin_spends, synthetic_key_to_puzzle_hash,
9    update_store_metadata, writer_delegated_puzzle_from_key,
10};
11use dig_wallet::Wallet;
12use num_bigint::BigInt;
13use std::cmp::Reverse;
14
15/// 0.005 $DIG per MB. 1000 mojos per $DIG
16pub const DIG_MOJO_COLLATERAL_PER_MB: f64 = 5_f64;
17
18pub fn calc_required_collateral(archive_file_size_bytes: u64) -> u64 {
19    ((archive_file_size_bytes as f64 / 1000_f64) * DIG_MOJO_COLLATERAL_PER_MB).round() as u64
20}
21
22#[derive(Debug, Clone)]
23pub struct MintParams<'mp> {
24    pub ssl_cert_path: &'mp str,
25    pub ssl_key_path: &'mp str,
26    pub wallet: &'mp Wallet,
27    pub root_hash: Bytes32,
28    pub network: NetworkType,
29    pub previous_height: Option<u32>,
30    pub label: Option<String>,
31    pub description: Option<String>,
32    pub size_in_bytes: Option<u64>,
33    pub size_proof: Option<String>,
34    pub writer_public_synthetic_key: Option<PublicKey>,
35    pub admin_public_synthetic_key: Option<PublicKey>,
36    pub fee: Option<u64>,
37}
38
39pub async fn mint(params: MintParams<'_>) -> datalayer_driver::Result<DataStore> {
40    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
41
42    let owner_secret_key = params.wallet.get_private_synthetic_key().await?;
43    let owner_public_key = secret_key_to_public_key(&owner_secret_key);
44    let owner_puzzle_hash = synthetic_key_to_puzzle_hash(&owner_public_key);
45
46    let height = if let Some(specified_height) = params.previous_height {
47        specified_height
48    } else {
49        DIG_MIN_HEIGHT
50    };
51    let owner_previous_header_hash = get_header_hash(&peer, height).await?;
52
53    let mut delegated_puzzles = vec![oracle_delegated_puzzle(owner_puzzle_hash, 100_000)];
54
55    if let Some(key) = params.admin_public_synthetic_key {
56        delegated_puzzles.push(admin_delegated_puzzle_from_key(&key))
57    }
58
59    if let Some(key) = params.writer_public_synthetic_key {
60        delegated_puzzles.push(writer_delegated_puzzle_from_key(&key))
61    }
62
63    let fee = if let Some(specified_fee) = params.fee {
64        specified_fee
65    } else {
66        get_fee_estimate(&peer, 60).await?
67    };
68    let required_wallet_balance = fee + 1;
69
70    let unspent_coin_states = get_all_unspent_coins(
71        &peer,
72        owner_puzzle_hash,
73        Some(height),
74        owner_previous_header_hash,
75    )
76    .await?;
77
78    let unspent_coins = unspent_coin_states
79        .coin_states
80        .iter()
81        .map(|coin_state| coin_state.coin)
82        .collect::<Vec<Coin>>();
83    let selected_coins = select_coins(&unspent_coins, required_wallet_balance)?;
84
85    let SuccessResponse {
86        coin_spends,
87        new_datastore,
88    } = mint_store(
89        owner_public_key,
90        selected_coins,
91        params.root_hash,
92        params.label,
93        params.description,
94        params.size_in_bytes,
95        params.size_proof,
96        owner_puzzle_hash,
97        delegated_puzzles,
98        fee,
99    )?;
100
101    let signature = sign_coin_spends(
102        &coin_spends,
103        &[owner_secret_key],
104        params.network == NetworkType::Testnet11,
105    )?;
106    let spend_bundle = SpendBundle::new(coin_spends, signature);
107    broadcast_spend_bundle(&peer, spend_bundle).await?;
108
109    Ok(new_datastore)
110}
111
112#[derive(Debug, Clone)]
113pub struct UpdateParams<'up> {
114    pub ssl_cert_path: &'up str,
115    pub ssl_key_path: &'up str,
116    pub wallet: &'up Wallet,
117    pub data_store: DataStore,
118    pub new_root_hash: Bytes32,
119    pub network: NetworkType,
120    pub previous_height: Option<u32>,
121    pub new_label: Option<String>,
122    pub new_description: Option<String>,
123    pub new_size_in_bytes: Option<u64>,
124    pub new_size_proof: Option<String>,
125    pub admin_or_writer_public_key: Option<DataStoreInnerSpend>,
126    pub fee: Option<u64>,
127}
128
129pub async fn update(params: UpdateParams<'_>) -> datalayer_driver::Result<DataStore> {
130    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
131
132    let owner_secret_key = params.wallet.get_private_synthetic_key().await?;
133    let owner_public_key = secret_key_to_public_key(&owner_secret_key);
134    let inner_spend = if let Some(key) = params.admin_or_writer_public_key {
135        key
136    } else {
137        DataStoreInnerSpend::Owner(owner_public_key)
138    };
139
140    let SuccessResponse {
141        coin_spends: update_store_coin_spends,
142        new_datastore: updated_datastore,
143    } = update_store_metadata(
144        params.data_store,
145        params.new_root_hash,
146        params.new_label,
147        params.new_description,
148        params.new_size_in_bytes,
149        params.new_size_proof,
150        inner_spend,
151    )?;
152
153    let fee = if let Some(specified_fee) = params.fee {
154        specified_fee
155    } else {
156        get_fee_estimate(&peer, 60).await?
157    };
158
159    let height = if let Some(specified_height) = params.previous_height {
160        specified_height
161    } else {
162        DIG_MIN_HEIGHT
163    };
164    let owner_puzzle_hash = synthetic_key_to_puzzle_hash(&owner_public_key);
165    let owner_previous_header_hash = get_header_hash(&peer, height).await?;
166
167    let unspent_coin_states = get_all_unspent_coins(
168        &peer,
169        owner_puzzle_hash,
170        Some(height),
171        owner_previous_header_hash,
172    )
173    .await?;
174
175    let unspent_coins = unspent_coin_states
176        .coin_states
177        .iter()
178        .map(|coin_state| coin_state.coin)
179        .collect::<Vec<Coin>>();
180    let selected_coins = select_coins(&unspent_coins, fee)?;
181
182    let coin_ids: Vec<Bytes32> = selected_coins.iter().map(|coin| coin.coin_id()).collect();
183    let mut coin_spends = add_fee(&owner_public_key, &selected_coins, &coin_ids, fee)?;
184    coin_spends.extend(update_store_coin_spends);
185
186    let signature = sign_coin_spends(
187        &coin_spends,
188        &[owner_secret_key],
189        params.network == NetworkType::Testnet11,
190    )?;
191    let spend_bundle = SpendBundle::new(coin_spends, signature);
192    broadcast_spend_bundle(&peer, spend_bundle).await?;
193
194    Ok(updated_datastore)
195}
196
197#[derive(Debug, Clone)]
198pub struct CollateralizeStoreParams<'cp> {
199    pub ssl_cert_path: &'cp str,
200    pub ssl_key_path: &'cp str,
201    pub wallet: &'cp Wallet,
202    pub store_id: Bytes32,
203    pub archive_file_size_bytes: u64,
204    pub network: NetworkType,
205    pub fee: Option<u64>,
206    pub verbose: bool,
207}
208
209pub async fn collateralize_store(
210    params: CollateralizeStoreParams<'_>,
211) -> datalayer_driver::Result<TransactionAck> {
212    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
213    let private_key = params.wallet.get_private_synthetic_key().await?;
214    let public_key = params.wallet.get_public_synthetic_key().await?;
215
216    let fee = if let Some(specified_fee) = params.fee {
217        specified_fee
218    } else {
219        get_fee_estimate(&peer, 60).await?
220    };
221
222    let required_dig_collateral_amount = calc_required_collateral(params.archive_file_size_bytes);
223
224    let dig_cats = params
225        .wallet
226        .select_unspent_dig_coins(
227            &peer,
228            required_dig_collateral_amount,
229            vec![],
230            params.verbose,
231        )
232        .await?;
233    let xch_fee_coins = params
234        .wallet
235        .select_unspent_coins(&peer, 0, fee, vec![])
236        .await?;
237
238    let create_collateral_spends = DigCollateralCoin::create_collateral(
239        dig_cats,
240        required_dig_collateral_amount,
241        params.store_id,
242        public_key,
243        xch_fee_coins,
244        fee,
245    )?;
246    let coin_spend_signature = sign_coin_spends(
247        &create_collateral_spends,
248        &[private_key],
249        params.network != NetworkType::Mainnet,
250    )?;
251    let spend_bundle = SpendBundle::new(create_collateral_spends, coin_spend_signature);
252
253    Ok(broadcast_spend_bundle(&peer, spend_bundle).await?)
254}
255
256#[derive(Debug, Clone)]
257pub struct CreateMirrorParams<'mp> {
258    pub ssl_cert_path: &'mp str,
259    pub ssl_key_path: &'mp str,
260    pub wallet: &'mp Wallet,
261    pub store_id: Bytes32,
262    pub epoch: BigInt,
263    pub amount: u64,
264    pub mirror_urls: Vec<String>,
265    pub network: NetworkType,
266    pub fee: Option<u64>,
267    pub verbose: bool,
268}
269
270/// Create a mirror coin for a store with memo URLs. Requires the same $DIG collateral as the store size.
271pub async fn create_store_mirror(
272    params: CreateMirrorParams<'_>,
273) -> datalayer_driver::Result<TransactionAck> {
274    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
275    let private_key = params.wallet.get_private_synthetic_key().await?;
276    let public_key = params.wallet.get_public_synthetic_key().await?;
277
278    let fee = if let Some(specified_fee) = params.fee { specified_fee } else { get_fee_estimate(&peer, 60).await? };
279
280    let dig_cats = params
281        .wallet
282        .select_unspent_dig_coins(
283            &peer,
284            params.amount,
285            vec![],
286            params.verbose,
287        )
288        .await?;
289
290    let xch_fee_coins = params
291        .wallet
292        .select_unspent_coins(&peer, 0, fee, vec![])
293        .await?;
294
295    let mirror_spends = DigCollateralCoin::create_mirror(
296        dig_cats,
297        params.amount,
298        params.store_id,
299        params.mirror_urls,
300        params.epoch,
301        public_key,
302        xch_fee_coins,
303        fee,
304    )?;
305
306    let coin_spend_signature = sign_coin_spends(
307        &mirror_spends,
308        &[private_key],
309        params.network != NetworkType::Mainnet,
310    )?;
311    let spend_bundle = SpendBundle::new(mirror_spends, coin_spend_signature);
312    Ok(broadcast_spend_bundle(&peer, spend_bundle).await?)
313}
314
315#[derive(Debug, Clone)]
316pub struct CheckCollateralizationParams<'ckp> {
317    pub ssl_cert_path: &'ckp str,
318    pub ssl_key_path: &'ckp str,
319    pub wallet: &'ckp Wallet,
320    pub store_id: Bytes32,
321    pub archive_file_size_bytes: u64,
322    pub network: NetworkType,
323}
324
325/// fetches and returns the highest value valid collateral coin, if it exists.
326/// returns None if there are no coins hinted for the store ID, or if no hinted coins have sufficient value
327pub async fn check_store_collateralization(
328    params: CheckCollateralizationParams<'_>,
329) -> datalayer_driver::Result<Option<DigCollateralCoin>> {
330    let wallet_puzzle_hash = params.wallet.get_owner_puzzle_hash().await?;
331    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
332    let required_dig_collateral_amount = calc_required_collateral(params.archive_file_size_bytes);
333
334    let morphed_store_launcher_id =
335        DigCollateralCoin::morph_store_launcher_id_for_collateral(params.store_id);
336    // filter coins for amount
337    let mut maybe_collateral_coin_states =
338        get_unspent_coin_states_by_hint(&peer, morphed_store_launcher_id, params.network)
339            .await?
340            .coin_states
341            .into_iter()
342            .filter(|coin_state| coin_state.coin.amount >= required_dig_collateral_amount)
343            .collect::<Vec<_>>();
344
345    if maybe_collateral_coin_states.is_empty() {
346        return Ok(None);
347    }
348
349    // sort by descending coin amount - validate highest value coins first
350    maybe_collateral_coin_states.sort_unstable_by_key(|coin_state| Reverse(coin_state.coin.amount));
351
352    for coin_state in maybe_collateral_coin_states {
353        if let Ok(collateral_coin) = DigCollateralCoin::from_coin_state(&peer, coin_state).await
354            && collateral_coin.proof().parent_inner_puzzle_hash == wallet_puzzle_hash
355        {
356            return Ok(Some(collateral_coin));
357        }
358    }
359
360    Ok(None)
361}
362
363#[derive(Debug, Clone)]
364pub struct GetStoreMirrorCoinsParams<'gmp> {
365    pub ssl_cert_path: &'gmp str,
366    pub ssl_key_path: &'gmp str,
367    pub wallet: &'gmp Wallet,
368    pub store_id: Bytes32,
369    pub epoch: &'gmp BigInt,
370    pub archive_file_size_bytes: u64,
371    pub network: NetworkType,
372}
373
374#[derive(Debug, Clone)]
375pub struct AllMirrorCoins {
376    pub store_id: Bytes32,
377    pub owned_collateral_coins: Vec<DigCollateralCoin>,
378    pub external_collateral_coins: Vec<DigCollateralCoin>,
379}
380
381pub async fn get_mirror_coins(
382    params: GetStoreMirrorCoinsParams<'_>,
383) -> datalayer_driver::Result<AllMirrorCoins> {
384    let wallet_puzzle_hash = params.wallet.get_owner_puzzle_hash().await?;
385    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
386    let required_dig_collateral_amount = calc_required_collateral(params.archive_file_size_bytes);
387
388    let morphed_store_launcher_id =
389        DigCollateralCoin::morph_store_launcher_id_for_mirror(params.store_id, params.epoch);
390    // filter coins for amount
391    let mut maybe_collateral_coin_states =
392        get_unspent_coin_states_by_hint(&peer, morphed_store_launcher_id, params.network)
393            .await?
394            .coin_states
395            .into_iter()
396            .filter(|coin_state| coin_state.coin.amount >= required_dig_collateral_amount)
397            .collect::<Vec<_>>();
398
399    let mut collateral = AllMirrorCoins {
400        store_id: params.store_id,
401        owned_collateral_coins: vec![],
402        external_collateral_coins: vec![],
403    };
404
405    if maybe_collateral_coin_states.is_empty() {
406        return Ok(collateral);
407    }
408
409    // sort by descending coin amount - validate highest value coins first
410    maybe_collateral_coin_states.sort_unstable_by_key(|coin_state| Reverse(coin_state.coin.amount));
411
412    for coin_state in maybe_collateral_coin_states {
413        if let Ok(collateral_coin) = DigCollateralCoin::from_coin_state(&peer, coin_state).await {
414            if collateral_coin.proof().parent_inner_puzzle_hash == wallet_puzzle_hash {
415                collateral.owned_collateral_coins.push(collateral_coin);
416            } else {
417                collateral.external_collateral_coins.push(collateral_coin);
418            }
419        }
420    }
421
422    Ok(collateral)
423}
424
425#[derive(Debug, Clone)]
426pub struct ReclaimCollateralParams<'rc> {
427    pub ssl_cert_path: &'rc str,
428    pub ssl_key_path: &'rc str,
429    pub wallet: &'rc Wallet,
430    pub collateral_coin: DigCollateralCoin,
431    pub network: NetworkType,
432    pub fee: Option<u64>,
433}
434
435/// spend a collateral coin to remove store collateral or mirror coin and return spendable $DIG to wallet
436pub async fn reclaim_store_collateral(
437    params: ReclaimCollateralParams<'_>,
438) -> datalayer_driver::Result<(TransactionAck, Bytes32)> {
439    let peer = connect_random(params.network, params.ssl_cert_path, params.ssl_key_path).await?;
440    let private_key = params.wallet.get_private_synthetic_key().await?;
441    let public_key = params.wallet.get_public_synthetic_key().await?;
442
443    let fee = if let Some(specified_fee) = params.fee {
444        specified_fee
445    } else {
446        get_fee_estimate(&peer, 60).await?
447    };
448
449    let xch_fee_coins = params
450        .wallet
451        .select_unspent_coins(&peer, 0, fee, vec![])
452        .await?;
453
454    let collateral_spends = params
455        .collateral_coin
456        .spend(public_key, xch_fee_coins, fee)?;
457    let coin_spend_signature = sign_coin_spends(
458        &collateral_spends,
459        &[private_key],
460        params.network != NetworkType::Mainnet,
461    )?;
462    let spend_bundle = SpendBundle::new(collateral_spends, coin_spend_signature);
463
464    let ack = broadcast_spend_bundle(&peer, spend_bundle).await?;
465    Ok((ack, params.collateral_coin.coin().coin_id()))
466}
467
468/// Convenience alias for reclaiming a mirror coin; identical to reclaim_store_collateral
469pub async fn reclaim_mirror_coin(
470    params: ReclaimCollateralParams<'_>,
471) -> datalayer_driver::Result<(TransactionAck, Bytes32)> {
472    reclaim_store_collateral(params).await
473}