feat: setup webhook bridge for bitvora
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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]]
|
||||
|
11
Cargo.toml
11
Cargo.toml
@ -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 }
|
22
README.md
22
README.md
@ -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
|
||||
|
@ -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
84
src/api/webhook.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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!"),
|
||||
}
|
||||
}
|
||||
|
@ -52,8 +52,10 @@ pub enum LightningConfig {
|
||||
cert: PathBuf,
|
||||
macaroon: PathBuf,
|
||||
},
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
Bitvora {
|
||||
token: String,
|
||||
webhook_secret: String,
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user