#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#[cfg(feature = "blocking")]
pub mod blocking;
mod chain_data;
mod chain_swaps;
pub mod clients;
mod error;
mod invoice_data;
mod lightning_payment;
mod prepare_pay_data;
mod reverse;
mod submarine;
mod swap_state;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use bip39::Mnemonic;
use boltz_client::boltz::BoltzApiClientV2;
use boltz_client::boltz::BoltzWsApi;
use boltz_client::boltz::BoltzWsConfig;
use boltz_client::boltz::GetChainPairsResponse;
use boltz_client::boltz::GetReversePairsResponse;
use boltz_client::boltz::GetSubmarinePairsResponse;
use boltz_client::boltz::SwapStatus;
use boltz_client::boltz::BOLTZ_MAINNET_URL_V2;
use boltz_client::boltz::BOLTZ_REGTEST;
use boltz_client::boltz::BOLTZ_TESTNET_URL_V2;
#[cfg(not(target_arch = "wasm32"))]
use boltz_client::network::electrum::ElectrumBitcoinClient;
#[cfg(not(target_arch = "wasm32"))]
use boltz_client::network::electrum::DEFAULT_ELECTRUM_TIMEOUT;
use boltz_client::network::BitcoinChain;
use boltz_client::network::Chain;
use boltz_client::network::LiquidChain;
use boltz_client::swaps::ChainClient;
use boltz_client::util::secrets::Preimage;
use boltz_client::util::sleep;
use boltz_client::Keypair;
use lightning::bitcoin::XKeyIdentifier;
use lwk_wollet::asyncr::async_now;
use lwk_wollet::asyncr::async_sleep;
use lwk_wollet::bitcoin::bip32::ChildNumber;
use lwk_wollet::bitcoin::bip32::DerivationPath;
use lwk_wollet::bitcoin::bip32::Xpriv;
use lwk_wollet::bitcoin::bip32::Xpub;
use lwk_wollet::bitcoin::NetworkKind;
use lwk_wollet::ElectrumUrl;
use lwk_wollet::ElementsNetwork;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::error::TryRecvError;
pub use crate::chain_data::{to_chain_data, ChainSwapData, ChainSwapDataSerializable};
pub use crate::chain_swaps::LockupResponse;
use crate::clients::AnyClient;
pub use crate::error::Error;
pub use crate::invoice_data::to_invoice_data;
pub use crate::invoice_data::InvoiceData;
pub use crate::invoice_data::InvoiceDataSerializable;
pub use crate::lightning_payment::LightningPayment;
pub use crate::prepare_pay_data::PreparePayData;
pub use crate::prepare_pay_data::PreparePayDataSerializable;
pub use crate::reverse::InvoiceResponse;
pub use crate::submarine::PreparePayResponse;
pub use crate::swap_state::SwapState;
pub use boltz_client::boltz::ChainSwapStates;
pub use boltz_client::boltz::{RevSwapStates, SubSwapStates, SwapRestoreResponse, Webhook};
pub use boltz_client::Bolt11Invoice;
use lwk_wollet::hashes::sha256;
use lwk_wollet::hashes::Hash;
pub use boltz_client::boltz::SwapRestoreType as SwapType;
pub(crate) const WAIT_TIME: std::time::Duration = std::time::Duration::from_secs(5);
pub struct BoltzSession {
ws: Arc<BoltzWsApi>,
api: Arc<BoltzApiClientV2>,
chain_client: Arc<ChainClient>,
liquid_chain: LiquidChain,
timeout: Duration,
mnemonic: Mnemonic,
next_index_to_use: AtomicU32,
polling: bool,
timeout_advance: Duration,
referral_id: Option<String>,
random_preimages: bool,
submarine_pairs: GetSubmarinePairsResponse,
reverse_pairs: GetReversePairsResponse,
chain_pairs: GetChainPairsResponse,
}
impl BoltzSession {
pub async fn new(network: ElementsNetwork, client: AnyClient) -> Result<Self, Error> {
Self::builder(network, client).build().await
}
pub fn builder(network: ElementsNetwork, client: AnyClient) -> BoltzSessionBuilder {
BoltzSessionBuilder::new(network, client)
}
#[allow(clippy::too_many_arguments)] async fn initialize(
network: ElementsNetwork,
client: AnyClient,
timeout: Option<Duration>,
mnemonic: Option<Mnemonic>,
polling: bool,
timeout_advance: Option<Duration>,
next_index_to_use: Option<u32>,
referral_id: Option<String>,
_bitcoin_electrum_client: Option<ElectrumUrl>,
random_preimages: bool,
) -> Result<Self, Error> {
let liquid_chain = elements_network_to_liquid_chain(network);
#[cfg(feature = "blocking")]
let chain_client = {
let bitcoin_network = bitcoin_chain_from_network(network);
let bitcoin_client = match _bitcoin_electrum_client {
Some(ElectrumUrl::Tls(url, validate_domain)) => ElectrumBitcoinClient::new(
bitcoin_network,
&url,
true,
validate_domain,
DEFAULT_ELECTRUM_TIMEOUT,
)?,
Some(ElectrumUrl::Plaintext(url)) => ElectrumBitcoinClient::new(
bitcoin_network,
&url,
false,
false,
DEFAULT_ELECTRUM_TIMEOUT,
)?,
None => ElectrumBitcoinClient::default(bitcoin_network, None)?,
};
Arc::new(
ChainClient::new()
.with_liquid(client)
.with_bitcoin(bitcoin_client),
)
};
#[cfg(not(feature = "blocking"))]
let chain_client = Arc::new(ChainClient::new().with_liquid(client));
let url = boltz_default_url(network);
let api = Arc::new(BoltzApiClientV2::new(url.to_string(), timeout));
let config = BoltzWsConfig::default();
let ws_url = url.replace("http", "ws") + "/ws"; let ws = Arc::new(BoltzWsApi::new(ws_url, config));
start_ws(ws.clone());
let (submarine_pairs, reverse_pairs, chain_pairs) = tokio::try_join!(
api.get_submarine_pairs(),
api.get_reverse_pairs(),
api.get_chain_pairs(),
)?;
let (next_index_to_use, mnemonic) = match mnemonic {
Some(mnemonic) => {
let next_index_to_use = match next_index_to_use {
Some(next_index_to_use) => next_index_to_use,
None => {
fetch_next_index_to_use(&mnemonic, network_kind(liquid_chain), &api).await?
}
};
(next_index_to_use, mnemonic)
}
None => (0, Mnemonic::generate(12).expect("12 is a valid word count")),
};
Ok(Self {
next_index_to_use: AtomicU32::new(next_index_to_use),
mnemonic,
ws,
api,
chain_client,
liquid_chain,
timeout: timeout.unwrap_or(Duration::from_secs(10)),
polling,
timeout_advance: timeout_advance.unwrap_or(Duration::from_secs(180)),
referral_id,
random_preimages,
submarine_pairs,
reverse_pairs,
chain_pairs,
})
}
fn chain(&self) -> Chain {
Chain::Liquid(self.liquid_chain)
}
fn btc_chain(&self) -> Chain {
match self.liquid_chain {
LiquidChain::Liquid => Chain::Bitcoin(BitcoinChain::Bitcoin),
LiquidChain::LiquidTestnet => Chain::Bitcoin(BitcoinChain::BitcoinTestnet),
LiquidChain::LiquidRegtest => Chain::Bitcoin(BitcoinChain::BitcoinRegtest),
}
}
fn network(&self) -> ElementsNetwork {
liquid_chain_to_elements_network(self.liquid_chain)
}
fn derive_next_keypair(&self) -> Result<(u32, Keypair), Error> {
let index = self.next_index_to_use.fetch_add(1, Ordering::Relaxed);
let keypair = derive_keypair(index, &self.mnemonic)?;
Ok((index, keypair))
}
pub fn next_index_to_use(&self) -> u32 {
self.next_index_to_use.load(Ordering::Relaxed)
}
pub fn set_next_index_to_use(&self, next_index_to_use: u32) {
self.next_index_to_use
.store(next_index_to_use, Ordering::Relaxed);
}
pub fn rescue_file(&self) -> RescueFile {
RescueFile {
mnemonic: self.mnemonic.to_string(),
}
}
pub async fn swap_restore(&self) -> Result<Vec<SwapRestoreResponse>, Error> {
let xpub = derive_xpub_from_mnemonic(&self.mnemonic, network_kind(self.liquid_chain))?;
let result = self.api.post_swap_restore(&xpub.to_string()).await?;
Ok(result)
}
pub async fn fetch_swaps_info(
&self,
) -> Result<
(
GetReversePairsResponse,
GetSubmarinePairsResponse,
GetChainPairsResponse,
),
Error,
> {
let a = self.api.get_reverse_pairs().await?;
let b = self.api.get_submarine_pairs().await?;
let c = self.api.get_chain_pairs().await?;
Ok((a, b, c))
}
pub(crate) fn preimage(&self, our_keys: &Keypair) -> Preimage {
if self.random_preimages {
Preimage::random()
} else {
preimage_from_keypair(our_keys)
}
}
}
#[cfg(feature = "blocking")]
fn bitcoin_chain_from_network(network: ElementsNetwork) -> BitcoinChain {
match network {
ElementsNetwork::Liquid => BitcoinChain::Bitcoin,
ElementsNetwork::LiquidTestnet => BitcoinChain::BitcoinTestnet,
ElementsNetwork::ElementsRegtest { .. } => BitcoinChain::BitcoinRegtest,
}
}
pub fn start_ws(ws: Arc<BoltzWsApi>) {
let future = ws.run_ws_loop();
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
{
tokio::spawn(future);
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
{
wasm_bindgen_futures::spawn_local(future);
}
}
pub struct BoltzSessionBuilder {
network: ElementsNetwork,
client: AnyClient,
create_swap_timeout: Option<Duration>,
mnemonic: Option<Mnemonic>,
polling: bool,
timeout_advance: Option<Duration>,
next_index_to_use: Option<u32>,
referral_id: Option<String>,
bitcoin_electrum_client: Option<ElectrumUrl>,
random_preimages: bool,
}
impl BoltzSessionBuilder {
pub fn new(network: ElementsNetwork, client: AnyClient) -> Self {
Self {
network,
client,
create_swap_timeout: None,
mnemonic: None,
polling: false,
timeout_advance: None,
next_index_to_use: None,
referral_id: None,
bitcoin_electrum_client: None,
random_preimages: false,
}
}
pub fn create_swap_timeout(mut self, timeout: Duration) -> Self {
self.create_swap_timeout = Some(timeout);
self
}
pub fn timeout_advance(mut self, timeout: Duration) -> Self {
self.timeout_advance = Some(timeout);
self
}
pub fn mnemonic(mut self, mnemonic: Mnemonic) -> Self {
self.mnemonic = Some(mnemonic);
self
}
pub fn polling(mut self, polling: bool) -> Self {
self.polling = polling;
self
}
pub fn next_index_to_use(mut self, next_index_to_use: u32) -> Self {
self.next_index_to_use = Some(next_index_to_use);
self
}
pub fn referral_id(mut self, referral_id: String) -> Self {
self.referral_id = Some(referral_id);
self
}
pub fn bitcoin_electrum_client(mut self, bitcoin_electrum_client: &str) -> Result<Self, Error> {
let url = bitcoin_electrum_client.parse::<ElectrumUrl>()?;
self.bitcoin_electrum_client = Some(url);
Ok(self)
}
pub fn random_preimages(mut self, random_preimages: bool) -> Self {
self.random_preimages = random_preimages;
self
}
pub async fn build(self) -> Result<BoltzSession, Error> {
BoltzSession::initialize(
self.network,
self.client,
self.create_swap_timeout,
self.mnemonic,
self.polling,
self.timeout_advance,
self.next_index_to_use,
self.referral_id,
self.bitcoin_electrum_client,
self.random_preimages,
)
.await
}
#[cfg(feature = "blocking")]
pub fn build_blocking(self) -> Result<blocking::BoltzSession, Error> {
let runtime = Arc::new(tokio::runtime::Runtime::new()?);
let _guard = runtime.enter();
let inner = runtime.block_on(self.build())?;
Ok(blocking::BoltzSession::new_from_async(inner, runtime))
}
}
#[derive(Deserialize, Serialize)]
pub struct RescueFile {
mnemonic: String,
}
fn network_kind(liquid_chain: LiquidChain) -> NetworkKind {
if liquid_chain == LiquidChain::Liquid {
NetworkKind::Main
} else {
NetworkKind::Test
}
}
pub(crate) fn preimage_from_keypair(our_keys: &Keypair) -> Preimage {
let hashed_bytes = sha256::Hash::hash(&our_keys.secret_bytes());
Preimage::from_vec(hashed_bytes.as_byte_array().to_vec()).expect("sha256 result is 32 bytes")
}
pub(crate) fn mnemonic_identifier(mnemonic: &Mnemonic) -> Result<XKeyIdentifier, Error> {
let seed = mnemonic.to_seed("");
let xpriv = Xpriv::new_master(NetworkKind::Test, &seed[..])?;
Ok(xpriv.identifier(&lwk_wollet::EC))
}
async fn fetch_next_index_to_use(
mnemonic: &Mnemonic,
network_kind: NetworkKind,
client: &BoltzApiClientV2,
) -> Result<u32, Error> {
let xpub = derive_xpub_from_mnemonic(mnemonic, network_kind)?;
log::info!("xpub for restore is: {xpub}");
let result = client.post_swap_restore_index(&xpub.to_string()).await?;
let next_index_to_use = (result.index + 1) as u32;
log::info!("next index to use is: {next_index_to_use}");
Ok(next_index_to_use)
}
pub fn elements_network_to_liquid_chain(network: ElementsNetwork) -> LiquidChain {
match network {
ElementsNetwork::Liquid => LiquidChain::Liquid,
ElementsNetwork::LiquidTestnet => LiquidChain::LiquidTestnet,
ElementsNetwork::ElementsRegtest { .. } => LiquidChain::LiquidRegtest,
}
}
pub fn liquid_chain_to_elements_network(chain: LiquidChain) -> ElementsNetwork {
match chain {
LiquidChain::Liquid => ElementsNetwork::Liquid,
LiquidChain::LiquidTestnet => ElementsNetwork::LiquidTestnet,
LiquidChain::LiquidRegtest => ElementsNetwork::default_regtest(),
}
}
fn derive_xpub_from_mnemonic(
mnemonic: &Mnemonic,
network_kind: NetworkKind,
) -> Result<Xpub, Error> {
let seed = mnemonic.to_seed("");
let xpriv = Xpriv::new_master(network_kind, &seed[..])?;
let derivation_path = DerivationPath::master();
let derived = xpriv.derive_priv(&lwk_wollet::EC, &derivation_path)?;
Ok(Xpub::from_priv(&lwk_wollet::EC, &derived))
}
pub fn boltz_default_url(network: ElementsNetwork) -> &'static str {
match network {
ElementsNetwork::Liquid => BOLTZ_MAINNET_URL_V2,
ElementsNetwork::LiquidTestnet => BOLTZ_TESTNET_URL_V2,
ElementsNetwork::ElementsRegtest { .. } => BOLTZ_REGTEST,
}
}
pub async fn next_status(
rx: &mut tokio::sync::broadcast::Receiver<SwapStatus>,
timeout: Duration,
swap_id: &str,
polling: bool,
) -> Result<SwapStatus, Error> {
let deadline = async_now().await + timeout.as_millis() as u64;
loop {
let update = if polling {
match rx.try_recv() {
Ok(update) => update,
Err(TryRecvError::Empty) => {
return Err(Error::NoBoltzUpdate);
}
Err(e) => return Err(e.into()),
}
} else {
let remaining = deadline - async_now().await;
tokio::select! {
update = rx.recv() => update?,
_ = async_sleep(remaining) => {
log::warn!("Timeout while waiting state for swap id {swap_id}");
return Err(Error::Timeout(swap_id.to_string()));
}
}
};
if update.id != swap_id {
log::debug!(
"Ignoring update for different swap: {} (waiting for {})",
update.id,
swap_id
);
continue;
}
log::info!(
"Received update on swap {swap_id}. status:{}",
update.status
);
return Ok(update);
}
}
pub(crate) fn derive_keypair(index: u32, mnemonic: &Mnemonic) -> Result<Keypair, Error> {
let derivation_path = DerivationPath::from(vec![
ChildNumber::from_normal_idx(44)?,
ChildNumber::from_normal_idx(0)?,
ChildNumber::from_normal_idx(0)?,
ChildNumber::from_normal_idx(0)?,
ChildNumber::from_normal_idx(index)?,
]);
let seed = mnemonic.to_seed("");
let xpriv = Xpriv::new_master(NetworkKind::Test, &seed[..])?; let derived = xpriv.derive_priv(&lwk_wollet::EC, &derivation_path)?;
log::info!("derive_next_keypair with index: {index}");
let keypair = Keypair::from_seckey_slice(&lwk_wollet::EC, &derived.private_key.secret_bytes())?;
Ok(keypair)
}
pub async fn broadcast_tx_with_retry(
chain_client: &ChainClient,
tx: &boltz_client::swaps::BtcLikeTransaction,
) -> Result<String, Error> {
for _ in 0..30 {
match chain_client.broadcast_tx(tx).await {
Ok(txid) => return Ok(txid),
Err(e) => {
log::info!("Failed broadcast {e}, retrying in 1 second");
sleep(Duration::from_secs(1)).await;
}
}
}
Err(Error::RetryBroadcastFailed)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use bip39::Mnemonic;
use boltz_client::boltz::SwapRestoreResponse;
use lightning::offers::offer::Offer;
use lwk_wollet::bitcoin::NetworkKind;
use crate::derive_xpub_from_mnemonic;
#[test]
fn test_elements_network_to_liquid_chain() {
let networks = vec![
lwk_wollet::ElementsNetwork::Liquid,
lwk_wollet::ElementsNetwork::LiquidTestnet,
lwk_wollet::ElementsNetwork::default_regtest(),
];
for network in networks {
let chain = crate::elements_network_to_liquid_chain(network);
let roundtrip_network = crate::liquid_chain_to_elements_network(chain);
assert_eq!(network, roundtrip_network);
}
}
#[test]
fn test_derive_xpub_from_mnemonic() {
let mnemonic = "damp cart merit asset obvious idea chef traffic absent armed road link";
let expected_xpub = "xpub661MyMwAqRbcGprhd8RLPkaDpHxrJxiSWUUibirDPMnsvmUTW3djk2S3wsaz21ASEdw4uXQAypXA4CZ9u5EhCnXtLgfwck5PwXNRgvcaDUm";
let mnemonic: Mnemonic = mnemonic.parse().unwrap();
let network_kind = NetworkKind::Main;
let xpub = derive_xpub_from_mnemonic(&mnemonic, network_kind).unwrap();
assert_eq!(xpub.to_string(), expected_xpub);
}
#[test]
fn test_derive_keypair() {
let mnemonic = "damp cart merit asset obvious idea chef traffic absent armed road link";
let expected_keypair_pubkey =
"0315a98cf1610e96ca92505c6e9536a208353399685440869dca58947a909d07ed";
let mnemonic: Mnemonic = mnemonic.parse().unwrap();
let index = 0;
let keypair = crate::derive_keypair(index, &mnemonic).unwrap();
assert_eq!(keypair.public_key().to_string(), expected_keypair_pubkey);
}
#[test]
fn test_bolt12() {
let bolt12_str = "lno1zcss9sy46p548rukhu2vt7g0dsy9r00n2jswepsrngjt7w988ac94hpv";
let bolt12 = Offer::from_str(bolt12_str).unwrap();
assert_eq!(bolt12.to_string(), bolt12_str);
}
#[test]
fn test_parse_swap_restore() {
let data = include_str!("../tests/data/swap_restore_response.json");
let data: Vec<SwapRestoreResponse> = serde_json::from_str(data).unwrap();
assert_eq!(data.len(), 32);
}
}