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#[derive(Debug, Clone)]
26pub struct IssueAssetResult {
27 pub ark_txid: Txid,
29 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 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 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 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}