Skip to main content

ark_client/
asset.rs

1use crate::error::ErrorContext;
2use crate::swap_storage::SwapStorage;
3use crate::wallet::BoardingWallet;
4use crate::wallet::OnchainWallet;
5use crate::Blockchain;
6use crate::Client;
7use crate::Error;
8use ark_core::asset::AssetId;
9use ark_core::asset::ControlAssetConfig;
10use ark_core::coin_select::select_vtxos;
11use ark_core::coin_select::select_vtxos_for_asset;
12use ark_core::coin_select::VirtualTxOutPoint;
13use ark_core::send::build_asset_burn_transactions;
14use ark_core::send::build_asset_reissuance_transactions;
15use ark_core::send::build_self_asset_issuance_transactions;
16use ark_core::send::AssetReissuanceTransactions;
17use ark_core::send::SelfAssetIssuanceTransactions;
18use bitcoin::Amount;
19use bitcoin::ScriptBuf;
20use bitcoin::Txid;
21use std::collections::HashMap;
22use std::collections::HashSet;
23
24/// Result of an asset issuance.
25#[derive(Debug, Clone)]
26pub struct IssueAssetResult {
27    /// The Ark transaction ID.
28    pub ark_txid: Txid,
29    /// The issued asset IDs. If a new control asset was created, it is first.
30    pub asset_ids: Vec<AssetId>,
31}
32
33impl<B, W, S, K> Client<B, W, S, K>
34where
35    B: Blockchain,
36    W: BoardingWallet + OnchainWallet,
37    S: SwapStorage + 'static,
38    K: crate::KeyProvider,
39{
40    /// Issue a new asset.
41    ///
42    /// Creates a fresh asset with the given `amount`. The asset is sent to the caller's own
43    /// address. If `control_asset` is provided, the asset can be reissued in the future.
44    pub async fn issue_asset(
45        &self,
46        amount: u64,
47        control_asset_config: Option<ControlAssetConfig>,
48        metadata: Option<Vec<(String, String)>>,
49    ) -> Result<IssueAssetResult, Error> {
50        if amount == 0 {
51            return Err(Error::ad_hoc("asset amount must be > 0"));
52        }
53
54        let (own_address, _) = self.get_offchain_address()?;
55        let (spendable, script_pubkey_to_vtxo_map) = self.spendable_virtual_vtxos().await?;
56
57        let selected_coins = select_vtxos(
58            spendable,
59            self.server_info.dust,
60            self.server_info.dust,
61            true,
62        )
63        .map_err(Error::from)
64        .context("failed to select coins for asset issuance")?;
65
66        let issuance_inputs = self.build_vtxo_inputs(selected_coins, &script_pubkey_to_vtxo_map)?;
67        let (change_address, change_address_vtxo) = self.get_offchain_address()?;
68
69        let SelfAssetIssuanceTransactions {
70            ark_tx,
71            checkpoint_txs,
72            asset_ids,
73        } = build_self_asset_issuance_transactions(
74            &own_address,
75            &change_address,
76            &issuance_inputs,
77            &self.server_info,
78            amount,
79            control_asset_config,
80            metadata,
81        )
82        .map_err(Error::from)
83        .context("failed to build asset issuance transactions")?;
84
85        let pending_tx = self
86            .submit_built_offchain_send(ark_tx, checkpoint_txs, change_address_vtxo.owner_pk())
87            .await
88            .context("failed to submit asset issuance transaction")?;
89
90        let ark_txid = pending_tx.ark_txid;
91        self.sign_and_finalize_pending_tx(pending_tx)
92            .await
93            .context("failed to finalize asset issuance transaction")?;
94
95        Ok(IssueAssetResult {
96            ark_txid,
97            asset_ids,
98        })
99    }
100
101    /// Reissue additional units of an existing asset.
102    ///
103    /// The asset must have been created with a control asset. The control asset is spent as input
104    /// and sent back to the caller, while the new asset units are minted.
105    pub async fn reissue_asset(&self, asset_id: AssetId, amount: u64) -> Result<Txid, Error> {
106        if amount == 0 {
107            return Err(Error::ad_hoc("reissue amount must be > 0"));
108        }
109
110        let asset_info = self
111            .get_asset(asset_id)
112            .await
113            .context("failed to get asset info")?;
114
115        let control_asset_id = asset_info.control_asset_id.ok_or_else(|| {
116            Error::ad_hoc(format!(
117                "Asset {} can't be reissued, no control asset",
118                asset_id
119            ))
120        })?;
121
122        let (spendable, script_pubkey_to_vtxo_map) = self.spendable_virtual_vtxos().await?;
123
124        let (control_coins, _control_change) =
125            select_vtxos_for_asset(&spendable, 1, control_asset_id)
126                .map_err(Error::from)
127                .context("failed to select control asset for reissuance")?;
128
129        let mut selected_outpoints: HashSet<_> =
130            control_coins.iter().map(|coin| coin.outpoint).collect();
131        let mut selected = control_coins;
132        let btc_provided: Amount = selected.iter().map(|coin| coin.amount).sum();
133        let btc_shortfall = self
134            .server_info
135            .dust
136            .checked_sub(btc_provided)
137            .unwrap_or(Amount::ZERO);
138
139        if btc_shortfall > Amount::ZERO {
140            let available: Vec<_> = spendable
141                .iter()
142                .filter(|coin| !selected_outpoints.contains(&coin.outpoint))
143                .cloned()
144                .collect();
145
146            let btc_coins = select_vtxos(available, btc_shortfall, self.server_info.dust, true)
147                .map_err(Error::from)
148                .context("failed to select BTC coins for reissuance")?;
149
150            for coin in btc_coins {
151                if selected_outpoints.insert(coin.outpoint) {
152                    selected.push(coin);
153                }
154            }
155        }
156
157        let reissuance_inputs = self.build_vtxo_inputs(selected, &script_pubkey_to_vtxo_map)?;
158        let (self_address, _) = self.get_offchain_address()?;
159        let (change_address, change_address_vtxo) = self.get_offchain_address()?;
160
161        let AssetReissuanceTransactions {
162            ark_tx,
163            checkpoint_txs,
164        } = build_asset_reissuance_transactions(
165            &self_address,
166            &change_address,
167            &reissuance_inputs,
168            &self.server_info,
169            asset_id,
170            control_asset_id,
171            amount,
172        )
173        .map_err(Error::from)
174        .context("failed to build asset reissuance transactions")?;
175
176        let pending_tx = self
177            .submit_built_offchain_send(ark_tx, checkpoint_txs, change_address_vtxo.owner_pk())
178            .await
179            .context("failed to submit reissuance transaction")?;
180
181        let ark_txid = pending_tx.ark_txid;
182        self.sign_and_finalize_pending_tx(pending_tx)
183            .await
184            .context("failed to finalize reissuance transaction")?;
185
186        Ok(ark_txid)
187    }
188
189    /// Burn a specific amount of an asset.
190    pub async fn burn_asset(&self, asset_id: AssetId, amount: u64) -> Result<Txid, Error> {
191        if amount == 0 {
192            return Err(Error::ad_hoc("burn amount must be > 0"));
193        }
194
195        let (spendable, script_pubkey_to_vtxo_map) = self.spendable_virtual_vtxos().await?;
196
197        let (asset_coins, asset_change) = select_vtxos_for_asset(&spendable, amount, asset_id)
198            .map_err(Error::from)
199            .context("failed to select coins for asset burn")?;
200
201        let mut selected_outpoints: HashSet<_> =
202            asset_coins.iter().map(|coin| coin.outpoint).collect();
203        let mut selected = asset_coins;
204
205        let mut carries_asset_change = asset_change > 0;
206        for coin in &selected {
207            if coin.assets.iter().any(|asset| asset.asset_id != asset_id) {
208                carries_asset_change = true;
209                break;
210            }
211        }
212
213        let btc_provided: Amount = selected.iter().map(|coin| coin.amount).sum();
214        let mut btc_needed = self.server_info.dust;
215        if carries_asset_change {
216            btc_needed += self.server_info.dust;
217        }
218
219        let btc_shortfall = btc_needed.checked_sub(btc_provided).unwrap_or(Amount::ZERO);
220        if btc_shortfall > Amount::ZERO {
221            let available: Vec<_> = spendable
222                .iter()
223                .filter(|coin| !selected_outpoints.contains(&coin.outpoint))
224                .cloned()
225                .collect();
226
227            let btc_coins = select_vtxos(available, btc_shortfall, self.server_info.dust, true)
228                .map_err(Error::from)
229                .context("failed to select BTC coins for asset burn")?;
230
231            for coin in btc_coins {
232                if selected_outpoints.insert(coin.outpoint) {
233                    selected.push(coin);
234                }
235            }
236        }
237
238        let burn_inputs = self.build_vtxo_inputs(selected, &script_pubkey_to_vtxo_map)?;
239        let (own_address, _) = self.get_offchain_address()?;
240        let (change_address, change_address_vtxo) = self.get_offchain_address()?;
241
242        let offchain = build_asset_burn_transactions(
243            &own_address,
244            &change_address,
245            &burn_inputs,
246            &self.server_info,
247            asset_id,
248            amount,
249        )
250        .map_err(Error::from)
251        .context("failed to build asset burn transactions")?;
252
253        let pending_tx = self
254            .submit_built_offchain_send(
255                offchain.ark_tx,
256                offchain.checkpoint_txs,
257                change_address_vtxo.owner_pk(),
258            )
259            .await
260            .context("failed to submit asset burn transaction")?;
261
262        let ark_txid = pending_tx.ark_txid;
263        self.sign_and_finalize_pending_tx(pending_tx)
264            .await
265            .context("failed to finalize asset burn transaction")?;
266
267        Ok(ark_txid)
268    }
269
270    async fn spendable_virtual_vtxos(
271        &self,
272    ) -> Result<(Vec<VirtualTxOutPoint>, HashMap<ScriptBuf, ark_core::Vtxo>), Error> {
273        let (vtxo_list, script_pubkey_to_vtxo_map) =
274            self.list_vtxos().await.context("failed to list VTXOs")?;
275
276        let spendable = vtxo_list
277            .spendable_offchain()
278            .map(|vtxo| VirtualTxOutPoint {
279                outpoint: vtxo.outpoint,
280                script_pubkey: vtxo.script.clone(),
281                expire_at: vtxo.expires_at,
282                amount: vtxo.amount,
283                assets: vtxo.assets.clone(),
284            })
285            .collect();
286
287        Ok((spendable, script_pubkey_to_vtxo_map))
288    }
289}