diff --git a/README.md b/README.md index 40e6b32..07fbf27 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,34 @@ -``` +# MutinyNet Faucet API + +1. Copy `.env.sample` to `.env.local` and fill it out with bitcoind and lnd connection info +2. Run `cargo build && cargo start` + +## Endpoint examples + +```sh curl -X POST \ http://localhost:3001/api/onchain \ -H 'Content-Type: application/json' \ -d '{"sats":10000,"address":"bcrt1..."}' ``` -``` +```sh curl -X POST \ http://localhost:3001/api/lightning \ -H 'Content-Type: application/json' \ -d '{"bolt11": "..."}' ``` -``` +```sh curl -X POST \ http://localhost:3001/api/bolt11 \ -H 'Content-Type: application/json' \ -d '{"amount_sats": 1234}' -``` \ No newline at end of file +``` + +```sh +curl -X POST \ + http://localhost:3001/api/channel \ + -H 'Content-Type: application/json' \ + -d '{"capacity": 2468,"push_amount": 1234,"pubkey":"023...","host":"127.0.0.1:9735"}' +``` diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..63351cd --- /dev/null +++ b/src/channel.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +use std::sync::{Arc, Mutex}; +use tonic_openssl_lnd::lnrpc::{self, channel_point}; + +use crate::{AppState, MAX_SEND_AMOUNT}; + +#[derive(Clone, Deserialize)] +pub struct ChannelRequest { + capacity: i64, + push_amount: i64, + pubkey: String, + host: Option, +} + +#[derive(Clone, Serialize)] +pub struct ChannelResponse { + pub txid: String, +} + +pub async fn open_channel( + state: Arc>, + payload: ChannelRequest, +) -> anyhow::Result { + if payload.capacity > MAX_SEND_AMOUNT.try_into().unwrap() { + anyhow::bail!("max capacity is 10,000,000"); + } + if payload.push_amount < 0 { + anyhow::bail!("push_amount must be positive"); + } + if payload.push_amount > payload.capacity { + anyhow::bail!("push_amount must be less than or equal to capacity"); + } + + let node_pubkey_result = hex::decode(payload.pubkey.clone()); + let node_pubkey = match node_pubkey_result { + Ok(pubkey) => pubkey, + Err(e) => anyhow::bail!("invalid pubkey: {}", e), + }; + + let channel_point = { + let mut lightning_client = state + .clone() + .lock() + .map_err(|_| anyhow::anyhow!("failed to get lock"))? + .lightning_client + .clone(); + + if let Some(host) = payload.host { + lightning_client + .connect_peer(lnrpc::ConnectPeerRequest { + addr: Some(lnrpc::LightningAddress { + pubkey: payload.pubkey.clone(), + host, + }), + ..Default::default() + }) + .await + .ok(); + } + + lightning_client + .open_channel_sync(lnrpc::OpenChannelRequest { + node_pubkey, + local_funding_amount: payload.capacity, + push_sat: payload.push_amount, + ..Default::default() + }) + .await? + .into_inner() + }; + + let txid = match channel_point.funding_txid { + Some(channel_point::FundingTxid::FundingTxidBytes(bytes)) => hex::encode(bytes), + Some(channel_point::FundingTxid::FundingTxidStr(string)) => string, + None => anyhow::bail!("failed to open channel"), + }; + + Ok(txid) +} diff --git a/src/main.rs b/src/main.rs index 39770d1..29fb9bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,11 +16,13 @@ use tonic_openssl_lnd::LndLightningClient; use tower_http::cors::{Any, CorsLayer}; mod bolt11; +mod channel; mod lightning; mod onchain; mod setup; use bolt11::{request_bolt11, Bolt11Request, Bolt11Response}; +use channel::{open_channel, ChannelRequest, ChannelResponse}; use lightning::{pay_lightning, LightningRequest, LightningResponse}; use onchain::{pay_onchain, OnchainRequest, OnchainResponse}; use setup::setup; @@ -57,6 +59,7 @@ async fn main() -> anyhow::Result<()> { .route("/api/onchain", post(onchain_handler)) .route("/api/lightning", post(lightning_handler)) .route("/api/bolt11", post(bolt11_handler)) + .route("/api/channel", post(channel_handler)) .with_state(state) .layer( CorsLayer::new() @@ -105,6 +108,16 @@ async fn bolt11_handler( Ok(Json(Bolt11Response { bolt11 })) } +#[axum::debug_handler] +async fn channel_handler( + State(state): State, + Json(payload): Json, +) -> Result, AppError> { + let txid = open_channel(state.clone(), payload.clone()).await?; + + Ok(Json(ChannelResponse { txid })) +} + // Make our own error that wraps `anyhow::Error`. struct AppError(anyhow::Error); diff --git a/src/onchain.rs b/src/onchain.rs index 76f9402..17522c8 100644 --- a/src/onchain.rs +++ b/src/onchain.rs @@ -24,7 +24,7 @@ pub async fn pay_onchain( payload: OnchainRequest, ) -> anyhow::Result { if payload.sats > MAX_SEND_AMOUNT { - anyhow::bail!("max amount is 1,000,000"); + anyhow::bail!("max amount is 10,000,000"); } let txid = {