feat: setup webhook bridge for bitvora

This commit is contained in:
2025-03-03 14:14:40 +00:00
parent b80048e372
commit 80ae12b33f
9 changed files with 145 additions and 19 deletions

2
Cargo.lock generated
View File

@ -2037,6 +2037,7 @@ dependencies = [
"ssh-key", "ssh-key",
"ssh2", "ssh2",
"tokio", "tokio",
"tokio-stream",
"tokio-tungstenite 0.21.0", "tokio-tungstenite 0.21.0",
"urlencoding", "urlencoding",
"virt", "virt",
@ -4210,6 +4211,7 @@ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tokio-util",
] ]
[[package]] [[package]]

View File

@ -7,17 +7,17 @@ edition = "2021"
name = "api" name = "api"
[features] [features]
default = ["mikrotik", "nostr-dm", "proxmox", "lnd"] default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "bitvora"]
mikrotik = ["dep:reqwest"] mikrotik = ["dep:reqwest"]
nostr-dm = ["dep:nostr-sdk"] nostr-dm = ["dep:nostr-sdk"]
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"] proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
libvirt = ["dep:virt"] libvirt = ["dep:virt"]
lnd = ["dep:fedimint-tonic-lnd"] lnd = ["dep:fedimint-tonic-lnd"]
bitvora = ["dep:reqwest"] bitvora = ["dep:reqwest", "dep:tokio-stream"]
[dependencies] [dependencies]
lnvps_db = { path = "lnvps_db" } lnvps_db = { path = "lnvps_db" }
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros", "sync"] }
anyhow = "1.0.83" anyhow = "1.0.83"
config = { version = "0.15.8", features = ["yaml"] } config = { version = "0.15.8", features = ["yaml"] }
log = "0.4.21" log = "0.4.21"
@ -45,7 +45,7 @@ nostr = { version = "0.39.0", default-features = false, features = ["std"] }
nostr-sdk = { version = "0.39.0", optional = true, default-features = false, features = ["nip44", "nip59"] } nostr-sdk = { version = "0.39.0", optional = true, default-features = false, features = ["nip44", "nip59"] }
#proxmox #proxmox
tokio-tungstenite = { version = "^0.21", features = ["native-tls"], optional = true} tokio-tungstenite = { version = "^0.21", features = ["native-tls"], optional = true }
ssh2 = { version = "0.9.4", optional = true } ssh2 = { version = "0.9.4", optional = true }
reqwest = { version = "0.12.8", optional = true } reqwest = { version = "0.12.8", optional = true }
@ -54,3 +54,6 @@ virt = { version = "0.4.2", optional = true }
#lnd #lnd
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true } fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc"], optional = true }
#bitvora
tokio-stream = { version = "0.1.17", features = ["sync"], optional = true }

View File

@ -5,7 +5,9 @@ A bitcoin powered VPS system.
## Requirements ## Requirements
- MySql database - MySql database
- LND node - Lightning node:
- LND
- [Bitvora](https://bitvora.com?r=lnvps)
- Proxmox server - Proxmox server
## Required Config ## Required Config
@ -14,22 +16,26 @@ A bitcoin powered VPS system.
# MySql database connection string # MySql database connection string
db: "mysql://root:root@localhost:3376/lnvps" db: "mysql://root:root@localhost:3376/lnvps"
# LND node connection details # LN node connection details (Only 1 allowed)
lightning: lightning:
lnd: lnd:
url: "https://127.0.0.1:10003" url: "https://127.0.0.1:10003"
cert: "$HOME/.lnd/tls.cert" cert: "$HOME/.lnd/tls.cert"
macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon" macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"
#bitvora:
# token: "my-api-token"
# webhook-secret: "my-webhook-secret"
# Number of days after a VM expires to delete # Number of days after a VM expires to delete
delete-after: 3 delete-after: 3
# Read-only mode prevents spawning VM's
read-only: false
# Provisioner is the main process which handles creating/deleting VM's # Provisioner is the main process which handles creating/deleting VM's
# Currently supports: Proxmox # Currently supports: Proxmox
provisioner: provisioner:
proxmox: proxmox:
# Read-only mode prevents spawning VM's
read-only: false
# Proxmox (QEMU) settings used for spawning VM's # Proxmox (QEMU) settings used for spawning VM's
qemu: qemu:
bios: "ovmf" bios: "ovmf"
@ -39,9 +45,17 @@ provisioner:
cpu: "kvm64" cpu: "kvm64"
vlan: 100 vlan: 100
kvm: false kvm: false
# Networking policy
network-policy:
# Configure network equipment on provisioning IP resources
access: "auto"
# Use SLAAC to auto-configure VM ipv6 addresses
ip6-slaac: true
``` ```
### Email notifications ### Email notifications
Email notifications can be enabled, this is primarily intended for admin notifications. Email notifications can be enabled, this is primarily intended for admin notifications.
```yaml ```yaml

View File

@ -1,4 +1,13 @@
use rocket::{routes, Route};
mod model; mod model;
mod routes; mod routes;
mod webhook;
pub use routes::routes; pub fn routes() -> Vec<Route> {
let mut r = routes::routes();
r.append(&mut webhook::routes());
r
}
pub use webhook::WEBHOOK_BRIDGE;

84
src/api/webhook.rs Normal file
View File

@ -0,0 +1,84 @@
use anyhow::anyhow;
use lettre::message::header::Headers;
use log::warn;
use reqwest::header::HeaderMap;
use reqwest::Request;
use rocket::data::{ByteUnit, FromData, ToByteUnit};
use rocket::http::Status;
use rocket::outcome::IntoOutcome;
use rocket::request::{FromRequest, Outcome};
use rocket::{post, routes, Data, Route};
use std::collections::HashMap;
use std::sync::LazyLock;
use tokio::io::AsyncReadExt;
use tokio::sync::broadcast;
/// Messaging bridge for webhooks to other parts of the system (bitvora)
pub static WEBHOOK_BRIDGE: LazyLock<WebhookBridge> = LazyLock::new(|| WebhookBridge::new());
pub fn routes() -> Vec<Route> {
if cfg!(feature = "bitvora") {
routes![bitvora_webhook]
} else {
routes![]
}
}
#[post("/api/v1/webhook/bitvora", data = "<req>")]
async fn bitvora_webhook(req: WebhookMessage) -> Status {
WEBHOOK_BRIDGE.send(req);
Status::Ok
}
#[derive(Debug, Clone)]
pub struct WebhookMessage {
pub body: Vec<u8>,
pub headers: HashMap<String, String>,
}
#[rocket::async_trait]
impl<'r> FromData<'r> for WebhookMessage {
type Error = ();
async fn from_data(
req: &'r rocket::Request<'_>,
data: Data<'r>,
) -> rocket::data::Outcome<'r, Self, Self::Error> {
let header = req
.headers()
.iter()
.map(|v| (v.name.to_string(), v.value.to_string()))
.collect();
let body = if let Ok(d) = data.open(4.megabytes()).into_bytes().await {
d
} else {
return rocket::data::Outcome::Error((Status::BadRequest, ()));
};
let msg = WebhookMessage {
headers: header,
body: body.value.to_vec(),
};
rocket::data::Outcome::Success(msg)
}
}
#[derive(Debug)]
pub struct WebhookBridge {
tx: broadcast::Sender<WebhookMessage>,
}
impl WebhookBridge {
pub fn new() -> Self {
let (tx, _rx) = broadcast::channel(100);
Self { tx }
}
pub fn send(&self, message: WebhookMessage) {
if let Err(e) = self.tx.send(message) {
warn!("Failed to send webhook message: {}", e);
}
}
pub fn listen(&self) -> broadcast::Receiver<WebhookMessage> {
self.tx.subscribe()
}
}

View File

@ -1,21 +1,25 @@
use crate::api::WEBHOOK_BRIDGE;
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode}; use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
use anyhow::bail; use anyhow::bail;
use futures::Stream; use futures::{Stream, StreamExt};
use lnvps_db::async_trait; use lnvps_db::async_trait;
use log::debug; use log::debug;
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
use reqwest::{Method, Url}; use reqwest::{Method, Url};
use rocket::http::ext::IntoCollection;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::pin::Pin; use std::pin::Pin;
use tokio_stream::wrappers::BroadcastStream;
pub struct BitvoraNode { pub struct BitvoraNode {
base: Url, base: Url,
client: reqwest::Client, client: reqwest::Client,
webhook_secret: String,
} }
impl BitvoraNode { impl BitvoraNode {
pub fn new(api_token: &str) -> Self { pub fn new(api_token: &str, webhook_secret: &str) -> Self {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( headers.insert(
"Authorization", "Authorization",
@ -30,6 +34,7 @@ impl BitvoraNode {
Self { Self {
base: Url::parse("https://api.bitvora.com/").unwrap(), base: Url::parse("https://api.bitvora.com/").unwrap(),
client, client,
webhook_secret: webhook_secret.to_string(),
} }
} }
@ -95,6 +100,13 @@ impl LightningNode for BitvoraNode {
let rsp: BitvoraResponse<CreateInvoiceResponse> = self let rsp: BitvoraResponse<CreateInvoiceResponse> = self
.req(Method::POST, "/v1/bitcoin/deposit/lightning-invoice", req) .req(Method::POST, "/v1/bitcoin/deposit/lightning-invoice", req)
.await?; .await?;
if rsp.status >= 400 {
bail!(
"API error: {} {}",
rsp.status,
rsp.message.unwrap_or_default()
);
}
Ok(AddInvoiceResult { Ok(AddInvoiceResult {
pr: rsp.data.payment_request, pr: rsp.data.payment_request,
payment_hash: rsp.data.r_hash, payment_hash: rsp.data.r_hash,
@ -103,9 +115,11 @@ impl LightningNode for BitvoraNode {
async fn subscribe_invoices( async fn subscribe_invoices(
&self, &self,
from_payment_hash: Option<Vec<u8>>, _from_payment_hash: Option<Vec<u8>>,
) -> anyhow::Result<Pin<Box<dyn Stream<Item = InvoiceUpdate> + Send>>> { ) -> anyhow::Result<Pin<Box<dyn Stream<Item = InvoiceUpdate> + Send>>> {
todo!() let rx = BroadcastStream::new(WEBHOOK_BRIDGE.listen());
let mapped = rx.then(|r| async move { InvoiceUpdate::Unknown });
Ok(Box::pin(mapped))
} }
} }
@ -118,8 +132,7 @@ struct CreateInvoiceRequest {
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
struct BitvoraResponse<T> struct BitvoraResponse<T> {
{
pub status: isize, pub status: isize,
pub message: Option<String>, pub message: Option<String>,
pub data: T, pub data: T,

View File

@ -1,6 +1,5 @@
use std::path::Path; use std::path::Path;
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode}; use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
use crate::settings::LndConfig;
use anyhow::Result; use anyhow::Result;
use fedimint_tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef; use fedimint_tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef;
use fedimint_tonic_lnd::invoicesrpc::LookupInvoiceMsg; use fedimint_tonic_lnd::invoicesrpc::LookupInvoiceMsg;

View File

@ -53,7 +53,7 @@ pub async fn get_node(settings: &Settings) -> Result<Arc<dyn LightningNode>> {
macaroon, macaroon,
} => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)), } => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)),
#[cfg(feature = "bitvora")] #[cfg(feature = "bitvora")]
LightningConfig::Bitvora { token } => Ok(Arc::new(bitvora::BitvoraNode::new(token))), LightningConfig::Bitvora { token, webhook_secret } => Ok(Arc::new(bitvora::BitvoraNode::new(token, webhook_secret))),
_ => anyhow::bail!("Unsupported lightning config!"), _ => anyhow::bail!("Unsupported lightning config!"),
} }
} }

View File

@ -52,8 +52,10 @@ pub enum LightningConfig {
cert: PathBuf, cert: PathBuf,
macaroon: PathBuf, macaroon: PathBuf,
}, },
#[serde(rename_all = "kebab-case")]
Bitvora { Bitvora {
token: String, token: String,
webhook_secret: String,
} }
} }