use crate::{spell, spell::CharmsFee};
use anyhow::bail;
use bitcoin::{
self, Address, Amount, FeeRate, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
Weight, absolute::LockTime, hashes::Hash, script::PushBytesBuf, transaction::Version,
};
use charms_client::{
NormalizedSpell, NormalizedTransaction,
bitcoin_tx::{BitcoinTx, SPELL_MARKER},
tx::{Chain, Tx},
};
use charms_data::{TxId, UtxoId};
use std::{collections::BTreeMap, str::FromStr};
const DUST_LIMIT: Amount = Amount::from_sat(300);
pub fn add_spell(
tx: Transaction,
spell_data: &[u8],
change_pubkey: ScriptBuf,
fee_rate: FeeRate,
prev_txs: &BTreeMap<TxId, Tx>,
charms_fee_pubkey: Option<ScriptBuf>,
charms_fee: Amount,
) -> anyhow::Result<Vec<Transaction>> {
let mut tx = tx;
if let Some(charms_fee_pubkey) = charms_fee_pubkey {
let existing_fee_amount: Amount = tx
.output
.iter()
.filter(|txout| txout.script_pubkey == charms_fee_pubkey)
.map(|txout| txout.value)
.sum();
if existing_fee_amount < charms_fee {
let additional_fee = charms_fee - existing_fee_amount + DUST_LIMIT;
tx.output.push(TxOut {
value: additional_fee,
script_pubkey: charms_fee_pubkey,
});
}
}
use bitcoin::script::Builder;
let spell_marker = PushBytesBuf::try_from(SPELL_MARKER.to_vec())
.map_err(|_| anyhow::anyhow!("failed to create spell marker"))?;
let spell_payload = PushBytesBuf::try_from(spell_data.to_vec())
.map_err(|_| anyhow::anyhow!("spell data too large for OP_RETURN"))?;
let op_return_script = Builder::new()
.push_opcode(bitcoin::opcodes::all::OP_RETURN)
.push_slice(&spell_marker)
.push_slice(&spell_payload)
.into_script();
tx.output.push(TxOut {
value: Amount::ZERO,
script_pubkey: op_return_script,
});
let change_amount = compute_change_amount(fee_rate, &tx, prev_txs);
if change_amount >= DUST_LIMIT {
tx.output.push(TxOut {
value: change_amount,
script_pubkey: change_pubkey,
});
}
Ok(vec![tx])
}
fn compute_change_amount(
fee_rate: FeeRate,
tx: &Transaction,
prev_txs: &BTreeMap<TxId, Tx>,
) -> Amount {
let change_output_weight = Weight::from_wu(172);
let signatures_weight = Weight::from_wu(65) * tx.input.len() as u64;
let total_tx_weight = tx.weight() + signatures_weight + change_output_weight;
let fee = fee_rate.fee_wu(total_tx_weight).unwrap();
let tx_amount_in = tx_total_amount_in(prev_txs, tx);
let tx_amount_out = tx.output.iter().map(|tx_out| tx_out.value).sum::<Amount>();
tx_amount_in - tx_amount_out - fee
}
pub fn tx_total_amount_in(prev_txs: &BTreeMap<TxId, Tx>, tx: &Transaction) -> Amount {
tx.input
.iter()
.map(|tx_in| (tx_in.previous_output.txid, tx_in.previous_output.vout))
.map(|(tx_id, i)| {
let txid = TxId(tx_id.to_byte_array());
let Tx::Bitcoin(tx) = prev_txs[&txid].clone() else {
unreachable!()
};
tx.inner().output[i as usize].value
})
.sum::<Amount>()
}
pub fn tx_total_amount_out(tx: &Transaction) -> Amount {
tx.output.iter().map(|tx_out| tx_out.value).sum::<Amount>()
}
pub fn tx_output(tx: &NormalizedTransaction) -> anyhow::Result<Vec<TxOut>> {
let tx_outputs = (tx.coins.as_ref().expect("coins should be provided"))
.iter()
.map(|u| {
let value = Amount::from_sat(u.amount);
let script_pubkey = ScriptBuf::from_bytes(u.dest.to_vec());
Ok(TxOut {
value,
script_pubkey,
})
})
.collect::<anyhow::Result<_>>()?;
Ok(tx_outputs)
}
pub fn tx_input(ins: &[UtxoId]) -> Vec<TxIn> {
ins.iter()
.map(|utxo_id| TxIn {
previous_output: OutPoint {
txid: Txid::from_byte_array(utxo_id.0.0),
vout: utxo_id.1,
},
script_sig: Default::default(),
sequence: Default::default(),
witness: Default::default(),
})
.collect()
}
pub fn from_spell(spell: &NormalizedSpell) -> anyhow::Result<BitcoinTx> {
let input = tx_input(&spell.tx.ins.as_ref().expect("inputs are expected"));
let output = tx_output(&spell.tx)?;
let tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input,
output,
};
Ok(BitcoinTx::Simple(tx))
}
pub fn make_transactions(
spell: &NormalizedSpell,
change_address: &String,
prev_txs_by_id: &BTreeMap<TxId, Tx>,
spell_data: &[u8],
fee_rate: f64,
charms_fee: Option<CharmsFee>,
total_cycles: u64,
) -> anyhow::Result<Vec<Tx>> {
let change_address = bitcoin::Address::from_str(&change_address)?;
let network = match &change_address {
a if a.is_valid_for_network(Network::Bitcoin) => Network::Bitcoin.to_core_arg(),
a if a.is_valid_for_network(Network::Testnet4) => Network::Testnet4.to_core_arg(),
a if a.is_valid_for_network(Network::Regtest) => Network::Regtest.to_core_arg(),
_ => bail!("Invalid change address: {:?}", change_address),
};
let change_address_checked = change_address.assume_checked();
let change_pubkey = change_address_checked.script_pubkey();
let charms_fee_pubkey = charms_fee
.as_ref()
.and_then(|charms_fee| charms_fee.fee_address(&Chain::Bitcoin, network))
.and_then(|fee_address| {
Address::from_str(fee_address)
.ok()
.map(|a| a.assume_checked().script_pubkey())
});
let charms_fee = spell::get_charms_fee(&charms_fee, total_cycles);
let fee_rate = FeeRate::from_sat_per_kwu((fee_rate * 250.0) as u64);
let tx = from_spell(spell)?;
let BitcoinTx::Simple(tx) = tx else {
bail!("expected simple transaction")
};
let transactions = add_spell(
tx,
spell_data,
change_pubkey,
fee_rate,
&prev_txs_by_id,
charms_fee_pubkey,
charms_fee,
)?;
Ok(transactions
.into_iter()
.map(|tx| Tx::Bitcoin(BitcoinTx::Simple(tx)))
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::{
OutPoint, ScriptBuf, TxIn, Txid, absolute::LockTime, hashes::Hash, transaction::Version,
};
use std::collections::BTreeMap;
#[test]
fn test_add_spell_op_return() {
let prev_txid_bytes = [1u8; 32];
let prev_txid = Txid::from_byte_array(prev_txid_bytes);
let prev_tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::from_sat(100_000), script_pubkey: ScriptBuf::new(),
}],
};
let mut prev_txs: BTreeMap<TxId, Tx> = BTreeMap::new();
prev_txs.insert(
TxId(prev_txid_bytes),
Tx::Bitcoin(BitcoinTx::Simple(prev_tx)),
);
let dummy_tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint {
txid: prev_txid,
vout: 0,
},
script_sig: Default::default(),
sequence: Default::default(),
witness: Default::default(),
}],
output: vec![],
};
let spell_data = b"spell";
let change_pubkey = ScriptBuf::new();
let fee_rate = FeeRate::from_sat_per_vb(1).unwrap();
let charms_fee_pubkey = None;
let charms_fee = Amount::ZERO;
let txs = add_spell(
dummy_tx,
spell_data,
change_pubkey,
fee_rate,
&prev_txs,
charms_fee_pubkey,
charms_fee,
)
.unwrap();
assert_eq!(txs.len(), 1);
let spell_tx = &txs[0];
let op_return_output = spell_tx
.output
.iter()
.find(|out| out.script_pubkey.is_op_return());
assert!(op_return_output.is_some());
assert_eq!(op_return_output.unwrap().value, Amount::ZERO);
}
}