feat: setup bitvora api
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2027,7 +2027,6 @@ dependencies = [
|
|||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"regex",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_okapi",
|
"rocket_okapi",
|
||||||
|
@ -7,11 +7,13 @@ edition = "2021"
|
|||||||
name = "api"
|
name = "api"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mikrotik", "nostr-dm", "proxmox"]
|
default = ["mikrotik", "nostr-dm", "proxmox", "lnd"]
|
||||||
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"]
|
||||||
|
bitvora = ["dep:reqwest"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lnvps_db = { path = "lnvps_db" }
|
lnvps_db = { path = "lnvps_db" }
|
||||||
@ -28,7 +30,6 @@ schemars = { version = "0.8.22", features = ["chrono"] }
|
|||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
base64 = { version = "0.22.1", features = ["alloc"] }
|
base64 = { version = "0.22.1", features = ["alloc"] }
|
||||||
urlencoding = "2.1.3"
|
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" }
|
ipnetwork = { git = "https://git.v0l.io/Kieran/ipnetwork.git", rev = "35977adc8103cfc232bc95fbc32f4e34f2b6a6d7" }
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
clap = { version = "4.5.21", features = ["derive"] }
|
clap = { version = "4.5.21", features = ["derive"] }
|
||||||
@ -50,4 +51,6 @@ reqwest = { version = "0.12.8", optional = true }
|
|||||||
|
|
||||||
#libvirt
|
#libvirt
|
||||||
virt = { version = "0.4.2", optional = true }
|
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 }
|
||||||
|
@ -15,10 +15,11 @@ A bitcoin powered VPS system.
|
|||||||
db: "mysql://root:root@localhost:3376/lnvps"
|
db: "mysql://root:root@localhost:3376/lnvps"
|
||||||
|
|
||||||
# LND node connection details
|
# LND node connection details
|
||||||
lnd:
|
lightning:
|
||||||
url: "https://127.0.0.1:10003"
|
lnd:
|
||||||
cert: "$HOME/.lnd/tls.cert"
|
url: "https://127.0.0.1:10003"
|
||||||
macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"
|
cert: "$HOME/.lnd/tls.cert"
|
||||||
|
macaroon: "$HOME/.lnd/data/chain/bitcoin/mainnet/admin.macaroon"
|
||||||
|
|
||||||
# Number of days after a VM expires to delete
|
# Number of days after a VM expires to delete
|
||||||
delete-after: 3
|
delete-after: 3
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
db: "mysql://root:root@localhost:3376/lnvps"
|
db: "mysql://root:root@localhost:3376/lnvps"
|
||||||
lnd:
|
lightning:
|
||||||
url: "https://127.0.0.1:10003"
|
lnd:
|
||||||
cert: "/home/kieran/.polar/networks/2/volumes/lnd/alice/tls.cert"
|
url: "https://127.0.0.1:10003"
|
||||||
macaroon: "/home/kieran/.polar/networks/2/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon"
|
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
|
delete-after: 3
|
||||||
provisioner:
|
provisioner:
|
||||||
proxmox:
|
proxmox:
|
||||||
|
133
src/lightning/bitvora.rs
Normal file
133
src/lightning/bitvora.rs
Normal file
@ -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<T: DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
|
||||||
|
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<T: DeserializeOwned, R: Serialize>(
|
||||||
|
&self,
|
||||||
|
path: &str,
|
||||||
|
body: R,
|
||||||
|
) -> anyhow::Result<T> {
|
||||||
|
self.req(Method::POST, path, body).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn req<T: DeserializeOwned, R: Serialize>(
|
||||||
|
&self,
|
||||||
|
method: Method,
|
||||||
|
path: &str,
|
||||||
|
body: R,
|
||||||
|
) -> anyhow::Result<T> {
|
||||||
|
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<AddInvoiceResult> {
|
||||||
|
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<CreateInvoiceResponse> = 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<Vec<u8>>,
|
||||||
|
) -> anyhow::Result<Pin<Box<dyn Stream<Item = InvoiceUpdate> + 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<T>
|
||||||
|
{
|
||||||
|
pub status: isize,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct CreateInvoiceResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub r_hash: String,
|
||||||
|
pub payment_request: String,
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::path::Path;
|
||||||
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
use crate::lightning::{AddInvoiceRequest, AddInvoiceResult, InvoiceUpdate, LightningNode};
|
||||||
use crate::settings::LndConfig;
|
use crate::settings::LndConfig;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -16,13 +17,8 @@ pub struct LndNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LndNode {
|
impl LndNode {
|
||||||
pub async fn new(settings: &LndConfig) -> Result<Self> {
|
pub async fn new(url: &str, cert: &Path, macaroon: &Path) -> Result<Self> {
|
||||||
let lnd = connect(
|
let lnd = connect(url.to_string(), cert, macaroon).await?;
|
||||||
settings.url.clone(),
|
|
||||||
settings.cert.clone(),
|
|
||||||
settings.macaroon.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Self { client: lnd })
|
Ok(Self { client: lnd })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
use crate::lightning::lnd::LndNode;
|
use crate::settings::{LightningConfig, Settings};
|
||||||
use crate::settings::Settings;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use lnvps_db::async_trait;
|
use lnvps_db::async_trait;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg(feature = "bitvora")]
|
||||||
|
mod bitvora;
|
||||||
|
#[cfg(feature = "lnd")]
|
||||||
mod lnd;
|
mod lnd;
|
||||||
|
|
||||||
/// Generic lightning node for creating payments
|
/// Generic lightning node for creating payments
|
||||||
@ -43,5 +45,15 @@ pub enum InvoiceUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_node(settings: &Settings) -> Result<Arc<dyn LightningNode>> {
|
pub async fn get_node(settings: &Settings) -> Result<Arc<dyn LightningNode>> {
|
||||||
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!"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ use crate::router::Router;
|
|||||||
use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Settings};
|
use crate::settings::{NetworkAccessPolicy, NetworkPolicy, ProvisionerConfig, Settings};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use chrono::{Days, Months, Utc};
|
use chrono::{Days, Months, Utc};
|
||||||
use fedimint_tonic_lnd::tonic::async_trait;
|
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
use lnvps_db::{DiskType, IpRange, LNVpsDb, Vm, VmCostPlanIntervalType, VmIpAssignment, VmPayment};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
|
@ -13,7 +13,9 @@ use std::sync::Arc;
|
|||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub listen: Option<String>,
|
pub listen: Option<String>,
|
||||||
pub db: String,
|
pub db: String,
|
||||||
pub lnd: LndConfig,
|
|
||||||
|
/// Lightning node config for creating LN payments
|
||||||
|
pub lightning: LightningConfig,
|
||||||
|
|
||||||
/// Readonly mode, don't spawn any VM's
|
/// Readonly mode, don't spawn any VM's
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
@ -42,10 +44,17 @@ pub struct Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct LndConfig {
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub url: String,
|
pub enum LightningConfig {
|
||||||
pub cert: PathBuf,
|
#[serde(rename = "lnd")]
|
||||||
pub macaroon: PathBuf,
|
LND {
|
||||||
|
url: String,
|
||||||
|
cert: PathBuf,
|
||||||
|
macaroon: PathBuf,
|
||||||
|
},
|
||||||
|
Bitvora {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
Reference in New Issue
Block a user