use std::str::FromStr;
use bitcoin::{Amount, FeeRate};
use bitcoin::secp256k1::SecretKey;
use elements::AssetId;
use hex_conservative::{DisplayHex, FromHex};
use wasm_bindgen::prelude::*;
use crate::{segwit, BitcoinUtxo, BondSpec, ElementsUtxo};
#[wasm_bindgen]
pub fn create_segwit_bond_spec(
pubkey: &str,
bond_value_sat: u64,
bond_asset: &str,
lock_time_unix: u64,
reclaim_pubkey: &str,
) -> Result<String, JsValue> {
console_error_panic_hook::set_once();
let pubkey = pubkey.parse().map_err(|e| format!("invalid pubkey: {}", e))?;
let bond_value = Amount::from_sat(bond_value_sat);
let bond_asset = parse_asset_id(bond_asset)?;
let lock_time = lock_time_from_unix(lock_time_unix)?;
let reclaim_pubkey = reclaim_pubkey.parse().map_err(|e| format!("invalid pubkey: {}", e))?;
let spec = segwit::BondSpec { pubkey, bond_value, bond_asset, lock_time, reclaim_pubkey };
Ok(BondSpec::Segwit(spec).to_base64())
}
#[wasm_bindgen]
pub fn inspect_bond(spec: &str) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let spec = BondSpec::from_base64(&spec)
.map_err(|e| format!("invalid spec: {}", e))?;
let (ws, spk) = match spec {
BondSpec::Segwit(ref s) => segwit::create_bond_script(&s),
};
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());
Ok(serde_wasm_bindgen::to_value(&json).unwrap())
}
#[wasm_bindgen]
pub fn bond_address(spec: &str, network: &str) -> Result<String, JsValue> {
console_error_panic_hook::set_once();
let spec = BondSpec::from_base64(&spec)
.map_err(|e| format!("invalid spec: {}", e))?;
let network = parse_elements_network(network)?;
let (_, spk) = match spec {
BondSpec::Segwit(ref s) => segwit::create_bond_script(&s),
};
let addr = elements::Address::from_script(&spk, None, network).expect("valid spk");
Ok(addr.to_string())
}
#[wasm_bindgen]
pub fn create_bitcoin_utxo(
tx: &str,
vout: usize,
) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let tx = btc_deserialize_hex::<bitcoin::Transaction>(tx)
.map_err(|e| format!("invalid tx: {}", e))?;
let ret = BitcoinUtxo {
outpoint: bitcoin::OutPoint::new(tx.txid(), vout.try_into().expect("vout overflow")),
output: tx.output.get(vout).ok_or("vout invalid for tx: too few outputs")?.clone(),
};
Ok(serde_wasm_bindgen::to_value(&ret).unwrap())
}
#[wasm_bindgen]
pub fn create_elements_utxo(
tx: &str,
vout: usize,
) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let tx = elem_deserialize_hex::<elements::Transaction>(tx)
.map_err(|e| format!("invalid tx: {}", e))?;
let output = tx.output.get(vout).ok_or("vout invalid for tx: too few outputs")?.clone();
let ret = ElementsUtxo {
outpoint: elements::OutPoint::new(tx.txid(), vout as u32),
output: output,
};
Ok(serde_wasm_bindgen::to_value(&ret).unwrap())
}
#[wasm_bindgen]
pub fn create_burn_tx(
bond_utxo: &str,
bond_tx: &str,
spec_base64: &str,
double_spend_utxo: &str,
double_spend_tx: &str,
tx1_hex: &str,
tx2_hex: &str,
fee_rate_sat_per_vb: u64,
reward_address: &str,
) -> Result<String, JsValue> {
console_error_panic_hook::set_once();
let utxo_outpoint = elements::OutPoint::from_str(bond_utxo)
.map_err(|e| format!("invalid bond UTXO outpoint: {}", e))?;
let utxo = ElementsUtxo {
outpoint: utxo_outpoint,
output: elem_deserialize_hex::<elements::Transaction>(bond_tx)
.map_err(|e| format!("invalid bond tx: {}", e))?
.output.get(utxo_outpoint.vout as usize)
.ok_or("bond tx and outpoint don't match")?.clone(),
};
let spec = BondSpec::from_base64(spec_base64)
.map_err(|e| format!("invalid spec: {}", e))?;
let double_spend_outpoint = bitcoin::OutPoint::from_str(double_spend_utxo)
.map_err(|e| format!("invalid bond UTXO: {}", e))?;
let double_spend_utxo = BitcoinUtxo {
outpoint: double_spend_outpoint,
output: btc_deserialize_hex::<bitcoin::Transaction>(double_spend_tx)
.map_err(|e| format!("invalid double spend tx: {}", e))?
.output.get(double_spend_outpoint.vout as usize)
.ok_or("double spend tx and outpoint don't match")?.clone(),
};
let tx1 = elem_deserialize_hex(tx1_hex)
.map_err(|e| format!("bad tx1_hex: {}", e))?;
let tx2 = elem_deserialize_hex(tx2_hex)
.map_err(|e| format!("bad tx2_hex: {}", e))?;
let fee_rate = FeeRate::from_sat_per_vb(fee_rate_sat_per_vb)
.ok_or_else(|| "invalid feerate")?;
let reward_address = elements::Address::from_str(reward_address)
.map_err(|e| format!("invalid reward address: {}", e))?;
let tx = crate::create_burn_tx(
&utxo, &spec, &double_spend_utxo, &tx1, &tx2, fee_rate, &reward_address,
)?;
Ok(elements::encode::serialize_hex(&tx))
}
#[wasm_bindgen]
pub fn create_unsigned_reclaim_tx(
bond_utxo: &str,
bond_tx: &str,
spec_base64: &str,
fee_rate_sat_per_vb: u64,
claim_address: &str,
) -> Result<String, JsValue> {
console_error_panic_hook::set_once();
let utxo_outpoint = elements::OutPoint::from_str(bond_utxo)
.map_err(|e| format!("invalid bond UTXO outpoint: {}", e))?;
let utxo = ElementsUtxo {
outpoint: utxo_outpoint,
output: elem_deserialize_hex::<elements::Transaction>(bond_tx)
.map_err(|e| format!("invalid bond tx: {}", e))?
.output.get(utxo_outpoint.vout as usize)
.ok_or("bond tx and outpoint don't match")?.clone(),
};
let spec = BondSpec::from_base64(spec_base64)
.map_err(|e| format!("invalid spec: {}", e))?;
let fee_rate = FeeRate::from_sat_per_vb(fee_rate_sat_per_vb).ok_or_else(|| "invalid feerate")?;
let claim_address = elements::Address::from_str(claim_address)
.map_err(|e| format!("invalid reward address: {}", e))?;
let tx = crate::create_unsigned_reclaim_tx(&utxo, &spec, fee_rate, &claim_address);
Ok(elements::encode::serialize_hex(&tx))
}
#[wasm_bindgen]
pub fn finalize_ecdsa_reclaim_tx(
spec_base64: &str,
reclaim_tx: &str,
signature: &str,
) -> Result<String, JsValue> {
let spec = BondSpec::from_base64(spec_base64)
.map_err(|e| format!("invalid spec: {}", e))?;
let reclaim_tx = elem_deserialize_hex::<elements::Transaction>(reclaim_tx)
.map_err(|e| format!("invalid reclaim tx: {}", e))?;
let signature_bytes = Vec::<u8>::from_hex(signature).map_err(|_| "invalid signature hex")?;
let signature = crate::util::parse_ecdsa_signature_all(&signature_bytes)?;
let ret = crate::finalize_ecdsa_reclaim_tx(&spec, reclaim_tx, signature)?;
Ok(elements::encode::serialize_hex(&ret))
}
#[wasm_bindgen]
pub fn create_signed_ecdsa_reclaim_tx(
bond_utxo: &str,
bond_tx: &str,
spec_base64: &str,
fee_rate_sat_per_vb: u64,
claim_address: &str,
reclaim_sk: &str,
) -> Result<String, JsValue> {
console_error_panic_hook::set_once();
let utxo_outpoint = elements::OutPoint::from_str(bond_utxo)
.map_err(|e| format!("invalid bond UTXO outpoint: {}", e))?;
let utxo = ElementsUtxo {
outpoint: utxo_outpoint,
output: elem_deserialize_hex::<elements::Transaction>(bond_tx)
.map_err(|e| format!("invalid bond tx: {}", e))?
.output.get(utxo_outpoint.vout as usize)
.ok_or("bond tx and outpoint don't match")?.clone(),
};
let spec = BondSpec::from_base64(spec_base64)
.map_err(|e| format!("invalid spec: {}", e))?;
let fee_rate = FeeRate::from_sat_per_vb(fee_rate_sat_per_vb).ok_or_else(|| "invalid feerate")?;
let reclaim_sk = parse_secret_key(reclaim_sk)?;
let claim_address = elements::Address::from_str(claim_address)
.map_err(|e| format!("invalid reward address: {}", e))?;
let tx = crate::create_signed_ecdsa_reclaim_tx(
&utxo, &spec, fee_rate, &claim_address, &reclaim_sk,
)?;
Ok(elements::encode::serialize_hex(&tx))
}
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(|_| "invalid secret key")?)
}
}
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))?)
}