Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add external-ln-receive-pubkey #11

Merged
merged 2 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
462 changes: 44 additions & 418 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions fedimint-clientd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ serde_json = "1.0.108"
tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
fedimint-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.0" }
fedimint-core = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.0" }
fedimint-wallet-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.0" }
fedimint-mint-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.0" }
fedimint-ln-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.0" }
fedimint-rocksdb = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.0" }
fedimint-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.1" }
fedimint-core = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.1" }
fedimint-wallet-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.1" }
fedimint-mint-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.1" }
fedimint-ln-client = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.1" }
fedimint-rocksdb = { git = "https://github.com/fedimint/fedimint", tag = "v0.3.0-rc.1" }
url = "2.5.0"
lazy_static = "1.4.0"
async-utility = "0.2.0"
Expand Down
19 changes: 19 additions & 0 deletions fedimint-clientd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ async fn main() -> Result<()> {
/// Lightning network related commands:
/// - `/fedimint/v2/ln/invoice`: Create a lightning invoice to receive payment
/// via gateway.
/// - `/fedimint/v2/ln/invoice-external-pubkey`: Create a lightning invoice to
/// receive payment via gateway with external pubkey.
/// - `/fedimint/v2/ln/await-invoice`: Wait for incoming invoice to be paid.
/// - `/fedimint/v2/ln/claim-external-receive`: Claim an external receive.
/// - `/fedimint/v2/ln/pay`: Pay a lightning invoice or lnurl via a gateway.
/// - `/fedimint/v2/ln/await-pay`: Wait for a lightning payment to complete.
/// - `/fedimint/v2/ln/list-gateways`: List registered gateways.
Expand All @@ -204,10 +207,26 @@ fn fedimint_v2_rest() -> Router<AppState> {

let ln_router = Router::new()
.route("/invoice", post(fedimint::ln::invoice::handle_rest))
.route(
"/invoice-external-pubkey",
post(fedimint::ln::invoice_external_pubkey::handle_rest),
)
.route(
"/invoice-external-pubkey-tweaked",
post(fedimint::ln::invoice_external_pubkey_tweaked::handle_rest),
)
.route(
"/await-invoice",
post(fedimint::ln::await_invoice::handle_rest),
)
.route(
"/claim-external-receive",
post(fedimint::ln::claim_external_receive::handle_rest),
)
.route(
"/claim-external-receive-tweaked",
post(fedimint::ln::claim_external_receive_tweaked::handle_rest),
)
.route("/pay", post(fedimint::ln::pay::handle_rest))
.route(
"/list-gateways",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use anyhow::anyhow;
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use bitcoin::util::key::KeyPair;
use fedimint_client::ClientHandleArc;
use fedimint_core::config::FederationId;
use fedimint_ln_client::{LightningClientModule, LnReceiveState};
use futures_util::StreamExt;
use serde::Deserialize;
use serde_json::{json, Value};
use tracing::info;

use crate::error::AppError;
use crate::router::handlers::fedimint::admin::get_note_summary;
use crate::router::handlers::fedimint::admin::info::InfoResponse;
use crate::state::AppState;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaimExternalReceiveRequest {
pub private_key: SecretKey,
pub federation_id: FederationId,
}

async fn _await_claim_external_receive(
client: ClientHandleArc,
req: ClaimExternalReceiveRequest,
) -> Result<InfoResponse, AppError> {
let secp = Secp256k1::new();
let key_pair = KeyPair::from_secret_key(&secp, &req.private_key);
let lightning_module = &client.get_first_module::<LightningClientModule>();
let operation_id = lightning_module.scan_receive_for_user(key_pair, ()).await?;
let mut updates = lightning_module
.subscribe_ln_claim(operation_id)
.await?
.into_stream();

while let Some(update) = updates.next().await {
info!("Update: {update:?}");
match update {
LnReceiveState::Claimed => {
return Ok(get_note_summary(&client).await?);
}
LnReceiveState::Canceled { reason } => {
return Err(AppError::new(
StatusCode::INTERNAL_SERVER_ERROR,
anyhow!(reason),
))
}
_ => {}
}

info!("Update: {update:?}");
}

Err(AppError::new(
StatusCode::INTERNAL_SERVER_ERROR,
anyhow!("Unexpected end of stream"),
))
}

pub async fn handle_ws(state: AppState, v: Value) -> Result<Value, AppError> {
let v = serde_json::from_value::<ClaimExternalReceiveRequest>(v)
.map_err(|e| AppError::new(StatusCode::BAD_REQUEST, anyhow!("Invalid request: {}", e)))?;
let client = state.get_client(v.federation_id).await?;
let invoice = _await_claim_external_receive(client, v).await?;
let invoice_json = json!(invoice);
Ok(invoice_json)
}

#[axum_macros::debug_handler]
pub async fn handle_rest(
State(state): State<AppState>,
Json(req): Json<ClaimExternalReceiveRequest>,
) -> Result<Json<InfoResponse>, AppError> {
let client = state.get_client(req.federation_id).await?;
let invoice = _await_claim_external_receive(client, req).await?;
Ok(Json(invoice))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use anyhow::anyhow;
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use bitcoin::util::key::KeyPair;
use fedimint_client::ClientHandleArc;
use fedimint_core::config::FederationId;
use fedimint_ln_client::{LightningClientModule, LnReceiveState};
use futures_util::StreamExt;
use serde::Deserialize;
use serde_json::{json, Value};
use tracing::info;

use crate::error::AppError;
use crate::router::handlers::fedimint::admin::get_note_summary;
use crate::router::handlers::fedimint::admin::info::InfoResponse;
use crate::state::AppState;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaimExternalReceiveTweakedRequest {
pub tweaks: Vec<u64>,
pub private_key: SecretKey,
pub federation_id: FederationId,
}

async fn _await_claim_external_receive_tweaked(
client: ClientHandleArc,
req: ClaimExternalReceiveTweakedRequest,
) -> Result<InfoResponse, AppError> {
let secp = Secp256k1::new();
let key_pair = KeyPair::from_secret_key(&secp, &req.private_key);
let lightning_module = &client.get_first_module::<LightningClientModule>();
let operation_id = lightning_module
.scan_receive_for_user_tweaked(key_pair, req.tweaks, ())
.await;

let mut final_response = get_note_summary(&client).await?;
for operation_id in operation_id {
let mut updates = lightning_module
.subscribe_ln_claim(operation_id)
.await?
.into_stream();

while let Some(update) = updates.next().await {
info!("Update: {update:?}");
match update {
LnReceiveState::Claimed => {
final_response = get_note_summary(&client).await?;
}
LnReceiveState::Canceled { reason } => {
return Err(AppError::new(
StatusCode::INTERNAL_SERVER_ERROR,
anyhow!(reason),
))
}
_ => {}
}

info!("Update: {update:?}");
}
}

Ok(final_response)
}

pub async fn handle_ws(state: AppState, v: Value) -> Result<Value, AppError> {
let v = serde_json::from_value::<ClaimExternalReceiveTweakedRequest>(v)
.map_err(|e| AppError::new(StatusCode::BAD_REQUEST, anyhow!("Invalid request: {}", e)))?;
let client = state.get_client(v.federation_id).await?;
let invoice = _await_claim_external_receive_tweaked(client, v).await?;
let invoice_json = json!(invoice);
Ok(invoice_json)
}

#[axum_macros::debug_handler]
pub async fn handle_rest(
State(state): State<AppState>,
Json(req): Json<ClaimExternalReceiveTweakedRequest>,
) -> Result<Json<InfoResponse>, AppError> {
let client = state.get_client(req.federation_id).await?;
let invoice = _await_claim_external_receive_tweaked(client, req).await?;
Ok(Json(invoice))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use anyhow::anyhow;
use axum::extract::State;
use axum::http::StatusCode;
use axum::Json;
use bitcoin::secp256k1::PublicKey;
use fedimint_client::ClientHandleArc;
use fedimint_core::config::FederationId;
use fedimint_core::core::OperationId;
use fedimint_core::Amount;
use fedimint_ln_client::LightningClientModule;
use lightning_invoice::{Bolt11InvoiceDescription, Description};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tracing::error;

use crate::error::AppError;
use crate::state::AppState;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LnInvoiceExternalPubkeyRequest {
pub amount_msat: Amount,
pub description: String,
pub expiry_time: Option<u64>,
pub external_pubkey: PublicKey,
pub federation_id: FederationId,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LnInvoiceExternalPubkeyResponse {
pub operation_id: OperationId,
pub invoice: String,
}

async fn _invoice(
client: ClientHandleArc,
req: LnInvoiceExternalPubkeyRequest,
) -> Result<LnInvoiceExternalPubkeyResponse, AppError> {
let lightning_module = client.get_first_module::<LightningClientModule>();
let gateway_id = match lightning_module.list_gateways().await.first() {
Some(gateway_announcement) => gateway_announcement.info.gateway_id,
None => {
error!("No gateways available");
return Err(AppError::new(
StatusCode::INTERNAL_SERVER_ERROR,
anyhow!("No gateways available"),
));
}
};
let gateway = lightning_module
.select_gateway(&gateway_id)
.await
.ok_or_else(|| {
error!("Failed to select gateway");
AppError::new(
StatusCode::INTERNAL_SERVER_ERROR,
anyhow!("Failed to select gateway"),
)
})?;

let (operation_id, invoice, _) = lightning_module
.create_bolt11_invoice_for_user(
req.amount_msat,
Bolt11InvoiceDescription::Direct(&Description::new(req.description)?),
req.expiry_time,
req.external_pubkey.clone(),
(),
Some(gateway),
)
.await?;
Ok(LnInvoiceExternalPubkeyResponse {
operation_id,
invoice: invoice.to_string(),
})
}

pub async fn handle_ws(state: AppState, v: Value) -> Result<Value, AppError> {
let v = serde_json::from_value::<LnInvoiceExternalPubkeyRequest>(v)
.map_err(|e| AppError::new(StatusCode::BAD_REQUEST, anyhow!("Invalid request: {}", e)))?;
let client = state.get_client(v.federation_id).await?;
let invoice = _invoice(client, v).await?;
let invoice_json = json!(invoice);
Ok(invoice_json)
}

#[axum_macros::debug_handler]
pub async fn handle_rest(
State(state): State<AppState>,
Json(req): Json<LnInvoiceExternalPubkeyRequest>,
) -> Result<Json<LnInvoiceExternalPubkeyResponse>, AppError> {
let client = state.get_client(req.federation_id).await?;
let invoice = _invoice(client, req).await?;
Ok(Json(invoice))
}
Loading
Loading