1use crate::anchor_output;
2use crate::asset;
3use crate::asset::packet::add_asset_packet_to_psbt;
4use crate::asset::AssetId;
5use crate::contract::SpendSelection;
6use crate::script::tr_script_pubkey;
7use crate::server;
8use crate::ArkAddress;
9use crate::Asset;
10use crate::Error;
11use crate::ErrorContext;
12use crate::UNSPENDABLE_KEY;
13use crate::VTXO_TAPROOT_KEY;
14use bitcoin::absolute::LockTime;
15use bitcoin::hashes::Hash;
16use bitcoin::key::PublicKey;
17use bitcoin::key::Secp256k1;
18use bitcoin::psbt;
19use bitcoin::secp256k1;
20use bitcoin::secp256k1::schnorr;
21use bitcoin::sighash::Prevouts;
22use bitcoin::sighash::SighashCache;
23use bitcoin::taproot;
24use bitcoin::taproot::ControlBlock;
25use bitcoin::taproot::LeafVersion;
26use bitcoin::taproot::TaprootBuilder;
27use bitcoin::taproot::TaprootSpendInfo;
28use bitcoin::transaction;
29use bitcoin::Amount;
30use bitcoin::OutPoint;
31use bitcoin::Psbt;
32use bitcoin::ScriptBuf;
33use bitcoin::TapLeafHash;
34use bitcoin::TapSighashType;
35use bitcoin::Transaction;
36use bitcoin::TxIn;
37use bitcoin::TxOut;
38use bitcoin::XOnlyPublicKey;
39use std::collections::BTreeMap;
40use std::collections::HashMap;
41use std::io;
42use std::io::Write;
43
44pub mod issue_asset;
45pub mod reissue_asset;
46
47pub use issue_asset::build_self_asset_issuance_transactions;
48pub use issue_asset::SelfAssetIssuanceTransactions;
49pub use reissue_asset::build_asset_reissuance_transactions;
50pub use reissue_asset::AssetReissuanceTransactions;
51
52#[derive(Debug, Clone)]
54pub struct VtxoInput {
55 spend_script: ScriptBuf,
59 locktime: Option<LockTime>,
62 control_block: ControlBlock,
63 tapscripts: Vec<ScriptBuf>,
65 script_pubkey: ScriptBuf,
66 amount: Amount,
68 outpoint: OutPoint,
70 assets: Vec<Asset>,
72}
73
74impl VtxoInput {
75 pub fn new(
76 vtxo_spend_script: ScriptBuf,
77 locktime: Option<LockTime>,
78 control_block: ControlBlock,
79 tapscripts: Vec<ScriptBuf>,
80 script_pubkey: ScriptBuf,
81 amount: Amount,
82 outpoint: OutPoint,
83 assets: Vec<Asset>,
84 ) -> Self {
85 Self {
86 spend_script: vtxo_spend_script,
87 locktime,
88 control_block,
89 tapscripts,
90 script_pubkey,
91 amount,
92 outpoint,
93 assets,
94 }
95 }
96
97 pub fn new_with_spend_selection(
98 spend_selection: SpendSelection,
99 tapscripts: Vec<ScriptBuf>,
100 script_pubkey: ScriptBuf,
101 amount: Amount,
102 outpoint: OutPoint,
103 assets: Vec<Asset>,
104 ) -> Self {
105 let (spend_script, control_block) = spend_selection.spend_info();
106 Self::new(
107 spend_script,
108 spend_selection.locktime,
109 control_block,
110 tapscripts,
111 script_pubkey,
112 amount,
113 outpoint,
114 assets,
115 )
116 }
117
118 pub fn outpoint(&self) -> OutPoint {
119 self.outpoint
120 }
121
122 pub fn spend_info(&self) -> (&ScriptBuf, &ControlBlock) {
123 (&self.spend_script, &self.control_block)
124 }
125
126 pub fn script_pubkey(&self) -> ScriptBuf {
127 self.script_pubkey.clone()
128 }
129
130 pub fn amount(&self) -> Amount {
131 self.amount
132 }
133
134 pub fn assets(&self) -> &[Asset] {
135 &self.assets
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct SendReceiver {
142 pub address: ArkAddress,
143 pub amount: Amount,
144 pub assets: Vec<Asset>,
145}
146
147impl SendReceiver {
148 pub fn bitcoin(address: ArkAddress, amount: Amount) -> Self {
149 Self {
150 address,
151 amount,
152 assets: Vec::new(),
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
158pub struct OffchainTransactions {
159 pub ark_tx: Psbt,
160 pub checkpoint_txs: Vec<Psbt>,
161}
162
163pub(crate) fn btc_change_output_index(ark_tx: &Psbt, num_receiver_outputs: usize) -> Option<u16> {
165 (ark_tx.unsigned_tx.output.len() > num_receiver_outputs + 1)
166 .then_some((ark_tx.unsigned_tx.output.len() - 2) as u16)
167}
168
169pub fn build_offchain_transactions(
197 receivers: &[SendReceiver],
198 change_address: &ArkAddress,
199 vtxo_inputs: &[VtxoInput],
200 server_info: &server::Info,
201) -> Result<OffchainTransactions, Error> {
202 if vtxo_inputs.is_empty() {
203 return Err(Error::transaction(
204 "cannot build Ark transaction without inputs",
205 ));
206 }
207
208 let vtxo_min_amount = server_info.vtxo_min_amount.unwrap_or(Amount::ONE_SAT);
209 if receivers
210 .iter()
211 .any(|SendReceiver { amount, .. }| *amount < vtxo_min_amount)
212 {
213 return Err(Error::transaction(format!(
214 "output amount smaller than minimum of {vtxo_min_amount}"
215 )));
216 }
217
218 let checkpoint_script = &server_info.checkpoint_tapscript;
219
220 let mut checkpoint_data = Vec::new();
221 for vtxo_input in vtxo_inputs.iter() {
222 let (psbt, spend_info) = build_checkpoint_psbt(vtxo_input, checkpoint_script.clone())
223 .with_context(|| {
224 format!(
225 "failed to build checkpoint psbt for input {:?}",
226 vtxo_input.outpoint
227 )
228 })?;
229
230 checkpoint_data.push((psbt, spend_info));
231 }
232
233 let mut outputs = receivers
234 .iter()
235 .map(
236 |SendReceiver {
237 address, amount, ..
238 }| {
239 if *amount >= server_info.dust {
240 TxOut {
241 value: *amount,
242 script_pubkey: address.to_p2tr_script_pubkey(),
243 }
244 } else {
245 TxOut {
246 value: *amount,
247 script_pubkey: address.to_sub_dust_script_pubkey(),
248 }
249 }
250 },
251 )
252 .collect::<Vec<_>>();
253
254 let total_input_amount: Amount = vtxo_inputs.iter().map(|v| v.amount).sum();
255 let total_output_amount: Amount = outputs.iter().map(|v| v.value).sum();
256
257 let change_amount = total_input_amount.checked_sub(total_output_amount).ok_or_else(|| {
258 Error::transaction(format!(
259 "cannot cover total output amount ({total_output_amount}) with total input amount ({total_input_amount})"
260 ))
261 })?;
262
263 if change_amount > Amount::ZERO {
264 if change_amount >= server_info.dust {
265 outputs.push(TxOut {
266 value: change_amount,
267 script_pubkey: change_address.to_p2tr_script_pubkey(),
268 })
269 } else {
270 outputs.push(TxOut {
271 value: change_amount,
272 script_pubkey: change_address.to_sub_dust_script_pubkey(),
273 })
274 }
275 }
276
277 outputs.push(anchor_output());
278
279 let timelocked_inputs = vtxo_inputs
280 .iter()
281 .filter_map(|x| x.locktime)
282 .collect::<Vec<_>>();
283
284 let highest_timelock = timelocked_inputs
285 .iter()
286 .try_fold(None, |acc, a| match (acc, a) {
287 (None, locktime) => Ok(Some(*locktime)),
288 (Some(a @ LockTime::Blocks(h1)), LockTime::Blocks(h2)) if h1 > *h2 => Ok(Some(a)),
289 (Some(LockTime::Blocks(_)), b @ LockTime::Blocks(_)) => Ok(Some(*b)),
290 (Some(a @ LockTime::Seconds(t1)), LockTime::Seconds(t2)) if t1 > *t2 => Ok(Some(a)),
291 (Some(LockTime::Seconds(_)), b @ LockTime::Seconds(_)) => Ok(Some(*b)),
292 _ => Err(Error::transaction("incompatible locktimes")),
293 })?;
294
295 let (lock_time, sequence) = match highest_timelock {
296 Some(timelock) => (timelock, bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF),
297 None => (LockTime::ZERO, bitcoin::Sequence::MAX),
298 };
299
300 let unsigned_ark_tx = Transaction {
301 version: transaction::Version::non_standard(3),
302 lock_time,
303 input: checkpoint_data
304 .iter()
305 .map(|(psbt, _)| TxIn {
306 previous_output: OutPoint {
307 txid: psbt.unsigned_tx.compute_txid(),
308 vout: 0,
309 },
310 script_sig: Default::default(),
311 sequence,
312 witness: Default::default(),
313 })
314 .collect(),
315 output: outputs,
316 };
317
318 let mut unsigned_ark_psbt =
319 Psbt::from_unsigned_tx(unsigned_ark_tx).map_err(Error::transaction)?;
320
321 for (i, (checkpoint_psbt, checkpoint_spend_info)) in checkpoint_data.iter().enumerate() {
322 unsigned_ark_psbt.inputs[i].witness_utxo =
325 Some(checkpoint_psbt.unsigned_tx.output[0].clone());
326
327 let vtxo_spend_script = &vtxo_inputs[i].spend_script;
330 let leaf_version = LeafVersion::TapScript;
331 let control_block = checkpoint_spend_info
332 .spend_info
333 .control_block(&(vtxo_spend_script.clone(), leaf_version))
334 .expect("control block for vtxo spend script");
335
336 unsigned_ark_psbt.inputs[i].tap_scripts =
337 BTreeMap::from_iter([(control_block, (vtxo_spend_script.clone(), leaf_version))]);
338
339 let mut bytes = Vec::new();
342
343 let spend_script = &vtxo_inputs[i].spend_script;
344 let scripts = [spend_script.clone(), checkpoint_script.clone()];
345
346 for script in scripts {
347 bytes.push(1);
349
350 bytes.push(LeafVersion::TapScript.to_consensus());
352
353 let mut script_bytes = script.to_bytes();
354
355 write_compact_size_uint(&mut bytes, script_bytes.len() as u64)
356 .map_err(Error::transaction)?;
357
358 bytes.append(&mut script_bytes);
359 }
360
361 unsigned_ark_psbt.inputs[i].unknown.insert(
362 psbt::raw::Key {
363 type_value: 222,
364 key: VTXO_TAPROOT_KEY.to_vec(),
365 },
366 bytes,
367 );
368 unsigned_ark_psbt.inputs[i].witness_script = Some(spend_script.clone());
369 }
370
371 Ok(OffchainTransactions {
372 ark_tx: unsigned_ark_psbt,
373 checkpoint_txs: checkpoint_data.into_iter().map(|(psbt, _)| psbt).collect(),
374 })
375}
376
377#[derive(Debug, Clone)]
378struct CheckpointSpendInfo {
379 spend_info: TaprootSpendInfo,
380}
381
382impl CheckpointSpendInfo {
383 fn new(vtxo_input: &VtxoInput, checkpoint_exit_script: ScriptBuf) -> Self {
384 let secp = Secp256k1::new();
385
386 let unspendable_key: PublicKey = UNSPENDABLE_KEY.parse().expect("valid key");
387 let (unspendable_key, _) = unspendable_key.inner.x_only_public_key();
388
389 let vtxo_spend_script = &vtxo_input.spend_script;
390
391 let spend_info = TaprootBuilder::new()
392 .add_leaf(1, vtxo_spend_script.clone())
393 .expect("valid spend leaf")
394 .add_leaf(1, checkpoint_exit_script)
395 .expect("valid exit leaf")
396 .finalize(&secp, unspendable_key)
397 .expect("can be finalized");
398
399 Self { spend_info }
400 }
401
402 fn script_pubkey(&self) -> ScriptBuf {
403 tr_script_pubkey(&self.spend_info)
404 }
405}
406
407fn build_checkpoint_psbt(
408 vtxo_input: &VtxoInput,
409 checkpoint_exit_script: ScriptBuf,
414) -> Result<(Psbt, CheckpointSpendInfo), Error> {
415 let (lock_time, sequence) = match vtxo_input.locktime {
416 Some(timelock) => (timelock, bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF),
417 None => (LockTime::ZERO, bitcoin::Sequence::MAX),
418 };
419
420 let inputs = vec![TxIn {
421 previous_output: vtxo_input.outpoint,
422 script_sig: Default::default(),
423 sequence,
424 witness: Default::default(),
425 }];
426
427 let checkpoint_spend_info = CheckpointSpendInfo::new(vtxo_input, checkpoint_exit_script);
428
429 let outputs = vec![
430 TxOut {
431 value: vtxo_input.amount,
432 script_pubkey: checkpoint_spend_info.script_pubkey(),
433 },
434 anchor_output(),
435 ];
436
437 let unsigned_tx = Transaction {
438 version: transaction::Version::non_standard(3),
439 lock_time,
440 input: inputs,
441 output: outputs,
442 };
443
444 let mut unsigned_checkpoint_psbt =
445 Psbt::from_unsigned_tx(unsigned_tx).map_err(Error::transaction)?;
446
447 unsigned_checkpoint_psbt.inputs[0].witness_utxo = Some(TxOut {
450 value: vtxo_input.amount,
451 script_pubkey: vtxo_input.script_pubkey.clone(),
452 });
453
454 let (vtxo_spend_script, vtxo_spend_control_block) = vtxo_input.spend_info();
457
458 let leaf_version = vtxo_spend_control_block.leaf_version;
459 unsigned_checkpoint_psbt.inputs[0].tap_scripts = BTreeMap::from_iter([(
460 vtxo_spend_control_block.clone(),
461 (vtxo_spend_script.clone(), leaf_version),
462 )]);
463
464 let mut bytes = Vec::new();
467
468 for script in vtxo_input.tapscripts.iter() {
469 bytes.push(1);
471
472 bytes.push(LeafVersion::TapScript.to_consensus());
474
475 let mut script_bytes = script.to_bytes();
476
477 write_compact_size_uint(&mut bytes, script_bytes.len() as u64)
478 .map_err(Error::transaction)?;
479
480 bytes.append(&mut script_bytes);
481 }
482
483 unsigned_checkpoint_psbt.inputs[0].unknown.insert(
484 psbt::raw::Key {
485 type_value: 222,
486 key: VTXO_TAPROOT_KEY.to_vec(),
487 },
488 bytes,
489 );
490 unsigned_checkpoint_psbt.inputs[0].witness_script = Some(vtxo_spend_script.clone());
491
492 Ok((unsigned_checkpoint_psbt, checkpoint_spend_info))
493}
494
495fn write_compact_size_uint<W: Write>(w: &mut W, val: u64) -> io::Result<()> {
496 if val < 253 {
497 w.write_all(&[val as u8])?;
498 } else if val < 0x10000 {
499 w.write_all(&[253])?;
500 w.write_all(&(val as u16).to_le_bytes())?;
501 } else if val < 0x100000000 {
502 w.write_all(&[254])?;
503 w.write_all(&(val as u32).to_le_bytes())?;
504 } else {
505 w.write_all(&[255])?;
506 w.write_all(&val.to_le_bytes())?;
507 }
508 Ok(())
509}
510
511pub fn sign_checkpoint_transaction<S>(sign_fn: S, psbt: &mut Psbt) -> Result<(), Error>
513where
514 S: FnOnce(
515 &mut psbt::Input,
516 secp256k1::Message,
517 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
518{
519 let witness_utxo = [psbt.inputs[0].witness_utxo.clone().expect("witness UTXO")];
520 let prevouts = Prevouts::All(&witness_utxo);
521
522 let psbt_input = psbt.inputs.get_mut(0).expect("input at index");
523
524 let (_, (vtxo_spend_script, leaf_version)) =
525 psbt_input.tap_scripts.first_key_value().expect("one entry");
526
527 let leaf_hash = TapLeafHash::from_script(vtxo_spend_script, *leaf_version);
528
529 let tap_sighash = SighashCache::new(&psbt.unsigned_tx)
530 .taproot_script_spend_signature_hash(0, &prevouts, leaf_hash, TapSighashType::Default)
531 .map_err(Error::crypto)
532 .context("failed to generate sighash")?;
533
534 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
535
536 let sigs = sign_fn(psbt_input, msg)?;
537 for (sig, pk) in sigs {
538 let sig = taproot::Signature {
539 signature: sig,
540 sighash_type: TapSighashType::Default,
541 };
542
543 psbt_input.tap_script_sigs.insert((pk, leaf_hash), sig);
544 }
545
546 Ok(())
547}
548
549pub fn sign_ark_transaction<S>(sign_fn: S, psbt: &mut Psbt, input_index: usize) -> Result<(), Error>
550where
551 S: FnOnce(
552 &mut psbt::Input,
553 secp256k1::Message,
554 ) -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, Error>,
555{
556 tracing::debug!(index = input_index, "Signing Ark transaction input");
557
558 let witness_utxos = psbt
559 .inputs
560 .iter()
561 .map(|i| i.witness_utxo.clone().expect("witness UTXO"))
562 .collect::<Vec<_>>();
563
564 let psbt_input = psbt.inputs.get_mut(input_index).expect("input at index");
565
566 let prevouts = Prevouts::All(&witness_utxos);
569
570 let (_, (vtxo_spend_script, leaf_version)) =
571 psbt_input.tap_scripts.first_key_value().expect("one entry");
572
573 let leaf_hash = TapLeafHash::from_script(vtxo_spend_script, *leaf_version);
574
575 let tap_sighash = SighashCache::new(&psbt.unsigned_tx)
576 .taproot_script_spend_signature_hash(
577 input_index,
578 &prevouts,
579 leaf_hash,
580 TapSighashType::Default,
581 )
582 .map_err(Error::crypto)
583 .context("failed to generate sighash")?;
584
585 let msg = secp256k1::Message::from_digest(tap_sighash.to_raw_hash().to_byte_array());
586
587 let sigs = sign_fn(psbt_input, msg)?;
588 for (sig, pk) in sigs {
589 let sig = taproot::Signature {
590 signature: sig,
591 sighash_type: TapSighashType::Default,
592 };
593
594 psbt_input.tap_script_sigs.insert((pk, leaf_hash), sig);
595 }
596
597 Ok(())
598}
599
600pub fn build_asset_send_transactions(
619 receivers: &[SendReceiver],
620 change_address: &ArkAddress,
621 vtxo_inputs: &[VtxoInput],
622 server_info: &server::Info,
623) -> Result<OffchainTransactions, Error> {
624 let mut offchain =
625 build_offchain_transactions(receivers, change_address, vtxo_inputs, server_info)?;
626
627 if let Some(packet) = create_send_packet(vtxo_inputs, receivers, &offchain.ark_tx)? {
628 add_asset_packet_to_psbt(&mut offchain.ark_tx, &packet)?;
629 }
630
631 Ok(offchain)
632}
633
634pub fn build_asset_burn_transactions(
646 own_address: &ArkAddress,
647 change_address: &ArkAddress,
648 vtxo_inputs: &[VtxoInput],
649 server_info: &server::Info,
650 burn_asset_id: AssetId,
651 burn_amount: u64,
652) -> Result<OffchainTransactions, Error> {
653 let mut offchain = build_offchain_transactions(
654 &[SendReceiver {
655 address: *own_address,
656 amount: server_info.dust,
657 assets: Vec::new(),
658 }],
659 change_address,
660 vtxo_inputs,
661 server_info,
662 )?;
663
664 if let Some(packet) =
665 create_burn_packet(vtxo_inputs, burn_asset_id, burn_amount, &offchain.ark_tx)?
666 {
667 add_asset_packet_to_psbt(&mut offchain.ark_tx, &packet)?;
668 }
669
670 Ok(offchain)
671}
672
673fn create_send_packet(
678 inputs: &[VtxoInput],
679 receivers: &[SendReceiver],
680 ark_tx: &Psbt,
681) -> Result<Option<asset::packet::Packet>, Error> {
682 struct AssetTransfer {
683 inputs: Vec<asset::packet::AssetInput>,
684 outputs: Vec<asset::packet::AssetOutput>,
685 input_amount: u64,
686 requested_amount: u64,
687 }
688
689 let mut transfers: HashMap<AssetId, AssetTransfer> = HashMap::new();
690
691 for (input_index, input) in inputs.iter().enumerate() {
692 for asset in &input.assets {
693 let transfer = transfers
694 .entry(asset.asset_id)
695 .or_insert_with(|| AssetTransfer {
696 inputs: Vec::new(),
697 outputs: Vec::new(),
698 input_amount: 0,
699 requested_amount: 0,
700 });
701
702 transfer.inputs.push(asset::packet::AssetInput {
703 input_index: input_index as u16,
704 amount: asset.amount,
705 });
706
707 transfer.input_amount = transfer
708 .input_amount
709 .checked_add(asset.amount)
710 .ok_or_else(|| Error::ad_hoc("asset input amount overflow"))?;
711 }
712 }
713
714 let any_receiver_assets = receivers.iter().any(|receiver| !receiver.assets.is_empty());
715 if transfers.is_empty() && !any_receiver_assets {
716 return Ok(None);
717 }
718
719 for (receiver_index, receiver) in receivers.iter().enumerate() {
720 for asset in &receiver.assets {
721 let transfer = transfers.get_mut(&asset.asset_id).ok_or_else(|| {
722 Error::ad_hoc(format!(
723 "receiver references asset {} that is not present in selected inputs",
724 asset.asset_id
725 ))
726 })?;
727
728 transfer.outputs.push(asset::packet::AssetOutput {
729 output_index: receiver_index as u16,
730 amount: asset.amount,
731 });
732 transfer.requested_amount = transfer
733 .requested_amount
734 .checked_add(asset.amount)
735 .ok_or_else(|| Error::ad_hoc("asset transfer amount overflow"))?;
736 }
737 }
738
739 let change_output_index = btc_change_output_index(ark_tx, receivers.len());
740 let mut groups = Vec::new();
741
742 for (asset_id, mut transfer) in transfers.into_iter() {
743 let leftover_amount = transfer
744 .input_amount
745 .checked_sub(transfer.requested_amount)
746 .ok_or_else(|| {
747 Error::ad_hoc(format!(
748 "requested amount for asset {} exceeds selected input amount",
749 asset_id
750 ))
751 })?;
752
753 match (change_output_index, leftover_amount) {
754 (Some(change_output_index), leftover_amount) if leftover_amount > 0 => {
755 transfer.outputs.push(asset::packet::AssetOutput {
756 output_index: change_output_index,
757 amount: leftover_amount,
758 });
759 }
760 (None, leftover_amount) if leftover_amount > 0 => {
761 return Err(Error::ad_hoc(
762 "asset transfer has preserved asset changes but no BTC change output",
763 ));
764 }
765 _ => {}
766 }
767
768 groups.push(asset::packet::AssetGroup {
769 asset_id: Some(asset_id),
770 control_asset: None,
771 metadata: None,
772 inputs: transfer.inputs,
773 outputs: transfer.outputs,
774 });
775 }
776
777 groups.sort_by_key(|group| {
778 let asset_id = group
779 .asset_id
780 .expect("generic asset-send groups always have asset ids");
781 (*asset_id.txid.as_byte_array(), asset_id.group_index)
782 });
783
784 Ok(Some(asset::packet::Packet { groups }))
785}
786
787fn create_burn_packet(
788 inputs: &[VtxoInput],
789 burn_asset_id: AssetId,
790 burn_amount: u64,
791 ark_tx: &Psbt,
792) -> Result<Option<asset::packet::Packet>, Error> {
793 struct AssetTransfer {
794 inputs: Vec<asset::packet::AssetInput>,
795 input_amount: u64,
796 }
797
798 let mut transfers: HashMap<AssetId, AssetTransfer> = HashMap::new();
799
800 for (input_index, input) in inputs.iter().enumerate() {
801 for asset in input.assets() {
802 let transfer = transfers
803 .entry(asset.asset_id)
804 .or_insert_with(|| AssetTransfer {
805 inputs: Vec::new(),
806 input_amount: 0,
807 });
808
809 transfer.inputs.push(asset::packet::AssetInput {
810 input_index: input_index as u16,
811 amount: asset.amount,
812 });
813 transfer.input_amount += asset.amount;
814 }
815 }
816
817 if transfers.is_empty() {
818 return Err(Error::ad_hoc(format!(
819 "selected inputs do not contain asset {}",
820 burn_asset_id
821 )));
822 }
823
824 let burn_input_amount = transfers
825 .get(&burn_asset_id)
826 .ok_or_else(|| {
827 Error::ad_hoc(format!(
828 "selected inputs do not contain asset {}",
829 burn_asset_id
830 ))
831 })?
832 .input_amount;
833
834 let burn_leftover_amount = burn_input_amount.checked_sub(burn_amount).ok_or_else(|| {
835 Error::ad_hoc(format!(
836 "requested burn amount for asset {} exceeds selected input amount",
837 burn_asset_id
838 ))
839 })?;
840
841 let preserved_output_index = btc_change_output_index(ark_tx, 1).unwrap_or(0);
842 let mut groups = Vec::new();
843
844 for (asset_id, transfer) in transfers.into_iter() {
845 let leftover_amount = if asset_id == burn_asset_id {
846 burn_leftover_amount
847 } else {
848 transfer.input_amount
849 };
850
851 let mut outputs = Vec::new();
852 if leftover_amount > 0 {
853 outputs.push(asset::packet::AssetOutput {
854 output_index: preserved_output_index,
855 amount: leftover_amount,
856 });
857 }
858
859 groups.push(asset::packet::AssetGroup {
860 asset_id: Some(asset_id),
861 control_asset: None,
862 metadata: None,
863 inputs: transfer.inputs,
864 outputs,
865 });
866 }
867
868 groups.sort_by_key(|group| {
869 let asset_id = group
870 .asset_id
871 .expect("asset-burn groups always have asset ids");
872 (*asset_id.txid.as_byte_array(), asset_id.group_index)
873 });
874
875 Ok(Some(asset::packet::Packet { groups }))
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881 use crate::asset::packet::AssetGroup;
882 use crate::asset::packet::AssetInput;
883 use crate::asset::packet::AssetOutput;
884 use crate::asset::packet::Packet;
885 use crate::script::multisig_script;
886 use crate::send::VtxoInput;
887 use crate::server::Info;
888 use bitcoin::key::Secp256k1;
889 use bitcoin::opcodes::OP_TRUE;
890 use bitcoin::script::Builder;
891 use bitcoin::taproot::LeafVersion;
892 use bitcoin::taproot::TaprootBuilder;
893 use bitcoin::Amount;
894 use bitcoin::Network;
895 use bitcoin::OutPoint;
896 use bitcoin::Sequence;
897 use bitcoin::Txid;
898
899 #[test]
900 fn build_offchain_transactions_has_no_packet_even_when_assets_are_present() {
901 let server_info = test_server_info();
902 let asset_id = AssetId {
903 txid: Txid::from_byte_array([10; 32]),
904 group_index: 0,
905 };
906 let (input, own_address) = asset_send_input(
907 1,
908 660,
909 vec![Asset {
910 asset_id,
911 amount: 10,
912 }],
913 );
914 let receiver = SendReceiver {
915 address: own_address,
916 amount: Amount::from_sat(330),
917 assets: vec![Asset {
918 asset_id,
919 amount: 6,
920 }],
921 };
922
923 let res =
924 build_offchain_transactions(&[receiver], &own_address, &[input], &server_info).unwrap();
925
926 assert_eq!(res.ark_tx.unsigned_tx.output.len(), 3);
927 }
928
929 #[test]
930 fn build_asset_send_transactions_routes_requested_assets_to_receiver_outputs_and_change() {
931 let server_info = test_server_info();
932 let asset_id = AssetId {
933 txid: Txid::from_byte_array([11; 32]),
934 group_index: 4,
935 };
936 let (input, own_address) = asset_send_input(
937 2,
938 660,
939 vec![Asset {
940 asset_id,
941 amount: 10,
942 }],
943 );
944 let receiver = SendReceiver {
945 address: own_address,
946 amount: Amount::from_sat(330),
947 assets: vec![Asset {
948 asset_id,
949 amount: 6,
950 }],
951 };
952
953 let res = build_asset_send_transactions(&[receiver], &own_address, &[input], &server_info)
954 .unwrap();
955
956 let expected_packet = Packet {
957 groups: vec![AssetGroup {
958 asset_id: Some(asset_id),
959 control_asset: None,
960 metadata: None,
961 inputs: vec![AssetInput {
962 input_index: 0,
963 amount: 10,
964 }],
965 outputs: vec![
966 AssetOutput {
967 output_index: 0,
968 amount: 6,
969 },
970 AssetOutput {
971 output_index: 1,
972 amount: 4,
973 },
974 ],
975 }],
976 };
977
978 assert_eq!(
979 res.ark_tx.unsigned_tx.output[asset_packet_index(&res.ark_tx)],
980 expected_packet.to_txout()
981 );
982 }
983
984 #[test]
985 fn build_asset_send_transactions_errors_when_receiver_references_missing_asset() {
986 let server_info = test_server_info();
987 let missing_asset_id = AssetId {
988 txid: Txid::from_byte_array([12; 32]),
989 group_index: 1,
990 };
991 let (input, own_address) = asset_send_input(3, 330, vec![]);
992 let receiver = SendReceiver {
993 address: own_address,
994 amount: Amount::from_sat(330),
995 assets: vec![Asset {
996 asset_id: missing_asset_id,
997 amount: 1,
998 }],
999 };
1000
1001 let err = build_asset_send_transactions(&[receiver], &own_address, &[input], &server_info)
1002 .unwrap_err();
1003
1004 assert!(err.to_string().contains("receiver references asset"));
1005 }
1006
1007 #[test]
1008 fn build_asset_send_transactions_errors_when_leftover_assets_exist_but_no_btc_change_output() {
1009 let server_info = test_server_info();
1010 let asset_id = AssetId {
1011 txid: Txid::from_byte_array([13; 32]),
1012 group_index: 2,
1013 };
1014 let (input, own_address) = asset_send_input(
1015 4,
1016 330,
1017 vec![Asset {
1018 asset_id,
1019 amount: 10,
1020 }],
1021 );
1022 let receiver = SendReceiver {
1023 address: own_address,
1024 amount: Amount::from_sat(330),
1025 assets: vec![Asset {
1026 asset_id,
1027 amount: 6,
1028 }],
1029 };
1030
1031 let err = build_asset_send_transactions(&[receiver], &own_address, &[input], &server_info)
1032 .unwrap_err();
1033
1034 assert!(err
1035 .to_string()
1036 .contains("asset transfer has preserved asset changes but no BTC change output"));
1037 }
1038
1039 #[test]
1040 fn build_asset_send_transactions_sorts_packet_groups_stably() {
1041 let server_info = test_server_info();
1042 let asset_id_a = AssetId {
1043 txid: Txid::from_byte_array([14; 32]),
1044 group_index: 1,
1045 };
1046 let asset_id_b = AssetId {
1047 txid: Txid::from_byte_array([15; 32]),
1048 group_index: 0,
1049 };
1050 let (input, own_address) = asset_send_input(
1051 5,
1052 660,
1053 vec![
1054 Asset {
1055 asset_id: asset_id_b,
1056 amount: 8,
1057 },
1058 Asset {
1059 asset_id: asset_id_a,
1060 amount: 10,
1061 },
1062 ],
1063 );
1064 let receiver = SendReceiver {
1065 address: own_address,
1066 amount: Amount::from_sat(330),
1067 assets: vec![
1068 Asset {
1069 asset_id: asset_id_b,
1070 amount: 3,
1071 },
1072 Asset {
1073 asset_id: asset_id_a,
1074 amount: 4,
1075 },
1076 ],
1077 };
1078
1079 let res = build_asset_send_transactions(&[receiver], &own_address, &[input], &server_info)
1080 .unwrap();
1081
1082 let expected_packet = Packet {
1083 groups: vec![
1084 AssetGroup {
1085 asset_id: Some(asset_id_a),
1086 control_asset: None,
1087 metadata: None,
1088 inputs: vec![AssetInput {
1089 input_index: 0,
1090 amount: 10,
1091 }],
1092 outputs: vec![
1093 AssetOutput {
1094 output_index: 0,
1095 amount: 4,
1096 },
1097 AssetOutput {
1098 output_index: 1,
1099 amount: 6,
1100 },
1101 ],
1102 },
1103 AssetGroup {
1104 asset_id: Some(asset_id_b),
1105 control_asset: None,
1106 metadata: None,
1107 inputs: vec![AssetInput {
1108 input_index: 0,
1109 amount: 8,
1110 }],
1111 outputs: vec![
1112 AssetOutput {
1113 output_index: 0,
1114 amount: 3,
1115 },
1116 AssetOutput {
1117 output_index: 1,
1118 amount: 5,
1119 },
1120 ],
1121 },
1122 ],
1123 };
1124
1125 assert_eq!(
1126 res.ark_tx.unsigned_tx.output[asset_packet_index(&res.ark_tx)],
1127 expected_packet.to_txout()
1128 );
1129 }
1130
1131 #[test]
1132 fn build_asset_burn_transactions_routes_leftover_assets_to_change() {
1133 let server_info = test_server_info();
1134 let burn_asset_id = AssetId {
1135 txid: Txid::from_byte_array([16; 32]),
1136 group_index: 0,
1137 };
1138 let carried_asset_id = AssetId {
1139 txid: Txid::from_byte_array([17; 32]),
1140 group_index: 1,
1141 };
1142 let (input, own_address) = asset_send_input(
1143 6,
1144 660,
1145 vec![
1146 Asset {
1147 asset_id: burn_asset_id,
1148 amount: 10,
1149 },
1150 Asset {
1151 asset_id: carried_asset_id,
1152 amount: 4,
1153 },
1154 ],
1155 );
1156
1157 let res = build_asset_burn_transactions(
1158 &own_address,
1159 &own_address,
1160 &[input],
1161 &server_info,
1162 burn_asset_id,
1163 6,
1164 )
1165 .unwrap();
1166
1167 let expected_packet = Packet {
1168 groups: vec![
1169 AssetGroup {
1170 asset_id: Some(burn_asset_id),
1171 control_asset: None,
1172 metadata: None,
1173 inputs: vec![AssetInput {
1174 input_index: 0,
1175 amount: 10,
1176 }],
1177 outputs: vec![AssetOutput {
1178 output_index: 1,
1179 amount: 4,
1180 }],
1181 },
1182 AssetGroup {
1183 asset_id: Some(carried_asset_id),
1184 control_asset: None,
1185 metadata: None,
1186 inputs: vec![AssetInput {
1187 input_index: 0,
1188 amount: 4,
1189 }],
1190 outputs: vec![AssetOutput {
1191 output_index: 1,
1192 amount: 4,
1193 }],
1194 },
1195 ],
1196 };
1197
1198 assert_eq!(
1199 res.ark_tx.unsigned_tx.output[asset_packet_index(&res.ark_tx)],
1200 expected_packet.to_txout()
1201 );
1202 }
1203
1204 #[test]
1205 fn build_asset_burn_transactions_errors_when_asset_is_missing() {
1206 let server_info = test_server_info();
1207 let missing_asset_id = AssetId {
1208 txid: Txid::from_byte_array([18; 32]),
1209 group_index: 0,
1210 };
1211 let (input, own_address) = asset_send_input(7, 330, vec![]);
1212
1213 let err = build_asset_burn_transactions(
1214 &own_address,
1215 &own_address,
1216 &[input],
1217 &server_info,
1218 missing_asset_id,
1219 1,
1220 )
1221 .unwrap_err();
1222
1223 assert!(err
1224 .to_string()
1225 .contains("selected inputs do not contain asset"));
1226 }
1227
1228 #[test]
1229 fn build_asset_burn_transactions_routes_leftover_assets_to_self_output_without_btc_change() {
1230 let server_info = test_server_info();
1231 let burn_asset_id = AssetId {
1232 txid: Txid::from_byte_array([19; 32]),
1233 group_index: 0,
1234 };
1235 let (input, own_address) = asset_send_input(
1236 8,
1237 330,
1238 vec![Asset {
1239 asset_id: burn_asset_id,
1240 amount: 10,
1241 }],
1242 );
1243
1244 let res = build_asset_burn_transactions(
1245 &own_address,
1246 &own_address,
1247 &[input],
1248 &server_info,
1249 burn_asset_id,
1250 6,
1251 )
1252 .unwrap();
1253
1254 let expected_packet = Packet {
1255 groups: vec![AssetGroup {
1256 asset_id: Some(burn_asset_id),
1257 control_asset: None,
1258 metadata: None,
1259 inputs: vec![AssetInput {
1260 input_index: 0,
1261 amount: 10,
1262 }],
1263 outputs: vec![AssetOutput {
1264 output_index: 0,
1265 amount: 4,
1266 }],
1267 }],
1268 };
1269
1270 assert_eq!(
1271 res.ark_tx.unsigned_tx.output[asset_packet_index(&res.ark_tx)],
1272 expected_packet.to_txout()
1273 );
1274 }
1275
1276 fn test_server_info() -> Info {
1277 let signer_pk = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
1278 .parse()
1279 .unwrap();
1280 let forfeit_pk = "03dff1d77f2a671c5f36183726db2341be58f8be17d2a3d1d2cd47b7b0f5f2d624"
1281 .parse()
1282 .unwrap();
1283
1284 Info {
1285 version: "test".into(),
1286 signer_pk,
1287 forfeit_pk,
1288 forfeit_address: "bcrt1q8frde3yn78tl9ecgq4anlz909jh0clefhucdur"
1289 .parse::<bitcoin::Address<_>>()
1290 .unwrap()
1291 .require_network(Network::Regtest)
1292 .unwrap(),
1293 checkpoint_tapscript: Builder::new().push_opcode(OP_TRUE).into_script(),
1294 network: Network::Regtest,
1295 session_duration: 0,
1296 unilateral_exit_delay: Sequence::MAX,
1297 boarding_exit_delay: Sequence::MAX,
1298 utxo_min_amount: None,
1299 utxo_max_amount: None,
1300 vtxo_min_amount: Some(Amount::from_sat(1)),
1301 vtxo_max_amount: None,
1302 dust: Amount::from_sat(330),
1303 fees: None,
1304 scheduled_session: None,
1305 deprecated_signers: vec![],
1306 service_status: Default::default(),
1307 digest: "test".into(),
1308 max_tx_weight: 40_000,
1309 max_op_return_outputs: 3,
1310 }
1311 }
1312
1313 fn asset_send_input(
1314 outpoint_tag: u8,
1315 amount_sat: u64,
1316 assets: Vec<Asset>,
1317 ) -> (VtxoInput, ArkAddress) {
1318 let secp = Secp256k1::new();
1319
1320 let server_pk: PublicKey =
1321 "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
1322 .parse()
1323 .unwrap();
1324 let owner_pk: PublicKey =
1325 "03dff1d77f2a671c5f36183726db2341be58f8be17d2a3d1d2cd47b7b0f5f2d624"
1326 .parse()
1327 .unwrap();
1328
1329 let server_xonly = server_pk.inner.x_only_public_key().0;
1330 let owner_xonly = owner_pk.inner.x_only_public_key().0;
1331 let spend_script = multisig_script(server_xonly, owner_xonly);
1332 let spend_info = TaprootBuilder::new()
1333 .add_leaf(0, spend_script.clone())
1334 .unwrap()
1335 .finalize(&secp, server_xonly)
1336 .unwrap();
1337 let control_block = spend_info
1338 .control_block(&(spend_script.clone(), LeafVersion::TapScript))
1339 .unwrap();
1340 let own_address = ArkAddress::new(Network::Regtest, server_xonly, spend_info.output_key());
1341
1342 (
1343 VtxoInput::new(
1344 spend_script.clone(),
1345 None,
1346 control_block,
1347 vec![spend_script],
1348 own_address.to_p2tr_script_pubkey(),
1349 Amount::from_sat(amount_sat),
1350 OutPoint::new(Txid::from_byte_array([outpoint_tag; 32]), 0),
1351 assets,
1352 ),
1353 own_address,
1354 )
1355 }
1356
1357 fn asset_packet_index(ark_tx: &Psbt) -> usize {
1358 ark_tx.unsigned_tx.output.len() - 2
1359 }
1360}