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