use anyswap_core::{
codec::{self, CodecError},
fee,
transaction::{self, liquid::liquid_prevout_secrets},
types::{Chain, Outpoint},
};
use bitcoin::{Amount, secp256k1::SecretKey};
use elements::AssetId;
use thiserror::Error;
#[derive(Clone, Debug, Error)]
pub enum UtxoError {
#[error("expected a {expected} utxo, got a {found} one")]
WrongChain { expected: Chain, found: Chain },
#[error("invalid fee rate: {0} sat/vB")]
InvalidFeeRate(f64),
#[error(transparent)]
Codec(#[from] CodecError),
}
#[derive(Clone, Debug)]
pub struct SwapUtxo {
pub txid: String,
pub vout: u32,
kind: Kind,
}
#[derive(Clone, Debug)]
enum Kind {
Bitcoin(Amount),
Liquid(Box<elements::TxOut>),
}
impl SwapUtxo {
pub fn bitcoin(txid: impl Into<String>, vout: u32, value: u64) -> Self {
Self {
txid: txid.into(),
vout,
kind: Kind::Bitcoin(Amount::from_sat(value)),
}
}
pub fn liquid(txid: impl Into<String>, vout: u32, output: elements::TxOut) -> Self {
Self {
txid: txid.into(),
vout,
kind: Kind::Liquid(Box::new(output)),
}
}
pub fn outpoint(&self) -> Outpoint {
Outpoint::new(self.txid.clone(), self.vout)
}
pub fn chain(&self) -> Chain {
match self.kind {
Kind::Bitcoin(_) => Chain::Bitcoin,
Kind::Liquid(_) => Chain::Liquid,
}
}
pub(crate) fn value(&self, liquid: Option<(&SecretKey, AssetId)>) -> Option<u64> {
match &self.kind {
Kind::Bitcoin(value) => Some(value.to_sat()),
Kind::Liquid(output) => {
let (blinding_key, policy_asset) = liquid?;
let secrets = liquid_prevout_secrets(output, blinding_key).ok()?;
(secrets.asset == policy_asset).then_some(secrets.value)
}
}
}
}
pub(crate) fn bitcoin_inputs(
utxos: &[SwapUtxo],
) -> Result<Vec<(bitcoin::OutPoint, Amount)>, UtxoError> {
utxos
.iter()
.map(|utxo| match &utxo.kind {
Kind::Bitcoin(value) => Ok((
codec::parse_bitcoin_outpoint(&utxo.txid, utxo.vout)?,
*value,
)),
Kind::Liquid(_) => Err(UtxoError::WrongChain {
expected: Chain::Bitcoin,
found: Chain::Liquid,
}),
})
.collect()
}
pub(crate) fn liquid_inputs(
utxos: &[SwapUtxo],
) -> Result<Vec<(elements::OutPoint, elements::TxOut)>, UtxoError> {
utxos
.iter()
.map(|utxo| match &utxo.kind {
Kind::Liquid(output) => Ok((
codec::parse_liquid_outpoint(&utxo.txid, utxo.vout)?,
(**output).clone(),
)),
Kind::Bitcoin(_) => Err(UtxoError::WrongChain {
expected: Chain::Liquid,
found: Chain::Bitcoin,
}),
})
.collect()
}
pub(crate) fn partition_economical(
utxos: &[SwapUtxo],
chain: Chain,
fee_rate: f64,
liquid: Option<(SecretKey, AssetId)>,
) -> Result<(Vec<SwapUtxo>, Vec<SwapUtxo>), UtxoError> {
let fee = refund_fee(chain, fee_rate).ok_or(UtxoError::InvalidFeeRate(fee_rate))?;
let liquid = liquid.as_ref().map(|(key, asset)| (key, *asset));
Ok(utxos
.iter()
.cloned()
.partition(|utxo| utxo.value(liquid).is_some_and(|value| value > fee)))
}
pub(crate) fn refund_fee(chain: Chain, fee_rate: f64) -> Option<u64> {
let vsize = match chain {
Chain::Bitcoin => transaction::REFUND_VSIZE,
Chain::Liquid => transaction::liquid::REFUND_VSIZE,
};
fee::fee_for_vsize(chain, vsize, fee_rate)
}
#[cfg(test)]
pub(crate) mod fixtures {
use anyswap_core::script::LiquidSwapScript;
use elements::{
AddressParams, AssetId, BlockHash, LockTime, Transaction, TxOut, TxOutSecrets,
confidential::{AssetBlindingFactor, ValueBlindingFactor},
encode,
secp256k1_zkp::Secp256k1 as LiquidSecp256k1,
};
pub(crate) fn confidential_tx(script: &LiquidSwapScript, values: &[u64]) -> String {
let policy_asset = AssetId::LIQUID_BTC;
let mut rng = bitcoin::key::rand::thread_rng();
let output = values
.iter()
.map(|value| {
let secrets = TxOutSecrets::new(
policy_asset,
AssetBlindingFactor::zero(),
*value,
ValueBlindingFactor::zero(),
);
TxOut::new_last_confidential(
&mut rng,
&LiquidSecp256k1::new(),
*value,
policy_asset,
script.script_pubkey(),
script.blinding_pubkey(),
&[secrets],
&[],
)
.unwrap()
.0
})
.collect();
let tx = Transaction {
version: 2,
lock_time: LockTime::ZERO,
input: vec![],
output,
};
hex::encode(encode::serialize(&tx))
}
pub(crate) fn liquid_destination() -> String {
let secp = LiquidSecp256k1::new();
let key = elements::secp256k1_zkp::SecretKey::from_slice(&[9; 32]).unwrap();
let blinding = elements::secp256k1_zkp::SecretKey::from_slice(&[10; 32]).unwrap();
elements::Address::p2tr(
&secp,
key.x_only_public_key(&secp).0,
None,
Some(blinding.public_key(&secp)),
&AddressParams::ELEMENTS,
)
.to_string()
}
pub(crate) fn liquid_asset() -> String {
AssetId::LIQUID_BTC.to_string()
}
pub(crate) fn genesis_hash() -> String {
BlockHash::from_byte_array([8; 32]).to_string()
}
}