use std::str::FromStr;
use bitcoin::{Amount, FeeRate};
use bitcoin::secp256k1::{PublicKey, SecretKey};
use clap::Parser;
use elements::AssetId;
use serde_json::json;
use hex_conservative::DisplayHex;
use doubletake::{BitcoinUtxo, BondSpec, ElementsUtxo};
#[derive(Parser)]
#[command(author = "Steven Roose <steven@roose.io>", version, about)]
enum App {
#[command()]
Create {
#[arg(long, required = true)]
segwit: bool,
#[arg(long)]
pubkey: PublicKey,
#[arg(long)]
bond_value: Amount,
#[arg(long)]
expiry: u64,
#[arg(long)]
reclaim_pubkey: PublicKey,
#[arg(long, default_value = "liquid", value_parser = parse_elements_network)]
network: &'static elements::AddressParams,
#[arg(long, default_value_t = AssetId::LIQUID_BTC, value_parser(parse_asset_id))]
bond_asset: AssetId,
},
#[command()]
Inspect {
spec: String,
},
#[command()]
Address {
spec: String,
#[arg(long, default_value = "liquid", value_parser = parse_elements_network)]
network: &'static elements::AddressParams,
},
#[command()]
Burn {
#[arg(long)]
bond_utxo: elements::OutPoint,
#[arg(long)]
bond_tx: String,
#[arg(long)]
spec: String,
#[arg(long)]
double_spend_utxo: bitcoin::OutPoint,
#[arg(long)]
double_spend_tx: String,
#[arg(long)]
tx1: String,
#[arg(long)]
tx2: String,
#[arg(long)]
reward_address: elements::Address,
#[arg(long, default_value_t = 1)]
feerate: u64,
},
#[command()]
Reclaim {
#[arg(long)]
bond_utxo: elements::OutPoint,
#[arg(long)]
bond_tx: String,
#[arg(long)]
spec: String,
#[arg(long)]
claim_address: elements::Address,
#[arg(long, default_value_t = 1)]
feerate: u64,
#[arg(long)]
reclaim_sk: Option<String>,
},
}
fn inner_main() -> Result<(), String> {
match App::parse() {
App::Create { segwit, network, pubkey, bond_value, bond_asset, expiry, reclaim_pubkey } => {
if !segwit {
return Err("please use the --segwit flag to indicate you want a segwit v0 bond")?;
}
let lock_time = lock_time_from_unix(expiry)?;
let spec = doubletake::segwit::BondSpec {
pubkey, bond_value, bond_asset, lock_time, reclaim_pubkey,
};
let (script, spk) = doubletake::segwit::create_bond_script(&spec);
let addr = elements::Address::from_script(&spk, None, network).expect("legit script");
serde_json::to_writer_pretty(::std::io::stdout(), &json!({
"spec": doubletake::BondSpec::Segwit(spec).to_base64(),
"address": addr.to_string(),
"witness_script": script.to_bytes().as_hex().to_string(),
})).unwrap();
println!();
},
App::Inspect { spec } => {
let spec = BondSpec::from_base64(&spec)
.map_err(|e| format!("invalid spec: {}", e))?;
let (ws, spk) = match spec {
BondSpec::Segwit(ref s) => doubletake::segwit::create_bond_script(&s),
_ => unreachable!(),
};
let mut json = serde_json::to_value(&spec).unwrap();
assert!(json.is_object());
let obj = json.as_object_mut().unwrap();
obj.insert("script_pubkey".into(), spk.to_bytes().as_hex().to_string().into());
obj.insert("witness_script".into(), ws.to_bytes().as_hex().to_string().into());
serde_json::to_writer_pretty(::std::io::stdout(), &json).unwrap();
println!();
},
App::Address { spec, network } => {
let spec = BondSpec::from_base64(&spec)
.map_err(|e| format!("invalid spec: {}", e))?;
let (_, spk) = match spec {
BondSpec::Segwit(ref s) => doubletake::segwit::create_bond_script(&s),
_ => unreachable!(),
};
let addr = elements::Address::from_script(&spk, None, network).expect("legit script");
println!("{}", addr);
},
App::Burn {
bond_utxo, bond_tx, spec, double_spend_utxo, double_spend_tx, tx1, tx2, feerate,
reward_address,
} => {
let utxo = ElementsUtxo {
outpoint: bond_utxo,
output: {
let tx = elem_deserialize_hex::<elements::Transaction>(&bond_tx)
.map_err(|e| format!("invalid bond tx hex: {}", e))?;
tx.output.get(bond_utxo.vout as usize)
.ok_or("invalid tx for bond UTXO")?
.clone()
},
};
let spec = BondSpec::from_base64(&spec)
.map_err(|e| format!("invalid spec: {}", e))?;
let double_spend_utxo = BitcoinUtxo {
outpoint: double_spend_utxo,
output: {
let tx = btc_deserialize_hex::<bitcoin::Transaction>(&double_spend_tx)
.map_err(|e| format!("invalid bond tx hex: {}", e))?;
tx.output.get(double_spend_utxo.vout as usize)
.ok_or("invalid tx for double spend UTXO")?
.clone()
},
};
let tx1 = elem_deserialize_hex(&tx1)
.map_err(|e| format!("bad tx1 hex: {}", e))?;
let tx2 = elem_deserialize_hex(&tx2)
.map_err(|e| format!("bad tx2 hex: {}", e))?;
let fee_rate = FeeRate::from_sat_per_vb(feerate).ok_or_else(|| "invalid feerate")?;
let tx = doubletake::create_burn_tx(
&utxo, &spec, &double_spend_utxo, &tx1, &tx2, fee_rate, &reward_address,
)?;
println!("{}", elements::encode::serialize_hex(&tx));
},
App::Reclaim { bond_utxo, bond_tx, spec, feerate, reclaim_sk, claim_address } => {
let utxo = ElementsUtxo {
outpoint: bond_utxo,
output: {
let tx = elem_deserialize_hex::<elements::Transaction>(&bond_tx)
.map_err(|e| format!("invalid bond tx hex: {}", e))?;
tx.output.get(bond_utxo.vout as usize)
.ok_or("invalid tx for bond UTXO")?
.clone()
},
};
let spec = BondSpec::from_base64(&spec)
.map_err(|e| format!("invalid spec: {}", e))?;
let fee_rate = FeeRate::from_sat_per_vb(feerate).ok_or_else(|| "invalid feerate")?;
let tx = if let Some(reclaim_sk) = reclaim_sk {
let reclaim_sk = parse_secret_key(&reclaim_sk)?;
doubletake::create_signed_ecdsa_reclaim_tx(
&utxo, &spec, fee_rate, &claim_address, &reclaim_sk,
)?
} else {
doubletake::create_unsigned_reclaim_tx(
&utxo, &spec, fee_rate, &claim_address,
)
};
println!("{}", elements::encode::serialize_hex(&tx));
},
}
Ok(())
}
fn main() {
if let Err(e) = inner_main() {
eprintln!("ERROR: {}", e);
}
}
fn btc_deserialize_hex<T: bitcoin::consensus::Decodable>(hex: &str) -> Result<T, String> {
let mut iter = hex_conservative::HexToBytesIter::new(hex)
.map_err(|e| format!("invalid hex string: {}", e))?;
Ok(T::consensus_decode(&mut iter).map_err(|e| format!("decoding failed: {}", e))?)
}
fn elem_deserialize_hex<T: elements::encode::Decodable>(hex: &str) -> Result<T, String> {
let mut iter = hex_conservative::HexToBytesIter::new(hex)
.map_err(|e| format!("invalid hex string: {}", e))?;
Ok(T::consensus_decode(&mut iter).map_err(|e| format!("decoding failed: {}", e))?)
}
fn parse_secret_key(s: &str) -> Result<SecretKey, String> {
if let Ok(k) = bitcoin::PrivateKey::from_str(&s) {
Ok(k.inner)
} else {
Ok(SecretKey::from_str(&s).map_err(|e| format!("invalid secret key: {}", e))?)
}
}
fn parse_elements_network(s: &str) -> Result<&'static elements::AddressParams, String> {
match s {
"liquid" => Ok(&elements::AddressParams::LIQUID),
"liquidtestnet" => Ok(&elements::AddressParams::LIQUID_TESTNET),
"elements" => Ok(&elements::AddressParams::ELEMENTS),
_ => Err("invalid network")?,
}
}
fn parse_asset_id(s: &str) -> Result<AssetId, String> {
match s {
"lbtc" => Ok(AssetId::LIQUID_BTC),
_ => Ok(AssetId::from_str(s).map_err(|_| "invalid asset id")?),
}
}
fn lock_time_from_unix(secs: u64) -> Result<elements::LockTime, String> {
let secs_u32 = secs.try_into().map_err(|_| "timelock overflow")?;
Ok(elements::LockTime::from_time(secs_u32).map_err(|e| format!("invalid timelock: {}", e))?)
}