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",
"ssh2",
"tokio",
"tokio-stream",
"tokio-tungstenite 0.21.0",
"urlencoding",
"virt",
@ -4210,6 +4211,7 @@ dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]

View File

@ -7,17 +7,17 @@ edition = "2021"
name = "api"
[features]
default = ["mikrotik", "nostr-dm", "proxmox", "lnd"]
default = ["mikrotik", "nostr-dm", "proxmox", "lnd", "bitvora"]
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"]
bitvora = ["dep:reqwest", "dep:tokio-stream"]
[dependencies]
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"
config = { version = "0.15.8", features = ["yaml"] }
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"] }
#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 }
reqwest = { version = "0.12.8", optional = true }
@ -54,3 +54,6 @@ virt = { version = "0.4.2", optional = true }
#lnd
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
- MySql database
- LND node
- Lightning node:
- LND
- [Bitvora](https://bitvora.com?r=lnvps)
- Proxmox server
## Required Config
@ -14,22 +16,26 @@ A bitcoin powered VPS system.
# MySql database connection string
db: "mysql://root:root@localhost:3376/lnvps"
# LND node connection details
# LN node connection details (Only 1 allowed)
lightning:
lnd:
url: "https://127.0.0.1:10003"
cert: "$HOME/.lnd/tls.cert"
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
delete-after: 3
# Read-only mode prevents spawning VM's
read-only: false
# Provisioner is the main process which handles creating/deleting VM's
# Currently supports: Proxmox
provisioner:
proxmox:
# Read-only mode prevents spawning VM's
read-only: false
# Proxmox (QEMU) settings used for spawning VM's
qemu:
bios: "ovmf"
@ -39,9 +45,17 @@ provisioner:
cpu: "kvm64"
vlan: 100
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 can be enabled, this is primarily intended for admin notifications.
```yaml
@ -75,7 +89,7 @@ nostr:
### Network Setup (Advanced)
When ARP is disabled (reply-only) on your router you may need to create static ARP entries when allocating
When ARP is disabled (reply-only) on your router you may need to create static ARP entries when allocating
IPs, we support managing ARP entries on routers directly as part of the provisioning process.
```yaml

View File

@ -1,4 +1,13 @@
use rocket::{routes, Route};
mod model;
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 anyhow::bail;
use futures::Stream;
use futures::{Stream, StreamExt};
use lnvps_db::async_trait;
use log::debug;
use reqwest::header::HeaderMap;
use reqwest::{Method, Url};
use rocket::http::ext::IntoCollection;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::pin::Pin;
use tokio_stream::wrappers::BroadcastStream;
pub struct BitvoraNode {
base: Url,
client: reqwest::Client,
webhook_secret: String,
}
impl BitvoraNode {
pub fn new(api_token: &str) -> Self {
pub fn new(api_token: &str, webhook_secret: &str) -> Self {
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
@ -30,6 +34,7 @@ impl BitvoraNode {
Self {
base: Url::parse("https://api.bitvora.com/").unwrap(),
client,
webhook_secret: webhook_secret.to_string(),
}
}
@ -95,6 +100,13 @@ impl LightningNode for BitvoraNode {
let rsp: BitvoraResponse<CreateInvoiceResponse> = self
.req(Method::POST, "/v1/bitcoin/deposit/lightning-invoice", req)
.await?;
if rsp.status >= 400 {
bail!(
"API error: {} {}",
rsp.status,
rsp.message.unwrap_or_default()
);
}
Ok(AddInvoiceResult {
pr: rsp.data.payment_request,
payment_hash: rsp.data.r_hash,
@ -103,9 +115,11 @@ impl LightningNode for BitvoraNode {
async fn subscribe_invoices(
&self,
from_payment_hash: Option<Vec<u8>>,
_from_payment_hash: Option<Vec<u8>>,
) -> 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)]
struct BitvoraResponse<T>
{
struct BitvoraResponse<T> {
pub status: isize,
pub message: Option<String>,
pub data: T,

View File

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

View File

@ -53,7 +53,7 @@ pub async fn get_node(settings: &Settings) -> Result<Arc<dyn LightningNode>> {
macaroon,
} => Ok(Arc::new(lnd::LndNode::new(url, cert, macaroon).await?)),
#[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!"),
}
}

View File

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