use core::str::FromStr;
use bitcoin::script::PushBytesBuf;
use bitcoin::transaction::Version;
use bitcoin::{absolute::LockTime, Address, Amount, Network, ScriptBuf, Transaction, TxIn, TxOut};
use sidestr_core::document::ChainDocument;
use sidestr_core::marker::{
checkpoint_data, parse_peg_marker, peg_marker_data, pegout_marker_data,
};
use sidestr_core::parents::Parent;
use crate::error::{Error, Result};
use crate::spend::{dust_threshold, resolve_to};
pub const PARENT_DATA_LIMIT: usize = 80;
pub fn parent_network(parent: &Parent) -> Option<Network> {
match parent.alias {
"btc" | "xbt" => Some(Network::Bitcoin),
"tbtc4" | "txbt4" => Some(Network::Testnet4),
_ => None,
}
}
pub const MARKER_DATA_LIMIT: usize = 255;
fn op_return(data: &[u8]) -> Result<ScriptBuf> {
if data.len() > MARKER_DATA_LIMIT {
return Err(Error::MarkerTooLong(data.len()));
}
let push = PushBytesBuf::try_from(data.to_vec())
.map_err(|_| Error::Encoding("push over 255 bytes".into()))?;
Ok(ScriptBuf::new_op_return(&push))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PegIn {
pub chain_id: String,
pub amount: u64,
pub side_script: ScriptBuf,
pub peg_address: Address,
pub peg: TxOut,
pub marker: TxOut,
}
pub fn build_pegin(
chain: &ChainDocument,
peg_address: &str,
amount: u64,
side_script_or_address: &str,
) -> Result<PegIn> {
let parent = chain.parent()?;
let network =
parent_network(parent).ok_or(Error::Core(sidestr_core::Error::ReservedParent {
alias: parent.alias,
label: parent.label,
}))?;
let wrong = |a: &str| Error::WrongNetwork {
address: a.to_string(),
network: network.to_string(),
parent: parent.alias.to_string(),
};
let address = Address::from_str(peg_address.trim())
.map_err(|_| Error::BadDestination(peg_address.to_string()))?
.require_network(network)
.map_err(|_| wrong(peg_address))?;
let peg_script = address.script_pubkey();
if !peg_script.is_p2tr() {
return Err(Error::BadDestination(format!(
"{peg_address}: a peg output is a taproot output (SPEC 6)"
)));
}
if amount == 0 {
return Err(Error::BadAmount);
}
let dust = dust_threshold(&peg_script);
if amount < dust {
return Err(Error::Dust {
value: amount,
min: dust,
script: peg_script.to_hex_string(),
});
}
let side_script = resolve_to(side_script_or_address, &chain.address_prefix)?.script;
let data = peg_marker_data(&chain.id, &side_script);
if data.len() > PARENT_DATA_LIMIT {
return Err(Error::MarkerTooLong(data.len()));
}
let marker_script = op_return(&data)?;
debug_assert_eq!(
parse_peg_marker(&marker_script, &chain.id).as_ref(),
Some(&side_script)
);
Ok(PegIn {
chain_id: chain.id.clone(),
amount,
side_script,
peg: TxOut {
value: Amount::from_sat(amount),
script_pubkey: peg_script,
},
marker: TxOut {
value: Amount::ZERO,
script_pubkey: marker_script,
},
peg_address: address,
})
}
impl PegIn {
pub fn outputs(&self) -> Vec<TxOut> {
vec![self.peg.clone(), self.marker.clone()]
}
pub fn transaction(&self, inputs: Vec<TxIn>, change: Option<TxOut>) -> Transaction {
let mut output = self.outputs();
output.extend(change);
Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: inputs,
output,
}
}
pub fn core_send_outputs(&self) -> serde_json::Value {
serde_json::json!([
{ self.peg_address.to_string(): btc_string(self.amount) },
{ "data": hex::encode(marker_payload(&self.marker.script_pubkey)) }
])
}
}
pub fn btc_string(sats: u64) -> String {
format!("{}.{:08}", sats / 100_000_000, sats % 100_000_000)
}
fn marker_payload(spk: &ScriptBuf) -> Vec<u8> {
sidestr_core::marker::op_return_data(spk)
.unwrap_or_default()
.to_vec()
}
pub fn pegout_payment_outputs(
chain_id: &str,
side_txid: &str,
parent_script_hex: &str,
value: u64,
) -> Result<Vec<TxOut>> {
let script = ScriptBuf::from_hex(parent_script_hex)
.map_err(|_| Error::BadDestination(parent_script_hex.to_string()))?;
let data = pegout_marker_data(chain_id, side_txid)?;
if data.len() > PARENT_DATA_LIMIT {
return Err(Error::MarkerTooLong(data.len()));
}
Ok(vec![
TxOut {
value: Amount::from_sat(value),
script_pubkey: script,
},
TxOut {
value: Amount::ZERO,
script_pubkey: op_return(&data)?,
},
])
}
pub fn checkpoint_output(chain_id: &str, height: u32, hash: &str) -> Result<TxOut> {
Ok(TxOut {
value: Amount::ZERO,
script_pubkey: op_return(&checkpoint_data(chain_id, height, hash)?)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
use sidestr_core::marker::{parse_checkpoint, parse_pegout_marker};
use sidestr_core::parents::resolve_parent;
#[test]
fn networks_and_records() {
assert_eq!(
parent_network(resolve_parent("btc").unwrap()),
Some(Network::Bitcoin)
);
assert_eq!(
parent_network(resolve_parent("txbt4").unwrap()),
Some(Network::Testnet4)
);
assert_eq!(btc_string(250_000), "0.00250000");
assert_eq!(btc_string(123_456_789_012), "1234.56789012");
let outs = pegout_payment_outputs(
"sidestr:trial",
&"c".repeat(64),
&format!("5120{}", "e9".repeat(32)),
12_345,
)
.unwrap();
assert_eq!(
parse_pegout_marker(&outs[1].script_pubkey, "sidestr:trial"),
Some("c".repeat(64))
);
assert_eq!(outs[0].value.to_sat(), 12_345);
let ck = checkpoint_output("sidestr:trial", 70_000, &"d".repeat(64)).unwrap();
assert_eq!(
parse_checkpoint(&ck.script_pubkey, "sidestr:trial"),
Some((70_000, "d".repeat(64)))
);
assert!(pegout_payment_outputs("x", "zz", "5120", 1).is_err());
}
#[test]
fn long_records_are_refused_not_emitted_unreadable() {
let txid = "c".repeat(64);
let id = format!("sidestr:{}", "x".repeat(32));
let outs = pegout_payment_outputs(&id, &txid, "5120", 1).unwrap();
assert_eq!(
sidestr_core::marker::op_return_data(&outs[1].script_pubkey).map(<[u8]>::len),
Some(PARENT_DATA_LIMIT)
);
assert_eq!(
parse_pegout_marker(&outs[1].script_pubkey, &id),
Some(txid.clone())
);
let over = format!("sidestr:{}", "x".repeat(33));
assert!(matches!(
pegout_payment_outputs(&over, &txid, "5120", 1),
Err(Error::MarkerTooLong(81))
));
let huge = "x".repeat(216);
assert!(matches!(
pegout_payment_outputs(&huge, &"a".repeat(64), "51", 10_000),
Err(Error::MarkerTooLong(256))
));
assert!(matches!(
op_return(&[0u8; MARKER_DATA_LIMIT + 1]),
Err(Error::MarkerTooLong(256))
));
assert!(op_return(&[0u8; MARKER_DATA_LIMIT]).is_ok());
}
}