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",
|
"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]]
|
||||||
|
11
Cargo.toml
11
Cargo.toml
@ -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 }
|
22
README.md
22
README.md
@ -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
|
||||||
|
@ -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
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 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,
|
||||||
|
@ -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;
|
||||||
|
@ -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!"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user