Skip to content

Commit

Permalink
Merge pull request #820 from BoltzExchange/recovery-rescan
Browse files Browse the repository at this point in the history
feat: swap recovery rescan
  • Loading branch information
michael1011 authored Feb 27, 2025
2 parents 3f47be3 + 315771f commit 68b4fc8
Show file tree
Hide file tree
Showing 64 changed files with 2,229 additions and 379 deletions.
41 changes: 41 additions & 0 deletions boltzr/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions boltzr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ csv = "1.3.1"
axum-extra = { version = "0.10.0", features = ["typed-header"] }
redis = { version = "0.29.0", features = ["tokio-comp", "r2d2"] }
bytes = "1.10.0"
rust-bip39 = "1.0.0"
elements-miniscript = "0.4.0"

[build-dependencies]
built = { version = "0.7.7", features = ["git2"] }
Expand Down
15 changes: 14 additions & 1 deletion boltzr/src/api/lightning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,10 @@ mod test {
use axum::Router;
use axum::body::Body;
use axum::extract::Request;
use bip39::Mnemonic;
use http_body_util::BodyExt;
use rstest::*;
use std::str::FromStr;
use tower::ServiceExt;

fn setup_router(manager: MockManager) -> Router {
Expand All @@ -196,7 +198,18 @@ mod test {
manager.expect_get_currency().returning(move |_| {
Some(Currency {
network: Network::Regtest,
wallet: Arc::new(crate::wallet::Bitcoin::new(Network::Regtest)),
wallet: Arc::new(
crate::wallet::Bitcoin::new(
Network::Regtest,
&Mnemonic::from_str(
"test test test test test test test test test test test junk",
)
.unwrap()
.to_seed(""),
"m/0/0".to_string(),
)
.unwrap(),
),
cln: Some(cln.clone()),
lnd: None,
chain: None,
Expand Down
12 changes: 4 additions & 8 deletions boltzr/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::api::errors::error_middleware;
use crate::api::recovery::swap_recovery;
use crate::api::sse::sse_handler;
use crate::api::stats::get_stats;
#[cfg(feature = "metrics")]
Expand All @@ -18,6 +19,7 @@ use ws::types::SwapStatus;
mod errors;
mod headers;
mod lightning;
mod recovery;
mod sse;
mod stats;
mod types;
Expand Down Expand Up @@ -123,6 +125,7 @@ where
"/v2/swap/{swap_type}/stats/{from}/{to}",
get(get_stats::<S, M>),
)
.route("/v2/swap/recovery", post(swap_recovery::<S, M>))
.route(
"/v2/lightning/{currency}/node/{node}",
get(lightning::node_info::<S, M>),
Expand All @@ -144,12 +147,10 @@ pub mod test {
use crate::api::ws::status::SwapInfos;
use crate::api::ws::types::SwapStatus;
use crate::api::{Config, Server};
use crate::cache::Redis;
use crate::service::Service;
use crate::swap::manager::test::MockManager;
use async_trait::async_trait;
use reqwest::StatusCode;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::broadcast::Sender;
Expand Down Expand Up @@ -196,12 +197,7 @@ pub mod test {
},
cancel.clone(),
Arc::new(MockManager::new()),
Arc::new(Service::new::<Redis>(
Arc::new(HashMap::new()),
None,
None,
None,
)),
Arc::new(Service::new_mocked_prometheus(false)),
Fetcher {
status_tx: status_tx.clone(),
},
Expand Down
146 changes: 146 additions & 0 deletions boltzr/src/api/recovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use crate::api::ServerState;
use crate::api::errors::AxumError;
use crate::api::ws::status::SwapInfos;
use crate::swap::manager::SwapManager;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::{Extension, Json};
use bitcoin::bip32::Xpub;
use serde::de::Visitor;
use serde::{Deserialize, Deserializer};
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;

struct XpubDeserialize(Xpub);

impl<'de> Deserialize<'de> for XpubDeserialize {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct XpubDeserializeVisitor;

impl Visitor<'_> for XpubDeserializeVisitor {
type Value = XpubDeserialize;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a valid xpub")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match Xpub::from_str(value) {
Ok(xpub) => Ok(XpubDeserialize(xpub)),
Err(err) => Err(E::custom(format!("invalid xpub: {}", err))),
}
}
}

deserializer.deserialize_string(XpubDeserializeVisitor)
}
}

#[derive(Deserialize)]
pub struct RecoveryParams {
xpub: XpubDeserialize,
}

pub async fn swap_recovery<S, M>(
Extension(state): Extension<Arc<ServerState<S, M>>>,
Json(RecoveryParams { xpub }): Json<RecoveryParams>,
) -> anyhow::Result<impl IntoResponse, AxumError>
where
S: SwapInfos + Send + Sync + Clone + 'static,
M: SwapManager + Send + Sync + 'static,
{
let res = state.service.swap_recovery.recover_xpub(&xpub.0)?;
Ok((StatusCode::OK, Json(res)).into_response())
}

#[cfg(test)]
mod test {
use crate::api::errors::ApiError;
use crate::api::test::Fetcher;
use crate::api::ws::types::SwapStatus;
use crate::api::{Server, ServerState};
use crate::service::Service;
use crate::service::test::RecoverableSwap;
use crate::swap::manager::test::MockManager;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::{Extension, Router};
use http_body_util::BodyExt;
use std::sync::Arc;
use tower::ServiceExt;

fn setup_router() -> Router {
let (status_tx, _) = tokio::sync::broadcast::channel::<Vec<SwapStatus>>(1);
Server::<Fetcher, MockManager>::add_routes(Router::new()).layer(Extension(Arc::new(
ServerState {
manager: Arc::new(MockManager::new()),
service: Arc::new(Service::new_mocked_prometheus(false)),
swap_status_update_tx: status_tx.clone(),
swap_infos: Fetcher { status_tx },
},
)))
}

#[tokio::test]
async fn test_swap_recovery() {
let res = setup_router()
.oneshot(
Request::builder()
.method(axum::http::Method::POST)
.uri("/v2/swap/recovery")
.header(axum::http::header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&serde_json::json!({
"xpub": "xpub661MyMwAqRbcGXPykvqCkK3sspTv2iwWTYpY9gBewku5Noj96ov1EqnKMDzGN9yPsncpRoUymJ7zpJ7HQiEtEC9Af2n3DmVu36TSV4oaiym"
}))
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();

assert_eq!(res.status(), StatusCode::OK);

let body = res.into_body().collect().await.unwrap().to_bytes();
assert_eq!(
serde_json::from_slice::<Vec<RecoverableSwap>>(&body).unwrap(),
vec![],
);
}

#[tokio::test]
async fn test_swap_recovery_invalid_xpub() {
let res = setup_router()
.oneshot(
Request::builder()
.method(axum::http::Method::POST)
.uri("/v2/swap/recovery")
.header(axum::http::header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::to_vec(&serde_json::json!({
"xpub": "invalid"
}))
.unwrap(),
))
.unwrap(),
)
.await
.unwrap();

assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);

let body = res.into_body().collect().await.unwrap().to_bytes();
assert_eq!(
serde_json::from_slice::<ApiError>(&body).unwrap().error,
"Failed to deserialize the JSON body into the target type: xpub: invalid xpub: base58 encoding error at line 1 column 17"
);
}
}
25 changes: 16 additions & 9 deletions boltzr/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub struct GlobalConfig {
#[serde(rename = "profilingEndpoint")]
pub profiling_endpoint: Option<String>,

#[serde(rename = "mnemonicpath")]
pub mnemonic_path: Option<String>,

#[serde(rename = "mnemonicpathEvm")]
pub mnemonic_path_evm: Option<String>,

Expand All @@ -77,16 +80,20 @@ pub fn parse_config(path: &str) -> Result<GlobalConfig, Box<dyn Error>> {
let mut config = toml::from_str::<GlobalConfig>(fs::read_to_string(path)?.as_ref())?;
trace!("Read config: {:#}", serde_json::to_string_pretty(&config)?);

let default_mnemonic_path = Path::new(path)
.parent()
.unwrap()
.join("seed.dat")
.to_str()
.unwrap()
.to_string();

if config.mnemonic_path.is_none() {
config.mnemonic_path = Some(default_mnemonic_path.clone());
}

if config.mnemonic_path_evm.is_none() {
config.mnemonic_path_evm = Some(
Path::new(path)
.parent()
.unwrap()
.join("seed.dat")
.to_str()
.unwrap()
.to_string(),
);
config.mnemonic_path_evm = Some(default_mnemonic_path);
}

let data_dir = config.clone().sidecar.data_dir.unwrap_or(
Expand Down
Loading

0 comments on commit 68b4fc8

Please sign in to comment.