mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-19 14:04:22 +01:00
abstract onchain wallet behind its own module and trait
This commit is contained in:
@@ -10,3 +10,4 @@ pub mod persist;
|
||||
pub mod sdk;
|
||||
pub(crate) mod swapper;
|
||||
pub(crate) mod utils;
|
||||
pub(crate) mod wallet;
|
||||
|
||||
@@ -6,8 +6,7 @@ use boltz_client::swaps::boltzv2::{
|
||||
BOLTZ_TESTNET_URL_V2,
|
||||
};
|
||||
use boltz_client::{Keypair, LBtcSwapScriptV2, ToHex};
|
||||
use lwk_signer::SwSigner;
|
||||
use lwk_wollet::{ElectrumClient, ElectrumUrl, ElementsNetwork, WolletDescriptor};
|
||||
use lwk_wollet::{ElectrumClient, ElectrumUrl, ElementsNetwork};
|
||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
|
||||
use rusqlite::ToSql;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -128,15 +127,6 @@ pub enum LiquidSdkEvent {
|
||||
Synced,
|
||||
}
|
||||
|
||||
pub struct LiquidSdkOptions {
|
||||
pub config: Config,
|
||||
pub signer: SwSigner,
|
||||
/// Output script descriptor
|
||||
///
|
||||
/// See <https://github.com/bitcoin/bips/pull/1143>
|
||||
pub descriptor: WolletDescriptor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConnectRequest {
|
||||
pub mnemonic: String,
|
||||
|
||||
@@ -9,17 +9,12 @@ use boltz_client::{
|
||||
boltzv2::*,
|
||||
},
|
||||
util::secrets::Preimage,
|
||||
Amount, Bolt11Invoice, ElementsAddress,
|
||||
Amount, Bolt11Invoice,
|
||||
};
|
||||
use log::{debug, error, info, warn};
|
||||
use lwk_common::{singlesig_desc, Signer, Singlesig};
|
||||
use lwk_signer::{AnySigner, SwSigner};
|
||||
use lwk_wollet::bitcoin::Witness;
|
||||
use lwk_wollet::hashes::{sha256, Hash};
|
||||
use lwk_wollet::{
|
||||
elements::{Address, LockTime, Transaction},
|
||||
BlockchainBackend, ElementsNetwork, FsPersister, Wollet as LwkWollet, WolletDescriptor,
|
||||
};
|
||||
use lwk_wollet::{elements::LockTime, BlockchainBackend, ElementsNetwork};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
fs,
|
||||
@@ -28,12 +23,13 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use tokio::sync::{watch, RwLock};
|
||||
use tokio::time::MissedTickBehavior;
|
||||
|
||||
use crate::error::LiquidSdkError;
|
||||
use crate::model::PaymentState::*;
|
||||
use crate::swapper::{BoltzSwapper, ReconnectHandler, Swapper, SwapperStatusStream};
|
||||
use crate::wallet::{LiquidOnchainWallet, OnchainWallet};
|
||||
use crate::{
|
||||
ensure_sdk,
|
||||
error::{LiquidSdkResult, PaymentError},
|
||||
@@ -52,10 +48,7 @@ pub const DEFAULT_DATA_DIR: &str = ".data";
|
||||
|
||||
pub struct LiquidSdk {
|
||||
config: Config,
|
||||
/// LWK Wollet, a watch-only Liquid wallet for this instance
|
||||
lwk_wollet: Arc<Mutex<LwkWollet>>,
|
||||
/// LWK Signer, for signing Liquid transactions
|
||||
lwk_signer: SwSigner,
|
||||
onchain_wallet: Arc<dyn OnchainWallet>,
|
||||
persister: Arc<Persister>,
|
||||
event_manager: Arc<EventManager>,
|
||||
status_stream: Arc<dyn SwapperStatusStream>,
|
||||
@@ -68,33 +61,15 @@ pub struct LiquidSdk {
|
||||
impl LiquidSdk {
|
||||
pub async fn connect(req: ConnectRequest) -> Result<Arc<LiquidSdk>> {
|
||||
let config = req.config;
|
||||
let is_mainnet = config.network == Network::Mainnet;
|
||||
let signer = SwSigner::new(&req.mnemonic, is_mainnet)?;
|
||||
let descriptor = LiquidSdk::get_descriptor(&signer, config.network)?;
|
||||
|
||||
let sdk = LiquidSdk::new(LiquidSdkOptions {
|
||||
signer,
|
||||
descriptor,
|
||||
config,
|
||||
})?;
|
||||
let sdk = LiquidSdk::new(config, req.mnemonic)?;
|
||||
sdk.start().await?;
|
||||
|
||||
Ok(sdk)
|
||||
}
|
||||
|
||||
fn new(opts: LiquidSdkOptions) -> Result<Arc<Self>> {
|
||||
let config = opts.config;
|
||||
let elements_network: ElementsNetwork = config.network.into();
|
||||
|
||||
fn new(config: Config, mnemonic: String) -> Result<Arc<Self>> {
|
||||
fs::create_dir_all(&config.working_dir)?;
|
||||
|
||||
let lwk_persister = FsPersister::new(
|
||||
config.working_dir.clone(),
|
||||
elements_network,
|
||||
&opts.descriptor,
|
||||
)?;
|
||||
let lwk_wollet = LwkWollet::new(elements_network, lwk_persister, opts.descriptor)?;
|
||||
|
||||
let persister = Arc::new(Persister::new(&config.working_dir, config.network)?);
|
||||
persister.init()?;
|
||||
|
||||
@@ -105,9 +80,8 @@ impl LiquidSdk {
|
||||
let status_stream = Arc::<dyn SwapperStatusStream>::from(swapper.create_status_stream());
|
||||
|
||||
let sdk = Arc::new(LiquidSdk {
|
||||
config,
|
||||
lwk_wollet: Arc::new(Mutex::new(lwk_wollet)),
|
||||
lwk_signer: opts.signer,
|
||||
config: config.clone(),
|
||||
onchain_wallet: Arc::new(LiquidOnchainWallet::new(mnemonic, config)?),
|
||||
persister: persister.clone(),
|
||||
event_manager,
|
||||
status_stream: status_stream.clone(),
|
||||
@@ -268,7 +242,7 @@ impl LiquidSdk {
|
||||
async fn check_send_swap_expiration(&self, send_swap: &SendSwap) -> Result<()> {
|
||||
if send_swap.lockup_tx_id.is_some() && send_swap.refund_tx_id.is_none() {
|
||||
let swap_script = send_swap.get_swap_script()?;
|
||||
let current_height = self.lwk_wollet.lock().await.tip().height();
|
||||
let current_height = self.onchain_wallet.tip().await.height();
|
||||
let locktime_from_height = LockTime::from_height(current_height)?;
|
||||
|
||||
info!("Checking Send Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", send_swap.id, swap_script.locktime);
|
||||
@@ -300,18 +274,6 @@ impl LiquidSdk {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_descriptor(signer: &SwSigner, network: Network) -> Result<WolletDescriptor> {
|
||||
let is_mainnet = network == Network::Mainnet;
|
||||
let descriptor_str = singlesig_desc(
|
||||
signer,
|
||||
Singlesig::Wpkh,
|
||||
lwk_common::DescriptorBlindingKey::Slip77,
|
||||
is_mainnet,
|
||||
)
|
||||
.map_err(|e| anyhow!("Invalid descriptor: {e}"))?;
|
||||
Ok(descriptor_str.parse()?)
|
||||
}
|
||||
|
||||
fn validate_state_transition(
|
||||
from_state: PaymentState,
|
||||
to_state: PaymentState,
|
||||
@@ -686,16 +648,12 @@ impl LiquidSdk {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the next unused onchain Liquid address
|
||||
async fn next_unused_address(&self) -> Result<Address, lwk_wollet::Error> {
|
||||
let lwk_wollet = self.lwk_wollet.lock().await;
|
||||
Ok(lwk_wollet.address(None)?.address().clone())
|
||||
}
|
||||
|
||||
pub async fn get_info(&self, req: GetInfoRequest) -> Result<GetInfoResponse> {
|
||||
self.ensure_is_started().await?;
|
||||
|
||||
debug!("next_unused_address: {}", self.next_unused_address().await?);
|
||||
debug!(
|
||||
"next_unused_address: {}",
|
||||
self.onchain_wallet.next_unused_address().await?
|
||||
);
|
||||
if req.with_scan {
|
||||
self.sync().await?;
|
||||
}
|
||||
@@ -735,35 +693,10 @@ impl LiquidSdk {
|
||||
balance_sat: confirmed_received_sat - confirmed_sent_sat - pending_send_sat,
|
||||
pending_send_sat,
|
||||
pending_receive_sat,
|
||||
pubkey: self.lwk_signer.xpub().public_key.to_string(),
|
||||
pubkey: self.onchain_wallet.pubkey().await,
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_tx(
|
||||
&self,
|
||||
fee_rate: Option<f32>,
|
||||
recipient_address: &str,
|
||||
amount_sat: u64,
|
||||
) -> Result<Transaction, PaymentError> {
|
||||
let lwk_wollet = self.lwk_wollet.lock().await;
|
||||
let mut pset = lwk_wollet::TxBuilder::new(self.config.network.into())
|
||||
.add_lbtc_recipient(
|
||||
&ElementsAddress::from_str(recipient_address).map_err(|e| {
|
||||
PaymentError::Generic {
|
||||
err: format!(
|
||||
"Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
|
||||
),
|
||||
}
|
||||
})?,
|
||||
amount_sat,
|
||||
)?
|
||||
.fee_rate(fee_rate)
|
||||
.finish(&lwk_wollet)?;
|
||||
let signer = AnySigner::Software(self.lwk_signer.clone());
|
||||
signer.sign(&mut pset)?;
|
||||
Ok(lwk_wollet.finalize(&mut pset)?)
|
||||
}
|
||||
|
||||
fn validate_invoice(&self, invoice: &str) -> Result<Bolt11Invoice, PaymentError> {
|
||||
let invoice = invoice
|
||||
.trim()
|
||||
@@ -812,6 +745,7 @@ impl LiquidSdk {
|
||||
|
||||
// Create a throw-away tx similar to the lockup tx, in order to estimate fees
|
||||
Ok(self
|
||||
.onchain_wallet
|
||||
.build_tx(None, temp_p2tr_addr, amount_sat)
|
||||
.await?
|
||||
.all_fees()
|
||||
@@ -864,8 +798,8 @@ impl LiquidSdk {
|
||||
&swap.id
|
||||
);
|
||||
|
||||
let current_height = self.lwk_wollet.lock().await.tip().height();
|
||||
let output_address = self.next_unused_address().await?.to_string();
|
||||
let current_height = self.onchain_wallet.tip().await.height();
|
||||
let output_address = self.onchain_wallet.next_unused_address().await?.to_string();
|
||||
let refund_tx_id = self.swapper.refund_send_swap_non_cooperative(
|
||||
swap,
|
||||
broadcast_fees_sat,
|
||||
@@ -885,7 +819,7 @@ impl LiquidSdk {
|
||||
let broadcast_fees_sat =
|
||||
Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat).await?);
|
||||
|
||||
let output_address = self.next_unused_address().await?.to_string();
|
||||
let output_address = self.onchain_wallet.next_unused_address().await?.to_string();
|
||||
let refund_res =
|
||||
self.swapper
|
||||
.refund_send_swap_cooperative(swap, &output_address, broadcast_fees_sat);
|
||||
@@ -917,7 +851,7 @@ impl LiquidSdk {
|
||||
"Claim is pending for Send Swap {}. Initiating cooperative claim",
|
||||
&send_swap.id
|
||||
);
|
||||
let output_address = self.next_unused_address().await?.to_string();
|
||||
let output_address = self.onchain_wallet.next_unused_address().await?.to_string();
|
||||
let claim_tx_details = self.swapper.get_claim_tx_details(send_swap)?;
|
||||
self.try_handle_send_swap_update(
|
||||
&send_swap.id,
|
||||
@@ -943,6 +877,7 @@ impl LiquidSdk {
|
||||
);
|
||||
|
||||
let lockup_tx = self
|
||||
.onchain_wallet
|
||||
.build_tx(
|
||||
None,
|
||||
&create_response.address,
|
||||
@@ -1087,7 +1022,7 @@ impl LiquidSdk {
|
||||
PaymentError::AlreadyClaimed
|
||||
);
|
||||
let swap_id = &ongoing_receive_swap.id;
|
||||
let claim_address = self.next_unused_address().await?.to_string();
|
||||
let claim_address = self.onchain_wallet.next_unused_address().await?.to_string();
|
||||
let claim_tx_id = self
|
||||
.swapper
|
||||
.claim_receive_swap(ongoing_receive_swap, claim_address)?;
|
||||
@@ -1218,9 +1153,7 @@ impl LiquidSdk {
|
||||
/// it inserts or updates a corresponding entry in our Payments table.
|
||||
async fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> {
|
||||
if with_scan {
|
||||
let mut electrum_client = self.config.get_electrum_client()?;
|
||||
let mut lwk_wollet = self.lwk_wollet.lock().await;
|
||||
lwk_wollet::full_scan_with_electrum_client(&mut lwk_wollet, &mut electrum_client)?;
|
||||
self.onchain_wallet.full_scan().await?;
|
||||
}
|
||||
|
||||
let pending_receive_swaps_by_claim_tx_id =
|
||||
@@ -1228,7 +1161,7 @@ impl LiquidSdk {
|
||||
let pending_send_swaps_by_refund_tx_id =
|
||||
self.persister.list_pending_send_swaps_by_refund_tx_id()?;
|
||||
|
||||
for tx in self.lwk_wollet.lock().await.transactions()? {
|
||||
for tx in self.onchain_wallet.transactions().await? {
|
||||
let tx_id = tx.txid.to_string();
|
||||
let is_tx_confirmed = tx.height.is_some();
|
||||
let amount_sat = tx.balance.values().sum::<i64>();
|
||||
|
||||
132
lib/core/src/wallet.rs
Normal file
132
lib/core/src/wallet.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use boltz_client::ElementsAddress;
|
||||
use lwk_common::{singlesig_desc, Singlesig};
|
||||
use lwk_signer::{AnySigner, SwSigner};
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use lwk_common::Signer;
|
||||
use lwk_wollet::{
|
||||
elements::{Address, Transaction},
|
||||
ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister, Tip, WalletTx, Wollet,
|
||||
WolletDescriptor,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::PaymentError,
|
||||
model::{Config, Network},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait OnchainWallet: Send + Sync {
|
||||
async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError>;
|
||||
async fn build_tx(
|
||||
&self,
|
||||
fee_rate: Option<f32>,
|
||||
recipient_address: &str,
|
||||
amount_sat: u64,
|
||||
) -> Result<Transaction, PaymentError>;
|
||||
|
||||
async fn next_unused_address(&self) -> Result<Address, PaymentError>;
|
||||
|
||||
async fn tip(&self) -> Tip;
|
||||
|
||||
async fn pubkey(&self) -> String;
|
||||
|
||||
async fn full_scan(&self) -> Result<(), PaymentError>;
|
||||
}
|
||||
|
||||
pub(crate) struct LiquidOnchainWallet {
|
||||
wallet: Arc<Mutex<Wollet>>,
|
||||
lwk_signer: SwSigner,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl LiquidOnchainWallet {
|
||||
pub(crate) fn new(mnemonic: String, config: Config) -> Result<Self> {
|
||||
let is_mainnet = config.network == Network::Mainnet;
|
||||
let lwk_signer = SwSigner::new(&mnemonic, is_mainnet)?;
|
||||
let descriptor = LiquidOnchainWallet::get_descriptor(&lwk_signer, config.network)?;
|
||||
let elements_network: ElementsNetwork = config.network.into();
|
||||
|
||||
let lwk_persister =
|
||||
FsPersister::new(config.working_dir.clone(), elements_network, &descriptor)?;
|
||||
let wollet = Wollet::new(elements_network, lwk_persister, descriptor)?;
|
||||
Ok(Self {
|
||||
wallet: Arc::new(Mutex::new(wollet)),
|
||||
lwk_signer,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_descriptor(
|
||||
signer: &SwSigner,
|
||||
network: Network,
|
||||
) -> Result<WolletDescriptor, PaymentError> {
|
||||
let is_mainnet = network == Network::Mainnet;
|
||||
let descriptor_str = singlesig_desc(
|
||||
signer,
|
||||
Singlesig::Wpkh,
|
||||
lwk_common::DescriptorBlindingKey::Slip77,
|
||||
is_mainnet,
|
||||
)
|
||||
.map_err(|e| anyhow!("Invalid descriptor: {e}"))?;
|
||||
Ok(descriptor_str.parse()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnchainWallet for LiquidOnchainWallet {
|
||||
async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError> {
|
||||
let wallet = self.wallet.lock().await;
|
||||
wallet.transactions().map_err(|e| PaymentError::Generic {
|
||||
err: format!("Failed to fetch wallet transactions: {e:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_tx(
|
||||
&self,
|
||||
fee_rate: Option<f32>,
|
||||
recipient_address: &str,
|
||||
amount_sat: u64,
|
||||
) -> Result<Transaction, PaymentError> {
|
||||
let lwk_wollet = self.wallet.lock().await;
|
||||
let mut pset = lwk_wollet::TxBuilder::new(self.config.network.into())
|
||||
.add_lbtc_recipient(
|
||||
&ElementsAddress::from_str(recipient_address).map_err(|e| {
|
||||
PaymentError::Generic {
|
||||
err: format!(
|
||||
"Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
|
||||
),
|
||||
}
|
||||
})?,
|
||||
amount_sat,
|
||||
)?
|
||||
.fee_rate(fee_rate)
|
||||
.finish(&lwk_wollet)?;
|
||||
let signer = AnySigner::Software(self.lwk_signer.clone());
|
||||
signer.sign(&mut pset)?;
|
||||
Ok(lwk_wollet.finalize(&mut pset)?)
|
||||
}
|
||||
|
||||
async fn next_unused_address(&self) -> Result<Address, PaymentError> {
|
||||
Ok(self.wallet.lock().await.address(None)?.address().clone())
|
||||
}
|
||||
|
||||
async fn tip(&self) -> Tip {
|
||||
self.wallet.lock().await.tip()
|
||||
}
|
||||
|
||||
async fn pubkey(&self) -> String {
|
||||
self.lwk_signer.xpub().to_string()
|
||||
}
|
||||
|
||||
async fn full_scan(&self) -> Result<(), PaymentError> {
|
||||
let mut wallet = self.wallet.lock().await;
|
||||
let mut electrum_client =
|
||||
ElectrumClient::new(&ElectrumUrl::new(&self.config.electrum_url, true, true))?;
|
||||
lwk_wollet::full_scan_with_electrum_client(&mut wallet, &mut electrum_client)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user