From daab4fe9c8baf3bff8b9888525c1524892e2e535 Mon Sep 17 00:00:00 2001 From: elnosh Date: Mon, 25 Mar 2024 17:28:27 -0500 Subject: [PATCH] extractor to handle error responses --- Cargo.lock | 13 ++ mutiny-core/src/lib.rs | 30 ----- mutiny-server/Cargo.toml | 4 +- mutiny-server/src/extractor.rs | 33 +++++ mutiny-server/src/main.rs | 55 ++++---- mutiny-server/src/routes.rs | 227 +++++++++++++++++++++++++++------ mutiny-server/src/sled.rs | 8 +- 7 files changed, 272 insertions(+), 98 deletions(-) create mode 100644 mutiny-server/src/extractor.rs diff --git a/Cargo.lock b/Cargo.lock index b32c16342..76f9312e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,6 +314,7 @@ checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http 1.0.0", @@ -361,6 +362,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "backtrace" version = "0.3.69" diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 38ce6ace9..43810e6d5 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -704,36 +704,6 @@ pub struct MutinyWalletConfig { skip_hodl_invoices: bool, } -impl MutinyWalletConfig { - pub fn new( - xprivkey: ExtendedPrivKey, - network: Network, - esplora: Option, - rgs: Option, - lsp: Option, - ) -> MutinyWalletConfig { - MutinyWalletConfig { - xprivkey, - #[cfg(target_arch = "wasm32")] - websocket_proxy_addr: None, - network, - user_esplora_url: esplora, - user_rgs_url: rgs, - lsp_url: lsp, - lsp_token: None, - lsp_connection_string: None, - auth_client: None, - subscription_url: None, - scorer_url: None, - primal_url: None, - do_not_connect_peers: false, - skip_device_lock: false, - safe_mode: false, - skip_hodl_invoices: true, - } - } -} - pub struct MutinyWalletBuilder { xprivkey: ExtendedPrivKey, nostr_key_source: NostrKeySource, diff --git a/mutiny-server/Cargo.toml b/mutiny-server/Cargo.toml index 98c831b71..d07aae56c 100644 --- a/mutiny-server/Cargo.toml +++ b/mutiny-server/Cargo.toml @@ -10,7 +10,7 @@ mutiny-core = { path = "../mutiny-core" } anyhow = "1.0.70" async-trait = "0.1.68" -axum = "0.7.4" +axum = { version = "0.7.4" , features = ["default", "macros"]} bitcoin = { version = "0.30.2" } clap = { version = "4.1.14", features = ["derive"] } lightning = { version = "0.0.121", default-features = false, features = ["max_level_trace", "grind_signatures", "std"] } @@ -23,4 +23,4 @@ futures-util = { version = "0.3", default-features = false } sled = "0.34.7" tokio = { version = "1.36.0", features = ["full"] } reqwest = { version = "0.11", features = ["json"] } -serde = { version = "1.0.196", features = ["derive"] } +serde = { version = "1.0.196", features = ["derive"] } \ No newline at end of file diff --git a/mutiny-server/src/extractor.rs b/mutiny-server/src/extractor.rs new file mode 100644 index 000000000..5939d16ff --- /dev/null +++ b/mutiny-server/src/extractor.rs @@ -0,0 +1,33 @@ +use async_trait::async_trait; +use axum::extract::{rejection::FormRejection, FromRequest, Request}; +use axum::http::StatusCode; +use axum::Json; +use serde_json::{json, Value}; + +pub struct Form(pub T); + +#[async_trait] +impl FromRequest for Form +where + axum::Form: FromRequest, + S: Send + Sync, +{ + type Rejection = (StatusCode, Json); + + async fn from_request(req: Request, state: &S) -> Result { + let (parts, body) = req.into_parts(); + + let request = Request::from_parts(parts, body); + + match axum::Form::::from_request(request, state).await { + Ok(value) => Ok(Self(value.0)), + Err(rejection) => { + let err_payload = json!({ + "error": rejection.body_text() + }); + + Err((StatusCode::BAD_REQUEST, Json(err_payload))) + } + } + } +} diff --git a/mutiny-server/src/main.rs b/mutiny-server/src/main.rs index 88dd864f8..826a77ac3 100644 --- a/mutiny-server/src/main.rs +++ b/mutiny-server/src/main.rs @@ -1,8 +1,9 @@ #![allow(incomplete_features)] mod config; -mod sled; +mod extractor; mod routes; +mod sled; use crate::config::Config; use crate::sled::SledStorage; @@ -13,7 +14,7 @@ use bitcoin::bip32::ExtendedPrivKey; use clap::Parser; use log::{debug, info}; use mutiny_core::storage::MutinyStorage; -use mutiny_core::{generate_seed, MutinyWalletBuilder, MutinyWalletConfig}; +use mutiny_core::{generate_seed, MutinyWalletBuilder, MutinyWalletConfigBuilder}; use shutdown::Shutdown; use std::time::Duration; @@ -41,18 +42,22 @@ async fn main() -> anyhow::Result<()> { let seed = mnemonic.to_seed(""); let xprivkey = ExtendedPrivKey::new_master(network, &seed).unwrap(); - let wallet_config = MutinyWalletConfig::new( - xprivkey, - network, - config.esplora_url, - config.rgs_url, - config.lsp_url, - ); + let mut config_builder = MutinyWalletConfigBuilder::new(xprivkey).with_network(network); + if let Some(url) = config.esplora_url { + config_builder.with_user_esplora_url(url); + } + if let Some(url) = config.rgs_url { + config_builder.with_user_rgs_url(url); + } + if let Some(url) = config.lsp_url { + config_builder.with_lsp_url(url); + } + let wallet_config = config_builder.build(); debug!("Initializing wallet..."); - let wallet = MutinyWalletBuilder::new(xprivkey, storage).with_config(wallet_config).build().await?; - - let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.bind, config.port)) + let wallet = MutinyWalletBuilder::new(xprivkey, storage) + .with_config(wallet_config) + .build() .await?; debug!("Wallet initialized!"); @@ -61,17 +66,23 @@ async fn main() -> anyhow::Result<()> { mutiny_wallet: wallet.clone(), }; - let server_router = Router::new() - .route("/newaddress", get(routes::new_address)) - .route("/sendtoaddress", post(routes::send_to_address)) - .route("/openchannel", post(routes::open_channel)) - .route("/invoice", post(routes::create_invoice)) - .route("/payinvoice", post(routes::pay_invoice)) - .route("/balance", get(routes::get_balance)) - .fallback(fallback) - .layer(Extension(state.clone())); + tokio::spawn(async move { + let server_router = Router::new() + .route("/newaddress", get(routes::new_address)) + .route("/sendtoaddress", post(routes::send_to_address)) + .route("/openchannel", post(routes::open_channel)) + .route("/createinvoice", post(routes::create_invoice)) + .route("/payinvoice", post(routes::pay_invoice)) + .route("/balance", get(routes::get_balance)) + .fallback(fallback) + .layer(Extension(state.clone())); + + let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.bind, config.port)) + .await + .expect("failed to parse bind/port"); - axum::serve(listener, server_router).await.unwrap(); + axum::serve(listener, server_router).await.unwrap(); + }); // wait for shutdown hook shutdown.recv().await; diff --git a/mutiny-server/src/routes.rs b/mutiny-server/src/routes.rs index a26b252b5..f27b1e3e6 100644 --- a/mutiny-server/src/routes.rs +++ b/mutiny-server/src/routes.rs @@ -1,100 +1,244 @@ -use axum::{Extension, Json}; +use crate::extractor::Form; +use crate::sled::SledStorage; + use axum::http::StatusCode; -use bitcoin::{Address}; -use serde_json::{json, Value}; -use mutiny_core::{InvoiceHandler, MutinyWallet}; +use axum::{Extension, Json}; +use bitcoin::{secp256k1::PublicKey, Address}; use lightning_invoice::Bolt11Invoice; -use std::str::FromStr; use mutiny_core::error::MutinyError; -use serde::Deserialize; -use crate::sled::SledStorage; +use mutiny_core::{InvoiceHandler, MutinyWallet}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::str::FromStr; #[derive(Clone)] pub struct State { pub mutiny_wallet: MutinyWallet, } -pub async fn new_address(Extension(state): Extension) -> Result, (StatusCode, Json)> { - let address = state.mutiny_wallet.node_manager.get_new_address(vec![]).map_err(|e|handle_mutiny_err(e))?; +pub async fn new_address( + Extension(state): Extension, +) -> Result, (StatusCode, Json)> { + let address = state + .mutiny_wallet + .node_manager + .get_new_address(vec![]) + .map_err(|e| handle_mutiny_err(e))?; Ok(Json(json!(address))) } #[derive(Deserialize)] +#[serde(rename_all = "camelCase")] pub struct SendTo { - amount: u64, - address: String + amount_sat: u64, + address: String, + fee_rate_sat_byte: Option, + label: Option, } pub async fn send_to_address( Extension(state): Extension, - Json(payload): Json + Form(request): Form, ) -> Result, (StatusCode, Json)> { - let address = Address::from_str(payload.address.as_str()).map_err(|_e| { - ( - StatusCode::BAD_REQUEST, - Json(json!({"error": "invalid address"})), - ) - })?; + let address = match Address::from_str(request.address.as_str()) { + Ok(address) => address, + Err(_) => { + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid address"})), + )) + } + }; - let tx_id = state.mutiny_wallet.node_manager.send_to_address(address, payload.amount, vec!["test".to_string()], None) + let label = match request.label { + Some(label) => vec![label.to_string()], + None => Vec::new(), + }; + + let tx_id = state + .mutiny_wallet + .node_manager + .send_to_address( + address, + request.amount_sat, + label, + request.fee_rate_sat_byte, + ) .await .map_err(|e| handle_mutiny_err(e))?; - Ok(Json(json!({ - "txid": tx_id.to_string() - }))) + Ok(Json(json!(tx_id.to_string()))) } #[derive(Deserialize)] -pub struct Amount { - amount: u64, +#[serde(rename_all = "camelCase")] +pub struct OpenChannel { + amount_sat: u64, + fee_rate: Option, + node_pubkey: Option, } pub async fn open_channel( Extension(state): Extension, - Json(payload): Json + Form(request): Form, ) -> Result, (StatusCode, Json)> { - let channel = state.mutiny_wallet.node_manager.open_channel(None, None, payload.amount, None, None) + let to_pubkey = match request.node_pubkey { + Some(pubkey) => { + let pubkey = match PublicKey::from_str(pubkey.as_str()) { + Ok(pk) => pk, + Err(_) => { + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid node pubkey"})), + )) + } + }; + Some(pubkey) + } + None => None, + }; + + let channel = state + .mutiny_wallet + .node_manager + .open_channel(None, to_pubkey, request.amount_sat, request.fee_rate, None) .await .map_err(|e| handle_mutiny_err(e))?; Ok(Json(json!(channel))) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateInvoiceRequest { + amount_sat: u64, + label: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateInvoiceResponse { + amount_sat: u64, + payment_hash: String, + serialized: String, +} + pub async fn create_invoice( Extension(state): Extension, - Json(payload): Json + Form(request): Form, ) -> Result, (StatusCode, Json)> { - let invoice = state.mutiny_wallet.create_invoice(payload.amount, vec!["test".to_string()]) + let label = match request.label { + Some(label) => vec![label.to_string()], + None => Vec::new(), + }; + + let invoice = state + .mutiny_wallet + .create_invoice(request.amount_sat, label) .await .map_err(|e| handle_mutiny_err(e))?; + + let amount_sat = match invoice.amount_sats { + Some(amount) => amount, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to create invoice"})), + )) + } + }; + + let pr = match invoice.bolt11 { + Some(amount) => amount, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to create invoice"})), + )) + } + } + .to_string(); + + let invoice = CreateInvoiceResponse { + amount_sat, + payment_hash: invoice.payment_hash.to_string(), + serialized: pr, + }; + Ok(Json(json!(invoice))) } #[derive(Deserialize)] -pub struct Invoice { - invoice: String, +#[serde(rename_all = "camelCase")] +pub struct PayInvoiceRequest { + amount_sat: Option, + invoice: Bolt11Invoice, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PayInvoiceResponse { + recipient_amount_sat: u64, + routing_fee_sat: u64, + payment_hash: String, + payment_preimage: String, } pub async fn pay_invoice( Extension(state): Extension, - Json(payload): Json + Form(request): Form, ) -> Result, (StatusCode, Json)> { - let invoice = Bolt11Invoice::from_str(payload.invoice.as_str()).map_err(|_e| { - ( - StatusCode::BAD_REQUEST, - Json(json!({"error": "invalid invoice provided"})), - ) - })?; - let invoice = state.mutiny_wallet.pay_invoice(&invoice, None, vec!["test".to_string()]) + let invoice = state + .mutiny_wallet + .pay_invoice(&request.invoice, request.amount_sat, Vec::new()) .await .map_err(|e| handle_mutiny_err(e))?; + + let amount_sat = match invoice.amount_sats { + Some(amount) => amount, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to pay invoice"})), + )) + } + }; + + let fees = match invoice.fees_paid { + Some(fee) => fee, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to pay invoice"})), + )) + } + }; + + let preimage = match invoice.preimage { + Some(preimage) => preimage, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to pay invoice"})), + )) + } + }; + + let invoice = PayInvoiceResponse { + recipient_amount_sat: amount_sat, + routing_fee_sat: fees, + payment_hash: invoice.payment_hash.to_string(), + payment_preimage: preimage, + }; + Ok(Json(json!(invoice))) } pub async fn get_balance( - Extension(state): Extension + Extension(state): Extension, ) -> Result, (StatusCode, Json)> { - let balance = state.mutiny_wallet.get_balance() + let balance = state + .mutiny_wallet + .get_balance() .await .map_err(|e| handle_mutiny_err(e))?; Ok(Json(json!(balance))) @@ -102,8 +246,7 @@ pub async fn get_balance( fn handle_mutiny_err(err: MutinyError) -> (StatusCode, Json) { let err = json!({ - "status": "ERROR", - "reason": format!("{err}"), + "error": format!("{err}"), }); (StatusCode::INTERNAL_SERVER_ERROR, Json(err)) } diff --git a/mutiny-server/src/sled.rs b/mutiny-server/src/sled.rs index 3c943607d..07f9cdeea 100644 --- a/mutiny-server/src/sled.rs +++ b/mutiny-server/src/sled.rs @@ -134,12 +134,16 @@ impl MutinyStorage for SledStorage { } fn scan_keys(&self, prefix: &str, suffix: Option<&str>) -> Result, MutinyError> { - Ok(self.db.iter() + Ok(self + .db + .iter() .keys() .filter_map(|key| key.ok()) .map(|key| ivec_to_string(key)) .filter_map(Result::ok) - .filter(|key| key.starts_with(prefix) && (suffix.is_none() || key.ends_with(suffix.unwrap()))) + .filter(|key| { + key.starts_with(prefix) && (suffix.is_none() || key.ends_with(suffix.unwrap())) + }) .collect()) }