abstract onchain wallet behind its own module and trait

This commit is contained in:
Roei Erez
2024-06-05 11:42:25 +03:00
parent 0a8f2545db
commit 5a02c4eaf0
4 changed files with 158 additions and 102 deletions

View File

@@ -10,3 +10,4 @@ pub mod persist;
pub mod sdk;
pub(crate) mod swapper;
pub(crate) mod utils;
pub(crate) mod wallet;

View File

@@ -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,

View File

@@ -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
View 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(())
}
}