use std::fmt;
#[cfg(feature = "driver")]
use anyswap_core::types::Outpoint;
use anyswap_core::{
script::{BitcoinSwapScript, LiquidSwapScript, derive_liquid_blinding_key},
transaction::{
self, ClaimByPreimageParams, KeypathSpendParams, LiquidChainCtx, RefundByCsvParams, TxError,
},
types::{Chain, SwapNetwork},
utils::sha256_payment_hash,
};
use bitcoin::secp256k1::{PublicKey, SecretKey};
use elements::AssetId;
use thiserror::Error;
use uuid::Uuid;
use crate::{
context::SwapContext,
utxo::{self, SwapUtxo, UtxoError},
wallet::SwapWallet,
};
#[derive(Clone, Debug, Error)]
pub enum ScriptError {
#[error("this client is not set up for Liquid; add the Liquid parameters to the context")]
LiquidNotConfigured,
#[error("the swap trades asset {found}, but this client is set up for {expected}")]
UnexpectedAsset { expected: String, found: String },
#[error("missing field: {0}")]
MissingField(&'static str),
#[error("wallet: {0}")]
Wallet(String),
#[error("invalid swap script: {0}")]
Script(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Side {
Source,
Dest,
}
impl fmt::Display for Side {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Source => "source",
Self::Dest => "dest",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Terms {
pub swap_id: Uuid,
pub side: Side,
pub chain: Chain,
pub asset: String,
pub csv: u32,
pub payment_hash: [u8; 32],
pub server_pubkey: PublicKey,
pub server_blinding_pubkey: Option<PublicKey>,
}
pub enum Tx {
Bitcoin(bitcoin::Transaction),
Liquid(elements::Transaction),
}
impl Tx {
pub fn decode(chain: Chain, hex: &str) -> Result<Self, TxError> {
Ok(match chain {
Chain::Bitcoin => Self::Bitcoin(transaction::bitcoin::decode_tx_hex(hex)?),
Chain::Liquid => Self::Liquid(transaction::liquid::decode_tx_hex(hex)?),
})
}
pub fn txid(&self) -> String {
match self {
Self::Bitcoin(tx) => tx.compute_txid().to_string(),
Self::Liquid(tx) => tx.txid().to_string(),
}
}
#[cfg(feature = "driver")]
pub fn inputs(&self) -> Vec<Outpoint> {
let outpoint = |txid: String, vout| Outpoint::new(txid, vout);
match self {
Self::Bitcoin(tx) => tx
.input
.iter()
.map(|i| outpoint(i.previous_output.txid.to_string(), i.previous_output.vout))
.collect(),
Self::Liquid(tx) => tx
.input
.iter()
.map(|i| outpoint(i.previous_output.txid.to_string(), i.previous_output.vout))
.collect(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpendPath {
Keypath,
Preimage([u8; 32]),
Csv,
}
enum Inner {
Bitcoin(Box<BitcoinSwapScript>),
Liquid {
script: Box<LiquidSwapScript>,
blinding_key: SecretKey,
ctx: LiquidChainCtx,
},
}
pub struct SwapScript {
chain: Chain,
network: SwapNetwork,
payment_hash: [u8; 32],
inner: Inner,
}
impl<W: SwapWallet> SwapContext<W> {
pub fn script(&self, terms: &Terms) -> Result<SwapScript, ScriptError> {
let wallet = self.wallet();
let wallet_error = |e: W::Error| ScriptError::Wallet(e.to_string());
let script_error = |e: anyhow::Error| ScriptError::Script(e.to_string());
let (claim, refund) = match terms.side {
Side::Source => (
terms.server_pubkey,
wallet
.source_refund_pubkey(&terms.swap_id)
.map_err(wallet_error)?,
),
Side::Dest => (
wallet
.dest_claim_pubkey(&terms.swap_id)
.map_err(wallet_error)?,
terms.server_pubkey,
),
};
let (claim, refund) = (claim.x_only_public_key().0, refund.x_only_public_key().0);
let inner = match terms.chain {
Chain::Bitcoin => Inner::Bitcoin(Box::new(
BitcoinSwapScript::new(claim, refund, terms.payment_hash, terms.csv)
.map_err(script_error)?,
)),
Chain::Liquid => {
let liquid = self.liquid().ok_or(ScriptError::LiquidNotConfigured)?;
if terms.asset != liquid.policy_asset.to_string() {
return Err(ScriptError::UnexpectedAsset {
expected: liquid.policy_asset.to_string(),
found: terms.asset.clone(),
});
}
let own = match terms.side {
Side::Source => wallet.source_blinding_privkey(&terms.swap_id),
Side::Dest => wallet.dest_blinding_privkey(&terms.swap_id),
}
.map_err(wallet_error)?;
let peer = terms
.server_blinding_pubkey
.ok_or(ScriptError::MissingField("server_blinding_pubkey"))?;
let (blinding_pubkey, blinding_key) =
derive_liquid_blinding_key(&own, &peer).map_err(ScriptError::Script)?;
let script = LiquidSwapScript::new(
claim,
refund,
terms.payment_hash,
terms.csv,
blinding_pubkey,
)
.map_err(script_error)?;
Inner::Liquid {
script: Box::new(script),
blinding_key,
ctx: liquid,
}
}
};
Ok(SwapScript {
chain: terms.chain,
network: self.network(),
payment_hash: terms.payment_hash,
inner,
})
}
}
impl SwapScript {
pub fn address(&self) -> String {
match &self.inner {
Inner::Bitcoin(script) => script.address(self.network),
Inner::Liquid { script, .. } => script.confidential_address(self.network).to_string(),
}
}
pub fn outputs(&self, tx: &Tx) -> Vec<SwapUtxo> {
let txid = tx.txid();
match (&self.inner, tx) {
(Inner::Bitcoin(script), Tx::Bitcoin(tx)) => {
let spk = script.script_pubkey();
tx.output
.iter()
.enumerate()
.filter(|(_, o)| o.script_pubkey == spk)
.map(|(vout, o)| SwapUtxo::bitcoin(txid.clone(), vout as u32, o.value.to_sat()))
.collect()
}
(Inner::Liquid { script, .. }, Tx::Liquid(tx)) => {
let spk = script.script_pubkey();
tx.output
.iter()
.enumerate()
.filter(|(_, o)| o.script_pubkey == spk)
.map(|(vout, o)| SwapUtxo::liquid(txid.clone(), vout as u32, o.clone()))
.filter(|utxo| self.value(utxo).is_some())
.collect()
}
_ => Vec::new(),
}
}
pub fn output_at(&self, tx: &Tx, vout: u32) -> Result<(SwapUtxo, u64), TxError> {
let txid = tx.txid();
match (&self.inner, tx) {
(Inner::Bitcoin(script), Tx::Bitcoin(tx)) => {
let value = transaction::bitcoin::opening_value(tx, vout, script)?.to_sat();
Ok((SwapUtxo::bitcoin(txid, vout, value), value))
}
(
Inner::Liquid {
script,
blinding_key,
ctx,
},
Tx::Liquid(tx),
) => {
let value = transaction::liquid::opening_value(
tx,
vout,
script,
blinding_key,
ctx.policy_asset,
)?;
let output = tx.output[vout as usize].clone();
Ok((SwapUtxo::liquid(txid, vout, output), value))
}
_ => Err(TxError::InvalidPrevout(
"transaction of another chain".into(),
)),
}
}
pub fn value(&self, utxo: &SwapUtxo) -> Option<u64> {
utxo.value(self.unblinding())
}
pub fn economical(
&self,
utxos: &[SwapUtxo],
fee_rate: f64,
) -> Result<(Vec<SwapUtxo>, Vec<SwapUtxo>), UtxoError> {
let liquid = self.unblinding().map(|(key, asset)| (*key, asset));
utxo::partition_economical(utxos, self.chain, fee_rate, liquid)
}
pub fn spend_path(&self, tx: &Tx, vin: u32) -> Option<SpendPath> {
let mut items: Vec<&[u8]> = match tx {
Tx::Bitcoin(tx) => tx.input.get(vin as usize)?.witness.iter().collect(),
Tx::Liquid(tx) => tx
.input
.get(vin as usize)?
.witness
.script_witness
.iter()
.collect(),
};
if items.len() > 1 && items.last()?.first() == Some(&0x50) {
items.pop();
}
let (claim, refund) = match &self.inner {
Inner::Bitcoin(script) => (
script.claim_script().as_bytes(),
script.refund_script().as_bytes(),
),
Inner::Liquid { script, .. } => (
script.claim_script().as_bytes(),
script.refund_script().as_bytes(),
),
};
match items.as_slice() {
[_signature] => Some(SpendPath::Keypath),
[_signature, leaf, _control] if *leaf == refund => Some(SpendPath::Csv),
[_signature, preimage, leaf, _control] if *leaf == claim => {
let preimage = <[u8; 32]>::try_from(*preimage).ok()?;
(sha256_payment_hash(&preimage) == self.payment_hash)
.then_some(SpendPath::Preimage(preimage))
}
_ => None,
}
}
pub fn keypath(
&self,
utxos: &[SwapUtxo],
claim_privkey: SecretKey,
refund_privkey: SecretKey,
destination: &str,
fee_rate: f64,
) -> Result<String, TxError> {
let params = match &self.inner {
Inner::Bitcoin(script) => KeypathSpendParams::Bitcoin {
inputs: utxo::bitcoin_inputs(utxos).map_err(wrong_chain)?,
script: (**script).clone(),
claim_privkey,
refund_privkey,
},
Inner::Liquid {
script,
blinding_key,
ctx,
} => KeypathSpendParams::Liquid {
inputs: utxo::liquid_inputs(utxos).map_err(wrong_chain)?,
blinding_key: *blinding_key,
script: (**script).clone(),
genesis_hash: ctx.genesis_hash,
policy_asset: ctx.policy_asset,
claim_privkey,
refund_privkey,
},
};
transaction::build_refund_by_keypath(params, destination, fee_rate)
}
pub fn refund_by_csv(
&self,
utxos: &[SwapUtxo],
refund_privkey: SecretKey,
destination: &str,
fee_rate: f64,
) -> Result<String, TxError> {
let params = match &self.inner {
Inner::Bitcoin(script) => RefundByCsvParams::Bitcoin {
inputs: utxo::bitcoin_inputs(utxos).map_err(wrong_chain)?,
script: (**script).clone(),
refund_privkey,
},
Inner::Liquid {
script,
blinding_key,
ctx,
} => RefundByCsvParams::Liquid {
inputs: utxo::liquid_inputs(utxos).map_err(wrong_chain)?,
blinding_key: *blinding_key,
script: (**script).clone(),
genesis_hash: ctx.genesis_hash,
policy_asset: ctx.policy_asset,
refund_privkey,
},
};
transaction::build_refund_by_csv(params, destination, fee_rate)
}
pub fn claim_by_preimage(
&self,
utxos: &[SwapUtxo],
claim_privkey: SecretKey,
preimage: [u8; 32],
destination: &str,
fee_rate: f64,
) -> Result<String, TxError> {
let params = match &self.inner {
Inner::Bitcoin(script) => ClaimByPreimageParams::Bitcoin {
inputs: utxo::bitcoin_inputs(utxos).map_err(wrong_chain)?,
script: (**script).clone(),
claim_privkey,
preimage,
},
Inner::Liquid {
script,
blinding_key,
ctx,
} => ClaimByPreimageParams::Liquid {
inputs: utxo::liquid_inputs(utxos).map_err(wrong_chain)?,
blinding_key: *blinding_key,
script: (**script).clone(),
genesis_hash: ctx.genesis_hash,
policy_asset: ctx.policy_asset,
claim_privkey,
preimage,
},
};
transaction::build_claim_by_preimage(params, destination, fee_rate)
}
#[cfg(test)]
pub(crate) fn bitcoin(&self) -> Option<&BitcoinSwapScript> {
match &self.inner {
Inner::Bitcoin(script) => Some(script),
Inner::Liquid { .. } => None,
}
}
#[cfg(test)]
pub(crate) fn liquid(&self) -> Option<&LiquidSwapScript> {
match &self.inner {
Inner::Liquid { script, .. } => Some(script),
Inner::Bitcoin(_) => None,
}
}
fn unblinding(&self) -> Option<(&SecretKey, AssetId)> {
match &self.inner {
Inner::Bitcoin(_) => None,
Inner::Liquid {
blinding_key, ctx, ..
} => Some((blinding_key, ctx.policy_asset)),
}
}
}
fn wrong_chain(e: UtxoError) -> TxError {
TxError::InvalidPrevout(e.to_string())
}