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 server_info = self.server_info()?;
55 let (own_address, _) = self.get_offchain_address()?;
56 let (spendable, script_pubkey_to_vtxo_map) = self.spendable_virtual_vtxos().await?;
57
58 let selected_coins = select_vtxos(spendable, server_info.dust, server_info.dust, true)
59 .map_err(Error::from)
60 .context("failed to select coins for asset issuance")?;
61
62 let issuance_inputs = self.build_vtxo_inputs(selected_coins, &script_pubkey_to_vtxo_map)?;
63 let (change_address, change_address_vtxo) = self.get_offchain_address()?;
64
65 let SelfAssetIssuanceTransactions {
66 ark_tx,
67 checkpoint_txs,
68 asset_ids,
69 } = build_self_asset_issuance_transactions(
70 &own_address,
71 &change_address,
72 &issuance_inputs,
73 &server_info,
74 amount,
75 control_asset_config,
76 metadata,
77 )
78 .map_err(Error::from)
79 .context("failed to build asset issuance transactions")?;
80
81 let pending_tx = self
82 .submit_built_offchain_send(ark_tx, checkpoint_txs, change_address_vtxo.owner_pk())
83 .await
84 .context("failed to submit asset issuance transaction")?;
85
86 let ark_txid = pending_tx.ark_txid;
87 self.sign_and_finalize_pending_tx(pending_tx)
88 .await
89 .context("failed to finalize asset issuance transaction")?;
90
91 Ok(IssueAssetResult {
92 ark_txid,
93 asset_ids,
94 })
95 }
96
97 pub async fn reissue_asset(&self, asset_id: AssetId, amount: u64) -> Result<Txid, Error> {
102 if amount == 0 {
103 return Err(Error::ad_hoc("reissue amount must be > 0"));
104 }
105
106 let server_info = self.server_info()?;
107 let asset_info = self
108 .get_asset(asset_id)
109 .await
110 .context("failed to get asset info")?;
111
112 let control_asset_id = asset_info.control_asset_id.ok_or_else(|| {
113 Error::ad_hoc(format!(
114 "Asset {} can't be reissued, no control asset",
115 asset_id
116 ))
117 })?;
118
119 let (spendable, script_pubkey_to_vtxo_map) = self.spendable_virtual_vtxos().await?;
120
121 let (control_coins, _control_change) =
122 select_vtxos_for_asset(&spendable, 1, control_asset_id)
123 .map_err(Error::from)
124 .context("failed to select control asset for reissuance")?;
125
126 let mut selected_outpoints: HashSet<_> =
127 control_coins.iter().map(|coin| coin.outpoint).collect();
128 let mut selected = control_coins;
129 let btc_provided: Amount = selected.iter().map(|coin| coin.amount).sum();
130 let btc_shortfall = server_info
131 .dust
132 .checked_sub(btc_provided)
133 .unwrap_or(Amount::ZERO);
134
135 if btc_shortfall > Amount::ZERO {
136 let available: Vec<_> = spendable
137 .iter()
138 .filter(|coin| !selected_outpoints.contains(&coin.outpoint))
139 .cloned()
140 .collect();
141
142 let btc_coins = select_vtxos(available, btc_shortfall, server_info.dust, true)
143 .map_err(Error::from)
144 .context("failed to select BTC coins for reissuance")?;
145
146 for coin in btc_coins {
147 if selected_outpoints.insert(coin.outpoint) {
148 selected.push(coin);
149 }
150 }
151 }
152
153 let reissuance_inputs = self.build_vtxo_inputs(selected, &script_pubkey_to_vtxo_map)?;
154 let (self_address, _) = self.get_offchain_address()?;
155 let (change_address, change_address_vtxo) = self.get_offchain_address()?;
156
157 let AssetReissuanceTransactions {
158 ark_tx,
159 checkpoint_txs,
160 } = build_asset_reissuance_transactions(
161 &self_address,
162 &change_address,
163 &reissuance_inputs,
164 &server_info,
165 asset_id,
166 control_asset_id,
167 amount,
168 )
169 .map_err(Error::from)
170 .context("failed to build asset reissuance transactions")?;
171
172 let pending_tx = self
173 .submit_built_offchain_send(ark_tx, checkpoint_txs, change_address_vtxo.owner_pk())
174 .await
175 .context("failed to submit reissuance transaction")?;
176
177 let ark_txid = pending_tx.ark_txid;
178 self.sign_and_finalize_pending_tx(pending_tx)
179 .await
180 .context("failed to finalize reissuance transaction")?;
181
182 Ok(ark_txid)
183 }
184
185 pub async fn burn_asset(&self, asset_id: AssetId, amount: u64) -> Result<Txid, Error> {
187 if amount == 0 {
188 return Err(Error::ad_hoc("burn amount must be > 0"));
189 }
190
191 let server_info = self.server_info()?;
192 let (spendable, script_pubkey_to_vtxo_map) = self.spendable_virtual_vtxos().await?;
193
194 let (asset_coins, asset_change) = select_vtxos_for_asset(&spendable, amount, asset_id)
195 .map_err(Error::from)
196 .context("failed to select coins for asset burn")?;
197
198 let mut selected_outpoints: HashSet<_> =
199 asset_coins.iter().map(|coin| coin.outpoint).collect();
200 let mut selected = asset_coins;
201
202 let mut carries_asset_change = asset_change > 0;
203 for coin in &selected {
204 if coin.assets.iter().any(|asset| asset.asset_id != asset_id) {
205 carries_asset_change = true;
206 break;
207 }
208 }
209
210 let btc_provided: Amount = selected.iter().map(|coin| coin.amount).sum();
211 let mut btc_needed = server_info.dust;
212 if carries_asset_change {
213 btc_needed += server_info.dust;
214 }
215
216 let btc_shortfall = btc_needed.checked_sub(btc_provided).unwrap_or(Amount::ZERO);
217 if btc_shortfall > Amount::ZERO {
218 let available: Vec<_> = spendable
219 .iter()
220 .filter(|coin| !selected_outpoints.contains(&coin.outpoint))
221 .cloned()
222 .collect();
223
224 let btc_coins = select_vtxos(available, btc_shortfall, server_info.dust, true)
225 .map_err(Error::from)
226 .context("failed to select BTC coins for asset burn")?;
227
228 for coin in btc_coins {
229 if selected_outpoints.insert(coin.outpoint) {
230 selected.push(coin);
231 }
232 }
233 }
234
235 let burn_inputs = self.build_vtxo_inputs(selected, &script_pubkey_to_vtxo_map)?;
236 let (own_address, _) = self.get_offchain_address()?;
237 let (change_address, change_address_vtxo) = self.get_offchain_address()?;
238
239 let offchain = build_asset_burn_transactions(
240 &own_address,
241 &change_address,
242 &burn_inputs,
243 &server_info,
244 asset_id,
245 amount,
246 )
247 .map_err(Error::from)
248 .context("failed to build asset burn transactions")?;
249
250 let pending_tx = self
251 .submit_built_offchain_send(
252 offchain.ark_tx,
253 offchain.checkpoint_txs,
254 change_address_vtxo.owner_pk(),
255 )
256 .await
257 .context("failed to submit asset burn transaction")?;
258
259 let ark_txid = pending_tx.ark_txid;
260 self.sign_and_finalize_pending_tx(pending_tx)
261 .await
262 .context("failed to finalize asset burn transaction")?;
263
264 Ok(ark_txid)
265 }
266
267 async fn spendable_virtual_vtxos(
268 &self,
269 ) -> Result<(Vec<VirtualTxOutPoint>, HashMap<ScriptBuf, ark_core::Vtxo>), Error> {
270 let (vtxo_list, script_pubkey_to_vtxo_map) =
271 self.list_vtxos().await.context("failed to list VTXOs")?;
272
273 let now = crate::utils::unix_now()?;
274 let server_info = self.server_info()?;
275 let spendable = vtxo_list
276 .spendable_offchain_at(&server_info, now, |script| {
277 script_pubkey_to_vtxo_map
278 .get(script)
279 .map(|vtxo| vtxo.server_pk())
280 })
281 .map(|vtxo| VirtualTxOutPoint {
282 outpoint: vtxo.outpoint,
283 script_pubkey: vtxo.script.clone(),
284 expire_at: vtxo.expires_at,
285 amount: vtxo.amount,
286 assets: vtxo.assets.clone(),
287 })
288 .collect();
289
290 Ok((spendable, script_pubkey_to_vtxo_map))
291 }
292}