use core::str::FromStr;
use bitcoin::absolute::LockTime;
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::secp256k1::{Keypair, Message, SecretKey};
use bitcoin::transaction::Version;
use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness};
use serde::{Deserialize, Serialize};
use sidestr_core::block::secp;
use sidestr_core::sighash::{key_path_sighash, verify_taproot_key_path, SighashRules};
use crate::account::check_sats;
use crate::deposit::{deposit_secret, DepositAddress};
use crate::error::{Error, Result};
use crate::{DUST, MIN_PAY, TXBT4_RULES};
pub const MAX_RATE: u64 = 1000;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Coin {
pub txid: String,
pub vout: u32,
pub value: u64,
pub script: String,
pub tweak: String,
}
impl Coin {
pub fn from_deposit(d: &DepositAddress, txid: &str, vout: u32, value: u64) -> Self {
Self {
txid: txid.to_owned(),
vout,
value,
script: d.script.clone(),
tweak: d.tweak.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlanOutput {
pub value: u64,
#[serde(rename = "scriptPubKey")]
pub script_pubkey: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Plan {
pub picked: Vec<Coin>,
pub outputs: Vec<PlanOutput>,
pub fee: u64,
pub change: u64,
pub rate: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PayoutParams<'a> {
pub coins: &'a [Coin],
pub amount: u64,
pub rate: u64,
pub to_script: &'a str,
pub change_script: &'a str,
}
pub fn vsize_estimate(n_in: u64, n_out: u64) -> u64 {
11 + 58 * n_in + 43 * n_out
}
pub fn plan_payout(p: &PayoutParams<'_>) -> Result<Plan> {
let v = check_sats(p.amount)?;
if v < MIN_PAY {
return Err(Error::PayoutTooSmall);
}
if !(1..=MAX_RATE).contains(&p.rate) {
return Err(Error::Rate);
}
let mut sorted: Vec<&Coin> = p.coins.iter().collect();
sorted.sort_by_key(|c| core::cmp::Reverse(c.value)); let mut picked: Vec<Coin> = Vec::new();
let mut in_sum = 0u64;
for c in sorted {
picked.push(c.clone());
in_sum = in_sum.saturating_add(c.value);
let n = picked.len() as u64;
let fee = p.rate * vsize_estimate(n, 2);
if in_sum >= v + fee {
let change = in_sum - v - fee;
if change >= DUST {
return Ok(Plan {
picked,
outputs: vec![
PlanOutput {
value: v,
script_pubkey: p.to_script.to_owned(),
},
PlanOutput {
value: change,
script_pubkey: p.change_script.to_owned(),
},
],
fee,
change,
rate: p.rate,
});
}
let fee1 = p.rate * vsize_estimate(n, 1);
if in_sum >= v + fee1 {
return Ok(Plan {
picked,
outputs: vec![PlanOutput {
value: v,
script_pubkey: p.to_script.to_owned(),
}],
fee: in_sum - v,
change: 0,
rate: p.rate,
});
}
}
}
Err(Error::NotCovered {
held: in_sum,
amount: v,
})
}
fn script(what: &'static str, hex: &str) -> Result<ScriptBuf> {
ScriptBuf::from_hex(hex).map_err(|e| Error::Encoding {
what,
reason: e.to_string(),
})
}
pub fn prevouts(plan: &Plan) -> Result<Vec<TxOut>> {
plan.picked
.iter()
.map(|c| {
Ok(TxOut {
value: Amount::from_sat(c.value),
script_pubkey: script("coin script", &c.script)?,
})
})
.collect()
}
pub fn unsigned_tx(plan: &Plan) -> Result<Transaction> {
let input = plan
.picked
.iter()
.map(|c| {
Ok(TxIn {
previous_output: OutPoint {
txid: Txid::from_str(&c.txid).map_err(|e| Error::Encoding {
what: "coin txid",
reason: e.to_string(),
})?,
vout: c.vout,
},
script_sig: ScriptBuf::new(),
sequence: Sequence(0xffff_fffd),
witness: Witness::new(),
})
})
.collect::<Result<_>>()?;
let output = plan
.outputs
.iter()
.map(|o| {
Ok(TxOut {
value: Amount::from_sat(o.value),
script_pubkey: script("output script", &o.script_pubkey)?,
})
})
.collect::<Result<_>>()?;
Ok(Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input,
output,
})
}
pub fn vsize_of(tx: &Transaction) -> u64 {
tx.vsize() as u64
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedPayout {
pub tx: Transaction,
pub hex: String,
pub txid: String,
pub vsize: u64,
}
pub fn sign_payout(
plan: &Plan,
operator_secret: &SecretKey,
rules: SighashRules,
) -> Result<SignedPayout> {
let mut tx = unsigned_tx(plan)?;
let prevouts = prevouts(plan)?;
let mut witnesses = Vec::with_capacity(tx.input.len());
for (i, coin) in plan.picked.iter().enumerate() {
let (m, ht) = key_path_sighash(&tx, i, &prevouts, rules)
.map_err(|reason| Error::Sighash { input: i, reason })?;
let d = deposit_secret(operator_secret, &coin.tweak)?;
let kp = Keypair::from_secret_key(secp(), &d);
let sig = secp().sign_schnorr_with_aux_rand(&Message::from_digest(m), &kp, &[0u8; 32]);
let mut w = sig.as_ref().to_vec();
w.push(ht);
witnesses.push(Witness::from_slice(&[w]));
}
for (input, w) in tx.input.iter_mut().zip(witnesses) {
input.witness = w;
}
for i in 0..tx.input.len() {
verify_taproot_key_path(&tx, i, &prevouts, rules).map_err(|reason| Error::ScriptCheck {
input: i,
reason: reason.to_owned(),
})?;
}
let vsize = vsize_of(&tx);
if plan.fee < plan.rate * vsize {
return Err(Error::FeeBelowRate {
vsize,
fee: plan.fee,
rate: plan.rate,
});
}
Ok(SignedPayout {
hex: serialize_hex(&tx),
txid: tx.compute_txid().to_string(),
tx,
vsize,
})
}
pub fn sign_payout_txbt4(plan: &Plan, operator_secret: &SecretKey) -> Result<SignedPayout> {
sign_payout(plan, operator_secret, TXBT4_RULES)
}