diff --git a/Cargo.lock b/Cargo.lock index 74e48c5..e327341 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2027,7 +2027,6 @@ dependencies = [ "nostr-sdk", "pretty_env_logger", "rand 0.9.0", - "regex", "reqwest", "rocket", "rocket_okapi", diff --git a/Cargo.toml b/Cargo.toml index 03989b5..b5d9a93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,13 @@ edition = "2021" name = "api" [features] -default = ["mikrotik", "nostr-dm", "proxmox"] +default = ["mikrotik", "nostr-dm", "proxmox", "lnd"] mikrotik = ["dep:reqwest"] nostr-dm = ["dep:nostr-sdk"] proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"] libvirt = ["dep:virt"] +lnd = ["dep:fedimint-tonic-lnd"] +bitvora = ["dep:reqwest"] [dependencies] lnvps_db = { path = "lnvps_db" } @@ -28,7 +30,6 @@ schemars = { version = "0.8.22", features = ["chrono"] } chrono = { version = "0.4.38", features = ["serde"] } base64 = { version = "0.22.1", features = ["alloc"] } urlencoding = "2.1.3" -fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"] } ipnetwork = { git = "https://git.v0l.io/Kieran/ipnetwork.git", rev = "35977adc8103cfc232bc95fbc32f4e34f2b6a6d7" } rand = "0.9.0" clap = { version = "4.5.21", features = ["derive"] } @@ -50,4 +51,6 @@ reqwest = { version = "0.12.8", optional = true } #libvirt virt = { version = "0.4.2", optional = true } -regex = "1.11.1" + +#lnd +fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true } diff --git a/README.md b/README.md index 1066fc5..218a49f 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,11 @@ A bitcoin powered VPS system. db: "mysql://root:root@localhost:3376/lnvps" # LND node connection details -lnd: - url: "https://127.0.0.1:10003" - cert: "$HOME/.lnd/tls.cert" - macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" +lightning: + lnd: + url: "https://127.0.0.1:10003" + cert: "$HOME/.lnd/tls.cert" + macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" # Number of days after a VM expires to delete delete-after: 3 diff --git a/config.yaml b/config.yaml index 371de64..4ea6fd9 100644 --- a/config.yaml +++ b/config.yaml @@ -1,8 +1,9 @@ db: "mysql://root:root@localhost:3376/lnvps" -lnd: - url: "https://127.0.0.1:10003" - cert: "/home/kieran/.polar/networks/2/volumes/lnd/alice/tls.cert" - macaroon: "/home/kieran/.polar/networks/2/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon" +lightning: + lnd: + url: "https://127.0.0.1:10003" + cert: "/home/kieran/.polar/networks/2/volumes/lnd/alice/tls.cert" + macaroon: "/home/kieran/.polar/networks/2/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon" delete-after: 3 provisioner: proxmox: diff --git a/src/lightning/bitvora.rs b/src/lightning/bitvora.rs new file mode 100644 index 0000000..bb9fd80 --- /dev/null +++ b/src/lightning/bitvora.rs @@ -0,0 +1,133 @@ +use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode}; +use anyhow::bail; +use futures::Stream; +use lnvps_db::async_trait; +use log::debug; +use reqwest::header::HeaderMap; +use reqwest::{Method, Url}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::pin::Pin; + +pub struct BitvoraNode { + base: Url, + client: reqwest::Client, +} + +impl BitvoraNode { + pub fn new(api_token: &str) -> Self { + let mut headers = HeaderMap::new(); + headers.insert( + "Authorization", + format!("Bearer {}", api_token).parse().unwrap(), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .build() + .unwrap(); + + Self { + base: Url::parse("https://api.bitvora.com/").unwrap(), + client, + } + } + + async fn get(&self, path: &str) -> anyhow::Result { + debug!(">> GET {}", path); + let rsp = self.client.get(self.base.join(path)?).send().await?; + let status = rsp.status(); + let text = rsp.text().await?; + #[cfg(debug_assertions)] + debug!("<< {}", text); + if status.is_success() { + Ok(serde_json::from_str(&text)?) + } else { + bail!("{}", status); + } + } + + async fn post( + &self, + path: &str, + body: R, + ) -> anyhow::Result { + self.req(Method::POST, path, body).await + } + + async fn req( + &self, + method: Method, + path: &str, + body: R, + ) -> anyhow::Result { + let body = serde_json::to_string(&body)?; + debug!(">> {} {}: {}", method.clone(), path, &body); + let rsp = self + .client + .request(method.clone(), self.base.join(path)?) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .body(body) + .send() + .await?; + let status = rsp.status(); + let text = rsp.text().await?; + #[cfg(debug_assertions)] + debug!("<< {}", text); + if status.is_success() { + Ok(serde_json::from_str(&text)?) + } else { + bail!("{} {}: {}: {}", method, path, status, &text); + } + } +} + +#[async_trait] +impl LightningNode for BitvoraNode { + async fn add_invoice(&self, req: AddInvoiceRequest) -> anyhow::Result { + let req = CreateInvoiceRequest { + amount: req.amount / 1000, + currency: "sats".to_string(), + description: req.memo.unwrap_or_default(), + expiry_seconds: req.expire.unwrap_or(3600) as u64, + }; + let rsp: BitvoraResponse = self + .req(Method::POST, "/v1/bitcoin/deposit/lightning-invoice", req) + .await?; + Ok(AddInvoiceResult { + pr: rsp.data.payment_request, + payment_hash: rsp.data.r_hash, + }) + } + + async fn subscribe_invoices( + &self, + from_payment_hash: Option>, + ) -> anyhow::Result + Send>>> { + todo!() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CreateInvoiceRequest { + pub amount: u64, + pub currency: String, + pub description: String, + pub expiry_seconds: u64, +} + +#[derive(Debug, Clone, Deserialize)] +struct BitvoraResponse +{ + pub status: isize, + pub message: Option, + pub data: T, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CreateInvoiceResponse { + pub id: String, + pub r_hash: String, + pub payment_request: String, +} diff --git a/src/lightning/lnd.rs b/src/lightning/lnd.rs index 73a4378..505aebd 100644 --- a/src/lightning/lnd.rs +++ b/src/lightning/lnd.rs @@ -1,3 +1,4 @@ +use std::path::Path; use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode}; use crate::settings::LndConfig; use anyhow::Result; @@ -16,13 +17,8 @@ pub struct LndNode { } impl LndNode { - pub async fn new(settings: &LndConfig) -> Result { - let lnd = connect( - settings.url.clone(), - settings.cert.clone(), - settings.macaroon.clone(), - ) - .await?; + pub async fn new(url: &str, cert: &Path, macaroon: &Path) -> Result { + let lnd = connect(url.to_string(), cert, macaroon).await?; Ok(Self { client: lnd }) } } diff --git a/src/lightning/mod.rs b/src/lightning/mod.rs index 9948fd8..00e6969 100644 --- a/src/lightning/mod.rs +++ b/src/lightning/mod.rs @@ -1,11 +1,13 @@ -use crate::lightning::lnd::LndNode; -use crate::settings::Settings; +use crate::settings::{LightningConfig, Settings}; use anyhow::Result; use futures::Stream; use lnvps_db::async_trait; use std::pin::Pin; use std::sync::Arc; +#[cfg(feature = "bitvora")] +mod bitvora; +#[cfg(feature = "lnd")] mod lnd; /// Generic lightning node for creating payments @@ -43,5 +45,15 @@ pub enum InvoiceUpdate { } pub async fn get_node(settings: &Settings) -> Result> { - Ok(Arc::new(LndNode::new(&settings.lnd).await?)) + match &settings.lightning { + #[cfg(feature = "lnd")] + LightningConfig::LND { + url, + cert, + macaroon, + } => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)), + #[cfg(feature = "bitvora")] + LightningConfig::Bitvora { token } => Ok(Arc::new(bitvora::BitvoraNode::new(token))), + _ => anyhow::bail!("Unsupported lightning config!"), + } } diff --git a/src/provisioner/lnvps.rs b/src/provisioner/lnvps.rs index 5655c1f..c645026 100644 --- a/src/provisioner/lnvps.rs +++ b/src/provisioner/lnvps.rs @@ -6,7 +6,6 @@ use crate::router::Router; use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Settings}; use anyhow::{bail, Result}; use chrono::{Days, Months, Utc}; -use fedimint_tonic_lnd::tonic::async_trait; use futures::future::join_all; use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment}; use log::{debug, info, warn}; diff --git a/src/settings.rs b/src/settings.rs index a9888e3..220aa3f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -13,7 +13,9 @@ use std::sync::Arc; pub struct Settings { pub listen: Option, pub db: String, - pub lnd: LndConfig, + + /// Lightning node config for creating LN payments + pub lightning: LightningConfig, /// Readonly mode, don't spawn any VM's pub read_only: bool, @@ -42,10 +44,17 @@ pub struct Settings { } #[derive(Debug, Clone, Deserialize, Serialize)] -pub struct LndConfig { - pub url: String, - pub cert: PathBuf, - pub macaroon: PathBuf, +#[serde(rename_all = "kebab-case")] +pub enum LightningConfig { + #[serde(rename = "lnd")] + LND { + url: String, + cert: PathBuf, + macaroon: PathBuf, + }, + Bitvora { + token: String, + } } #[derive(Debug, Clone, Deserialize, Serialize)]