From 5a02c4eaf04b04a5847a1a0d44f1eb24fff01ff0 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Wed, 5 Jun 2024 11:42:25 +0300 Subject: [PATCH] abstract onchain wallet behind its own module and trait --- lib/core/src/lib.rs | 1 + lib/core/src/model.rs | 12 +--- lib/core/src/sdk.rs | 115 ++++++++--------------------------- lib/core/src/wallet.rs | 132 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 102 deletions(-) create mode 100644 lib/core/src/wallet.rs diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index 3067617..d825003 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -10,3 +10,4 @@ pub mod persist; pub mod sdk; pub(crate) mod swapper; pub(crate) mod utils; +pub(crate) mod wallet; diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 66d661c..9f2b2a1 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -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 - pub descriptor: WolletDescriptor, -} - #[derive(Debug, Serialize)] pub struct ConnectRequest { pub mnemonic: String, diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index bdb82c6..cfc448d 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -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>, - /// LWK Signer, for signing Liquid transactions - lwk_signer: SwSigner, + onchain_wallet: Arc, persister: Arc, event_manager: Arc, status_stream: Arc, @@ -68,33 +61,15 @@ pub struct LiquidSdk { impl LiquidSdk { pub async fn connect(req: ConnectRequest) -> Result> { 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> { - let config = opts.config; - let elements_network: ElementsNetwork = config.network.into(); - + fn new(config: Config, mnemonic: String) -> Result> { 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::::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 { - 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 { - let lwk_wollet = self.lwk_wollet.lock().await; - Ok(lwk_wollet.address(None)?.address().clone()) - } - pub async fn get_info(&self, req: GetInfoRequest) -> Result { 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, - recipient_address: &str, - amount_sat: u64, - ) -> Result { - 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 { 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::(); diff --git a/lib/core/src/wallet.rs b/lib/core/src/wallet.rs new file mode 100644 index 0000000..49aed22 --- /dev/null +++ b/lib/core/src/wallet.rs @@ -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, PaymentError>; + async fn build_tx( + &self, + fee_rate: Option, + recipient_address: &str, + amount_sat: u64, + ) -> Result; + + async fn next_unused_address(&self) -> Result; + + async fn tip(&self) -> Tip; + + async fn pubkey(&self) -> String; + + async fn full_scan(&self) -> Result<(), PaymentError>; +} + +pub(crate) struct LiquidOnchainWallet { + wallet: Arc>, + lwk_signer: SwSigner, + config: Config, +} + +impl LiquidOnchainWallet { + pub(crate) fn new(mnemonic: String, config: Config) -> Result { + 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 { + 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, 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, + recipient_address: &str, + amount_sat: u64, + ) -> Result { + 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 { + 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(()) + } +}