From d77e5a4976690c241567743c23188ea8485b96d8 Mon Sep 17 00:00:00 2001 From: elnosh Date: Tue, 26 Mar 2024 22:30:00 -0500 Subject: [PATCH] close channel and payments endpoints --- .github/workflows/test.yml | 5 +- mutiny-server/src/main.rs | 3 + mutiny-server/src/routes.rs | 312 ++++++++++++++++++++++++++++-------- 3 files changed, 248 insertions(+), 72 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a00ef0ad7..3bf879a4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,10 @@ jobs: cargo +nightly-2023-10-24 fmt -- --check - name: Check docs - run: cargo +nightly-2023-10-24 doc + run: cargo +nightly-2023-10-24 doc --exclude mutiny-server --workspace + + - name: Check docs server + run: cargo +nightly-2023-10-24 doc --target x86_64-unknown-linux-gnu -p mutiny-server website: name: Build WASM binary diff --git a/mutiny-server/src/main.rs b/mutiny-server/src/main.rs index 6a4a7a90e..69a0197de 100644 --- a/mutiny-server/src/main.rs +++ b/mutiny-server/src/main.rs @@ -71,8 +71,11 @@ async fn main() -> anyhow::Result<()> { .route("/newaddress", get(routes::new_address)) .route("/sendtoaddress", post(routes::send_to_address)) .route("/openchannel", post(routes::open_channel)) + .route("/closechannel", post(routes::close_channel)) .route("/createinvoice", post(routes::create_invoice)) .route("/payinvoice", post(routes::pay_invoice)) + .route("/payments/incoming", get(routes::get_incoming_payments)) + .route("/payment/:paymentHash", get(routes::get_payment)) .route("/balance", get(routes::get_balance)) .route("/getinfo", get(routes::get_node_info)) .fallback(fallback) diff --git a/mutiny-server/src/routes.rs b/mutiny-server/src/routes.rs index 23aa81adc..1c457fd52 100644 --- a/mutiny-server/src/routes.rs +++ b/mutiny-server/src/routes.rs @@ -1,12 +1,17 @@ use crate::extractor::Form; use crate::sled::SledStorage; +use std::fmt; +use std::fmt::Formatter; +use axum::extract::Path; use axum::http::StatusCode; use axum::{Extension, Json}; +use bitcoin::hashes::sha256::Hash; use bitcoin::{secp256k1::PublicKey, Address}; use lightning_invoice::Bolt11Invoice; use mutiny_core::error::MutinyError; -use mutiny_core::{InvoiceHandler, MutinyWallet}; +use mutiny_core::event::HTLCStatus; +use mutiny_core::{InvoiceHandler, MutinyInvoice, MutinyWallet}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::str::FromStr; @@ -40,15 +45,12 @@ pub async fn send_to_address( Extension(state): Extension, Form(request): Form, ) -> Result, (StatusCode, Json)> { - 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 address = Address::from_str(request.address.as_str()).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid address"})), + ) + })?; let label = match request.label { Some(label) => vec![label.to_string()], @@ -84,15 +86,12 @@ pub async fn open_channel( ) -> Result, (StatusCode, Json)> { 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"})), - )) - } - }; + let pubkey = PublicKey::from_str(pubkey.as_str()).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid node pubkey"})), + ) + })?; Some(pubkey) } None => None, @@ -107,6 +106,77 @@ pub async fn open_channel( Ok(Json(json!(channel))) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CloseChannelRequest { + channel_id: String, + address: Option, +} + +pub async fn close_channel( + Extension(state): Extension, + Form(request): Form, +) -> Result, (StatusCode, Json)> { + let channels = state + .mutiny_wallet + .node_manager + .list_channels() + .await + .map_err(|e| handle_mutiny_err(e))?; + + let outpoint = match channels + .into_iter() + .find(|channel| channel.user_chan_id == request.channel_id) + { + Some(channel) => match channel.outpoint { + Some(outpoint) => outpoint, + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to close channel"})), + )) + } + }, + None => { + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({"error": "channel does not exist"})), + )) + } + }; + + let address = match request.address { + Some(address) => { + let address = Address::from_str(address.as_str()).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid address"})), + ) + })?; + + let address = address + .require_network(state.mutiny_wallet.get_network()) + .map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid address"})), + ) + })?; + Some(address) + } + None => None, + }; + + let _ = state + .mutiny_wallet + .node_manager + .close_channel(&outpoint, address, false, false) + .await + .map_err(|e| handle_mutiny_err(e))?; + + Ok(Json(json!("ok"))) +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateInvoiceRequest { @@ -137,31 +207,20 @@ pub async fn create_invoice( .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 amount_sat = invoice.amount_sats.ok_or(( + 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 pr = invoice.bolt11.ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to create invoice"})), + ))?; let invoice = CreateInvoiceResponse { amount_sat, payment_hash: invoice.payment_hash.to_string(), - serialized: pr, + serialized: pr.to_string(), }; Ok(Json(json!(invoice))) @@ -193,35 +252,20 @@ pub async fn pay_invoice( .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 amount_sat = invoice.amount_sats.ok_or(( + 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 fees = invoice.fees_paid.ok_or(( + 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 preimage = invoice.preimage.ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to pay invoice"})), + ))?; let invoice = PayInvoiceResponse { recipient_amount_sat: amount_sat, @@ -233,6 +277,94 @@ pub async fn pay_invoice( Ok(Json(json!(invoice))) } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentResponse { + payment_hash: String, + preimage: Option, + description: Option, + invoice: Option, + incoming: bool, + is_paid: bool, + amount_sats: u64, + fees: u64, +} + +pub async fn get_incoming_payments( + Extension(state): Extension, +) -> Result, (StatusCode, Json)> { + let invoices = state + .mutiny_wallet + .list_invoices() + .map_err(|e| handle_mutiny_err(e))?; + + let invoices: Vec = invoices + .into_iter() + .filter(|invoice| invoice.inbound && invoice.status != HTLCStatus::Succeeded) + .collect(); + + let mut payments = Vec::with_capacity(invoices.len()); + for invoice in invoices.into_iter() { + let pr = match invoice.bolt11.clone() { + Some(bolt_11) => Some(bolt_11.to_string()), + None => None, + }; + + let (is_paid, amount_sats, fees) = get_payment_info(&invoice)?; + + let payment = PaymentResponse { + payment_hash: invoice.payment_hash.to_string(), + preimage: invoice.preimage, + description: invoice.description, + invoice: pr, + incoming: invoice.inbound, + is_paid, + amount_sats, + fees, + }; + payments.push(payment); + } + + Ok(Json(json!(payments))) +} + +pub async fn get_payment( + Extension(state): Extension, + Path(payment_hash): Path, +) -> Result, (StatusCode, Json)> { + let hash = Hash::from_str(payment_hash.as_str()).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "invalid payment hash"})), + ) + })?; + + let invoice = state + .mutiny_wallet + .get_invoice_by_hash(&hash) + .await + .map_err(|e| handle_mutiny_err(e))?; + + let pr = match invoice.bolt11.clone() { + Some(bolt11) => Some(bolt11.to_string()), + None => None, + }; + let (is_paid, amount_sats, fees) = get_payment_info(&invoice)?; + + let payment = PaymentResponse { + payment_hash: hash.to_string(), + preimage: invoice.preimage, + description: invoice.description, + invoice: pr, + incoming: invoice.inbound, + is_paid, + amount_sats, + fees, + }; + + Ok(Json(json!(payment))) +} + pub async fn get_balance( Extension(state): Extension, ) -> Result, (StatusCode, Json)> { @@ -262,6 +394,20 @@ struct Channel { funding_tx_id: Option, } +enum ChannelState { + Usable, + Unusable, +} + +impl fmt::Display for ChannelState { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ChannelState::Usable => write!(f, "usable"), + ChannelState::Unusable => write!(f, "unusable"), + } + } +} + pub async fn get_node_info( Extension(state): Extension, ) -> Result, (StatusCode, Json)> { @@ -271,6 +417,7 @@ pub async fn get_node_info( .list_nodes() .await .map_err(|e| handle_mutiny_err(e))?; + let node_pubkey: PublicKey; if !nodes.is_empty() { node_pubkey = nodes[0]; @@ -287,21 +434,22 @@ pub async fn get_node_info( .list_channels() .await .map_err(|e| handle_mutiny_err(e))?; + let channels = channels .into_iter() .map(|channel| { let state = match channel.is_usable { - true => "usable", - false => "unusable", - } - .to_string(); + true => ChannelState::Usable, + false => ChannelState::Unusable, + }; + let funding_tx_id = match channel.outpoint { Some(outpoint) => Some(outpoint.txid.to_string()), None => None, }; Channel { - state, + state: state.to_string(), channel_id: channel.user_chan_id, balance_sat: channel.balance, inbound_liquidity_sat: channel.inbound, @@ -325,3 +473,25 @@ fn handle_mutiny_err(err: MutinyError) -> (StatusCode, Json) { }); (StatusCode::INTERNAL_SERVER_ERROR, Json(err)) } + +fn get_payment_info( + invoice: &MutinyInvoice, +) -> Result<(bool, u64, u64), (StatusCode, Json)> { + let (is_paid, amount_sat, fees) = match invoice.status { + HTLCStatus::Succeeded => { + let amount_sat = invoice.amount_sats.ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to fetch payment"})), + ))?; + + let fees = invoice.fees_paid.ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "unable to fetch payment"})), + ))?; + + (true, amount_sat, fees) + } + _ => (false, 0, 0), + }; + Ok((is_paid, amount_sat, fees)) +}