Skip to main content

ark_client/
asset.rs

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