Restore: Associate swap tx IDs from onchain data (#399)

* Add extend_incomplete_failed_send_swaps() on first sync

* Find lockup txs

* Send Swaps: find refund txs

* Simplify recover_send_swap_tx_ids, add recover_receive_swap_tx_ids

* recover_receive_swap_tx_ids: batch tx lookups

* Move onchain-restore methods to own module

* Store restored data in own struct

* Fix CI: bump pubspec.lock dependencies

* LiquidChainService: add get_scripts_history_electrum()

* restore_onchain: rely on batch call to fetch histories of all known swaps

* Rename get_scripts_history_electrum

* Rename restore_onchain.rs, flatten onchain inner module

* Rename ImmutableDb to SwapsList

* Simplify logic in restore.rs

* restore.rs: Add chain swap support, simplify logic

* restore.rs: add logging when script retrieval fails

* restore.rs: remove unused field create_resp

* restore.rs: rename SwapCompositeData to SwapHistory

* restore.rs: allow unused fields in simulated immutable data

* restore.rs: cargo fmt

* Cargo fmt

* Fix failing test

* When fetching script history, also fetch if tx is confirmed or not

* Recover send swaps: fetch claim tx IDs

* Recover onchain data: persist reconstructed swaps

* Simplify recover_from_onchain: store swap txs per swap ID

* Receive swaps: do not treat lockup/claim txs as pair

* Clarify meaning of partial swap state

* Cargo clippy

* Receive Chain Swap: distinguish BTC lockup from claim/refund tx

* Send Chain Swap: distinguish BTC lockup/claim by vout, not by history order

* get_partial_state: default to Created when state is unclear

* Receive Chain Swaps: differentiate BTC refund from BTC claim txs

* Send Swaps: clarify reason for defaulting to TimedOut on no lockup

* Chain swaps: add docs for meaning of server, user txs

* Recover Receive swaps: cover the case when only the lockup is available

* HistoryTxId: store confirmation block height

* Receive swaps: differentiate claim tx from swapper refund tx

* recover_from_onchain: extract immutable DB (swaps list) as arg

* Rename get_partial_state to derive_partial_state

* Restore: remove validation steps

* Restore chain swaps: treat as Complete only when claim is confirmed

* Fix clippy warnings

* Remove restore call from sync call
This commit is contained in:
ok300
2024-08-30 17:18:25 +00:00
committed by GitHub
parent b493f3dc03
commit 5f74b9df4b
9 changed files with 1130 additions and 21 deletions

View File

@@ -33,6 +33,9 @@ pub trait BitcoinChainService: Send + Sync {
/// Get the transactions involved for a script
fn get_script_history(&self, script: &Script) -> Result<Vec<History>>;
/// Get the transactions involved in a list of scripts.
fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>>;
/// Get the transactions involved for a script
async fn get_script_history_with_retry(
&self,
@@ -132,6 +135,15 @@ impl BitcoinChainService for HybridBitcoinChainService {
.collect())
}
fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>> {
Ok(self
.client
.batch_script_get_history(scripts)?
.into_iter()
.map(|v| v.into_iter().map(Into::into).collect())
.collect())
}
async fn get_script_history_with_retry(
&self,
script: &Script,

View File

@@ -31,9 +31,16 @@ pub trait LiquidChainService: Send + Sync {
/// Get a list of transactions
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>>;
/// Get the transactions involved in a list of scripts including lowball
/// Get the transactions involved in a script, including lowball transactions.
///
/// On mainnet, the data is fetched from Esplora. On testnet, it's fetched from Electrum.
async fn get_script_history(&self, scripts: &Script) -> Result<Vec<History>>;
/// Get the transactions involved in a list of scripts, including lowball transactions.
///
/// The data is fetched in a single call from the Electrum endpoint.
async fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>>;
/// Get the transactions involved in a list of scripts including lowball
async fn get_script_history_with_retry(
&self,
@@ -131,6 +138,12 @@ impl LiquidChainService for HybridLiquidChainService {
}
}
async fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>> {
self.electrum_client
.get_scripts_history(scripts)
.map_err(Into::into)
}
async fn get_script_history_with_retry(
&self,
script: &Script,

View File

@@ -175,6 +175,8 @@ pub mod logger;
pub mod model;
pub mod persist;
pub(crate) mod receive_swap;
#[allow(dead_code)]
mod restore;
pub mod sdk;
pub(crate) mod send_swap;
pub(crate) mod swapper;

View File

@@ -519,6 +519,8 @@ impl FromSql for Direction {
}
/// A chain swap
///
/// See <https://docs.boltz.exchange/v/api/lifecycle#chain-swaps>
#[derive(Clone, Debug)]
pub(crate) struct ChainSwap {
pub(crate) id: String,

View File

@@ -119,11 +119,16 @@ impl Persister {
})
}
pub(crate) fn list_receive_swaps(
pub(crate) fn list_receive_swaps(&self) -> Result<Vec<ReceiveSwap>> {
let con: Connection = self.get_connection()?;
self.list_receive_swaps_where(&con, vec![])
}
pub(crate) fn list_receive_swaps_where(
&self,
con: &Connection,
where_clauses: Vec<String>,
) -> rusqlite::Result<Vec<ReceiveSwap>> {
) -> Result<Vec<ReceiveSwap>> {
let query = Self::list_receive_swaps_query(where_clauses);
let ongoing_receive = con
.prepare(&query)?
@@ -133,16 +138,13 @@ impl Persister {
Ok(ongoing_receive)
}
pub(crate) fn list_ongoing_receive_swaps(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<ReceiveSwap>> {
pub(crate) fn list_ongoing_receive_swaps(&self, con: &Connection) -> Result<Vec<ReceiveSwap>> {
let where_clause = vec![get_where_clause_state_in(&[
PaymentState::Created,
PaymentState::Pending,
])];
self.list_receive_swaps(con, where_clause)
self.list_receive_swaps_where(con, where_clause)
}
pub(crate) fn list_pending_receive_swaps(&self) -> Result<Vec<ReceiveSwap>> {
@@ -288,7 +290,7 @@ mod tests {
}
let con = storage.get_connection()?;
let swaps = storage.list_receive_swaps(&con, vec![])?;
let swaps = storage.list_receive_swaps_where(&con, vec![])?;
assert_eq!(swaps.len(), range.len());
// List ongoing receive swaps

View File

@@ -137,11 +137,16 @@ impl Persister {
})
}
pub(crate) fn list_send_swaps(
pub(crate) fn list_send_swaps(&self) -> Result<Vec<SendSwap>> {
let con: Connection = self.get_connection()?;
self.list_send_swaps_where(&con, vec![])
}
pub(crate) fn list_send_swaps_where(
&self,
con: &Connection,
where_clauses: Vec<String>,
) -> rusqlite::Result<Vec<SendSwap>> {
) -> Result<Vec<SendSwap>> {
let query = Self::list_send_swaps_query(where_clauses);
let ongoing_send = con
.prepare(&query)?
@@ -151,16 +156,13 @@ impl Persister {
Ok(ongoing_send)
}
pub(crate) fn list_ongoing_send_swaps(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<SendSwap>> {
pub(crate) fn list_ongoing_send_swaps(&self, con: &Connection) -> Result<Vec<SendSwap>> {
let where_clause = vec![get_where_clause_state_in(&[
PaymentState::Created,
PaymentState::Pending,
])];
self.list_send_swaps(con, where_clause)
self.list_send_swaps_where(con, where_clause)
}
pub(crate) fn list_pending_send_swaps(&self) -> Result<Vec<SendSwap>> {
@@ -319,7 +321,7 @@ mod tests {
}
let con = storage.get_connection()?;
let swaps = storage.list_send_swaps(&con, vec![])?;
let swaps = storage.list_send_swaps_where(&con, vec![])?;
assert_eq!(swaps.len(), range.len());
// List ongoing send swaps

1056
lib/core/src/restore.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,10 +14,10 @@ use futures_util::stream::select_all;
use futures_util::StreamExt;
use log::{debug, error, info};
use lwk_wollet::bitcoin::hex::DisplayHex;
use lwk_wollet::elements::AssetId;
use lwk_wollet::elements::{AssetId, Txid};
use lwk_wollet::hashes::{sha256, Hash};
use lwk_wollet::secp256k1::ThirtyTwoByteHash;
use lwk_wollet::{elements, ElementsNetwork};
use lwk_wollet::{elements, ElementsNetwork, WalletTx};
use sdk_common::bitcoin::secp256k1::Secp256k1;
use sdk_common::bitcoin::util::bip32::ChildNumber;
use sdk_common::liquid::LiquidAddressData;
@@ -1871,7 +1871,15 @@ impl LiquidSdk {
let pending_chain_swaps_by_refund_tx_id =
self.persister.list_pending_chain_swaps_by_refund_tx_id()?;
for tx in self.onchain_wallet.transactions().await? {
let tx_map: HashMap<Txid, WalletTx> = self
.onchain_wallet
.transactions()
.await?
.iter()
.map(|tx| (tx.txid, tx.clone()))
.collect();
for tx in tx_map.values() {
let tx_id = tx.txid.to_string();
let is_tx_confirmed = tx.height.is_some();
let amount_sat = tx.balance.values().sum::<i64>();

View File

@@ -2,8 +2,9 @@
use anyhow::Result;
use async_trait::async_trait;
use boltz_client::elements::Script;
use lwk_wollet::elements::{BlockHash, Txid};
use lwk_wollet::{bitcoin::consensus::deserialize, elements::hex::FromHex};
use lwk_wollet::{bitcoin::consensus::deserialize, elements::hex::FromHex, History};
use crate::{
chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
@@ -82,6 +83,10 @@ impl LiquidChainService for MockLiquidChainService {
Ok(self.history.clone().into_iter().map(Into::into).collect())
}
async fn get_scripts_history(&self, _scripts: &[&Script]) -> Result<Vec<Vec<History>>> {
unimplemented!()
}
async fn verify_tx(
&self,
_address: &boltz_client::ElementsAddress,
@@ -143,6 +148,13 @@ impl BitcoinChainService for MockBitcoinChainService {
Ok(self.history.clone().into_iter().map(Into::into).collect())
}
fn get_scripts_history(
&self,
_scripts: &[&boltz_client::bitcoin::Script],
) -> Result<Vec<Vec<History>>> {
unimplemented!()
}
fn script_get_balance(
&self,
_script: &boltz_client::bitcoin::Script,