feat: exchange rates
This commit is contained in:
parent
7d8956e7c7
commit
088f22cea4
157
lnvps_db/Cargo.lock
generated
157
lnvps_db/Cargo.lock
generated
@ -245,6 +245,41 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -256,6 +291,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@ -349,6 +394,12 @@ dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@ -457,6 +508,12 @@ version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@ -668,6 +725,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
@ -689,6 +752,17 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.6.0"
|
||||
@ -697,6 +771,7 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -766,6 +841,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"sqlx",
|
||||
]
|
||||
|
||||
@ -855,6 +931,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
@ -989,6 +1071,12 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
@ -1150,6 +1238,36 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.6.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@ -1280,7 +1398,7 @@ dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"indexmap 2.6.0",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@ -1460,6 +1578,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@ -1521,6 +1645,37 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
|
5
lnvps_db/build.rs
Normal file
5
lnvps_db/build.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// generated by `sqlx migrate build-script`
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
5
lnvps_db/migrations/20241126205723_rates.sql
Normal file
5
lnvps_db/migrations/20241126205723_rates.sql
Normal file
@ -0,0 +1,5 @@
|
||||
alter table vm_payment
|
||||
add column rate float;
|
||||
update vm_payment set rate = 92000;
|
||||
alter table vm_payment
|
||||
modify column rate float not null;
|
@ -1,10 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub mod hydrate;
|
||||
mod model;
|
||||
#[cfg(feature = "mysql")]
|
||||
mod mysql;
|
||||
pub mod hydrate;
|
||||
|
||||
pub use model::*;
|
||||
#[cfg(feature = "mysql")]
|
||||
|
@ -246,6 +246,8 @@ pub struct VmPayment {
|
||||
pub amount: u64,
|
||||
pub invoice: String,
|
||||
pub is_paid: bool,
|
||||
/// Exchange rate
|
||||
pub rate: f32,
|
||||
|
||||
/// Number of seconds this payment will add to vm expiry
|
||||
#[serde(skip_serializing)]
|
||||
|
@ -1,4 +1,7 @@
|
||||
use crate::{IpRange, LNVpsDb, User, UserSshKey, Vm, VmCostPlan, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||
use crate::{
|
||||
IpRange, LNVpsDb, User, UserSshKey, Vm, VmCostPlan, VmHost, VmHostDisk, VmHostRegion,
|
||||
VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||
};
|
||||
use anyhow::{bail, Error, Result};
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{Executor, MySqlPool, Row};
|
||||
@ -11,9 +14,7 @@ pub struct LNVpsDbMysql {
|
||||
impl LNVpsDbMysql {
|
||||
pub async fn new(conn: &str) -> Result<Self> {
|
||||
let db = MySqlPool::connect(conn).await?;
|
||||
Ok(Self {
|
||||
db
|
||||
})
|
||||
Ok(Self { db })
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@ -62,9 +63,11 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
async fn insert_user_ssh_key(&self, new_key: &UserSshKey) -> Result<u64> {
|
||||
Ok(sqlx::query("insert into user_ssh_key(name,user_id,key_data) values(?, ?, ?) returning id")
|
||||
Ok(sqlx::query(
|
||||
"insert into user_ssh_key(name,user_id,key_data) values(?, ?, ?) returning id",
|
||||
)
|
||||
.bind(&new_key.name)
|
||||
.bind(&new_key.user_id)
|
||||
.bind(new_key.user_id)
|
||||
.bind(&new_key.key_data)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
@ -109,7 +112,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn get_host(&self, id: u64) -> Result<VmHost> {
|
||||
sqlx::query_as("select * from vm_host where id = ?")
|
||||
.bind(&id)
|
||||
.bind(id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -118,9 +121,9 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
async fn update_host(&self, host: &VmHost) -> Result<()> {
|
||||
sqlx::query("update vm_host set name = ?, cpu = ?, memory = ? where id = ?")
|
||||
.bind(&host.name)
|
||||
.bind(&host.cpu)
|
||||
.bind(&host.memory)
|
||||
.bind(&host.id)
|
||||
.bind(host.cpu)
|
||||
.bind(host.memory)
|
||||
.bind(host.id)
|
||||
.execute(&self.db)
|
||||
.await?;
|
||||
Ok(())
|
||||
@ -128,7 +131,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn list_host_disks(&self, host_id: u64) -> Result<Vec<VmHostDisk>> {
|
||||
sqlx::query_as("select * from vm_host_disk where host_id = ?")
|
||||
.bind(&host_id)
|
||||
.bind(host_id)
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -188,7 +191,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn list_user_vms(&self, id: u64) -> Result<Vec<Vm>> {
|
||||
sqlx::query_as("select * from vm where user_id = ?")
|
||||
.bind(&id)
|
||||
.bind(id)
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -196,7 +199,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn get_vm(&self, vm_id: u64) -> Result<Vm> {
|
||||
sqlx::query_as("select * from vm where id = ?")
|
||||
.bind(&vm_id)
|
||||
.bind(vm_id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -204,17 +207,17 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn insert_vm(&self, vm: &Vm) -> Result<u64> {
|
||||
Ok(sqlx::query("insert into vm(host_id,user_id,image_id,template_id,ssh_key_id,created,expires,cpu,memory,disk_size,disk_id) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id")
|
||||
.bind(&vm.host_id)
|
||||
.bind(&vm.user_id)
|
||||
.bind(&vm.image_id)
|
||||
.bind(&vm.template_id)
|
||||
.bind(&vm.ssh_key_id)
|
||||
.bind(&vm.created)
|
||||
.bind(&vm.expires)
|
||||
.bind(&vm.cpu)
|
||||
.bind(&vm.memory)
|
||||
.bind(&vm.disk_size)
|
||||
.bind(&vm.disk_id)
|
||||
.bind(vm.host_id)
|
||||
.bind(vm.user_id)
|
||||
.bind(vm.image_id)
|
||||
.bind(vm.template_id)
|
||||
.bind(vm.ssh_key_id)
|
||||
.bind(vm.created)
|
||||
.bind(vm.expires)
|
||||
.bind(vm.cpu)
|
||||
.bind(vm.memory)
|
||||
.bind(vm.disk_size)
|
||||
.bind(vm.disk_id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)?
|
||||
@ -222,9 +225,11 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
async fn insert_vm_ip_assignment(&self, ip_assignment: &VmIpAssignment) -> Result<u64> {
|
||||
Ok(sqlx::query("insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id")
|
||||
.bind(&ip_assignment.vm_id)
|
||||
.bind(&ip_assignment.ip_range_id)
|
||||
Ok(sqlx::query(
|
||||
"insert into vm_ip_assignment(vm_id,ip_range_id,ip) values(?, ?, ?) returning id",
|
||||
)
|
||||
.bind(ip_assignment.vm_id)
|
||||
.bind(ip_assignment.ip_range_id)
|
||||
.bind(&ip_assignment.ip)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
@ -257,15 +262,16 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
async fn insert_vm_payment(&self, vm_payment: &VmPayment) -> Result<()> {
|
||||
sqlx::query("insert into vm_payment(id,vm_id,created,expires,amount,invoice,time_value,is_paid) values(?,?,?,?,?,?,?,?)")
|
||||
sqlx::query("insert into vm_payment(id,vm_id,created,expires,amount,invoice,time_value,is_paid,rate) values(?,?,?,?,?,?,?,?,?)")
|
||||
.bind(&vm_payment.id)
|
||||
.bind(&vm_payment.vm_id)
|
||||
.bind(&vm_payment.created)
|
||||
.bind(&vm_payment.expires)
|
||||
.bind(&vm_payment.amount)
|
||||
.bind(vm_payment.vm_id)
|
||||
.bind(vm_payment.created)
|
||||
.bind(vm_payment.expires)
|
||||
.bind(vm_payment.amount)
|
||||
.bind(&vm_payment.invoice)
|
||||
.bind(&vm_payment.time_value)
|
||||
.bind(&vm_payment.is_paid)
|
||||
.bind(vm_payment.time_value)
|
||||
.bind(vm_payment.is_paid)
|
||||
.bind(vm_payment.rate)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)?;
|
||||
@ -274,7 +280,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn get_vm_payment(&self, id: &Vec<u8>) -> Result<VmPayment> {
|
||||
sqlx::query_as("select * from vm_payment where id=?")
|
||||
.bind(&id)
|
||||
.bind(id)
|
||||
.fetch_one(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
@ -282,7 +288,7 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
|
||||
async fn update_vm_payment(&self, vm_payment: &VmPayment) -> Result<()> {
|
||||
sqlx::query("update vm_payment set is_paid = ? where id = ?")
|
||||
.bind(&vm_payment.is_paid)
|
||||
.bind(vm_payment.is_paid)
|
||||
.bind(&vm_payment.id)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
@ -298,14 +304,14 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
let mut tx = self.db.begin().await?;
|
||||
|
||||
sqlx::query("update vm_payment set is_paid = true, settle_index = ? where id = ?")
|
||||
.bind(&vm_payment.settle_index)
|
||||
.bind(vm_payment.settle_index)
|
||||
.bind(&vm_payment.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query("update vm set expires = TIMESTAMPADD(SECOND, ?, expires) where id = ?")
|
||||
.bind(&vm_payment.time_value)
|
||||
.bind(&vm_payment.vm_id)
|
||||
.bind(vm_payment.time_value)
|
||||
.bind(vm_payment.vm_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@ -314,7 +320,9 @@ impl LNVpsDb for LNVpsDbMysql {
|
||||
}
|
||||
|
||||
async fn last_paid_invoice(&self) -> Result<Option<VmPayment>> {
|
||||
sqlx::query_as("select * from vm_payment where is_paid = true order by settle_index desc limit 1")
|
||||
sqlx::query_as(
|
||||
"select * from vm_payment where is_paid = true order by settle_index desc limit 1",
|
||||
)
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.map_err(Error::new)
|
||||
|
@ -3,6 +3,7 @@ use config::{Config, File};
|
||||
use fedimint_tonic_lnd::connect;
|
||||
use lnvps::api;
|
||||
use lnvps::cors::CORS;
|
||||
use lnvps::exchange::ExchangeRateCache;
|
||||
use lnvps::invoice::InvoiceHandler;
|
||||
use lnvps::provisioner::lnvps::LNVpsProvisioner;
|
||||
use lnvps::provisioner::Provisioner;
|
||||
@ -42,8 +43,10 @@ async fn main() -> Result<(), Error> {
|
||||
let db = LNVpsDbMysql::new(&settings.db).await?;
|
||||
db.migrate().await?;
|
||||
|
||||
|
||||
let exchange = ExchangeRateCache::new();
|
||||
let lnd = connect(settings.lnd.url, settings.lnd.cert, settings.lnd.macaroon).await?;
|
||||
let provisioner = LNVpsProvisioner::new(db.clone(), lnd.clone());
|
||||
let provisioner = LNVpsProvisioner::new(db.clone(), lnd.clone(), exchange.clone());
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let setup_script = include_str!("../../dev_setup.sql");
|
||||
@ -52,7 +55,7 @@ async fn main() -> Result<(), Error> {
|
||||
}
|
||||
|
||||
let status = VmStateCache::new();
|
||||
let mut worker = Worker::new(settings.read_only, db.clone(), lnd.clone(), status.clone());
|
||||
let mut worker = Worker::new(settings.read_only, db.clone(), lnd.clone(), status.clone(), exchange.clone());
|
||||
let sender = worker.sender();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@ -84,6 +87,21 @@ async fn main() -> Result<(), Error> {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
}
|
||||
});
|
||||
// refresh rates every 1min
|
||||
let rates = exchange.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rates.fetch_rates().await {
|
||||
Ok(z) => {
|
||||
for r in z {
|
||||
rates.set_rate(r.0, r.1).await;
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Failed to fetch rates: {}", e)
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
|
||||
let db: Box<dyn LNVpsDb> = Box::new(db.clone());
|
||||
let pv: Box<dyn Provisioner> = Box::new(provisioner);
|
||||
@ -101,6 +119,7 @@ async fn main() -> Result<(), Error> {
|
||||
.manage(db)
|
||||
.manage(pv)
|
||||
.manage(status)
|
||||
.manage(exchange)
|
||||
.mount("/", api::routes())
|
||||
.launch()
|
||||
.await
|
||||
|
105
src/exchange.rs
Normal file
105
src/exchange.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use anyhow::{Error, Result};
|
||||
use log::info;
|
||||
use rocket::serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{write, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Currency {
|
||||
EUR,
|
||||
BTC,
|
||||
USD,
|
||||
}
|
||||
|
||||
impl Display for Currency {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Currency::EUR => write!(f, "EUR"),
|
||||
Currency::BTC => write!(f, "BTC"),
|
||||
Currency::USD => write!(f, "USD"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Currency {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"eur" => Ok(Currency::EUR),
|
||||
"usd" => Ok(Currency::USD),
|
||||
"btc" => Ok(Currency::BTC),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Ticker(Currency, Currency);
|
||||
|
||||
impl Ticker {
|
||||
pub fn btc_rate(cur: &str) -> Result<Self> {
|
||||
let to_cur: Currency = cur.parse().map_err(|_| Error::msg(""))?;
|
||||
Ok(Ticker(Currency::BTC, to_cur))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Ticker {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}/{}", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct TickerRate(pub Ticker, pub f32);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExchangeRateCache {
|
||||
cache: Arc<RwLock<HashMap<Ticker, f32>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MempoolRates {
|
||||
pub time: u64,
|
||||
#[serde(rename = "USD")]
|
||||
pub usd: Option<f32>,
|
||||
#[serde(rename = "EUR")]
|
||||
pub eur: Option<f32>,
|
||||
}
|
||||
|
||||
impl ExchangeRateCache {
|
||||
pub fn new() -> Self {
|
||||
Self { cache: Arc::new(RwLock::new(HashMap::new())) }
|
||||
}
|
||||
|
||||
pub async fn fetch_rates(&self) -> Result<Vec<TickerRate>> {
|
||||
let rsp = reqwest::get("https://mempool.space/api/v1/prices")
|
||||
.await?
|
||||
.text().await?;
|
||||
let rates: MempoolRates = serde_json::from_str(&rsp)?;
|
||||
|
||||
let mut ret = vec![];
|
||||
if let Some(usd) = rates.usd {
|
||||
ret.push(TickerRate(Ticker(Currency::BTC, Currency::USD), usd));
|
||||
}
|
||||
if let Some(eur) = rates.eur {
|
||||
ret.push(TickerRate(Ticker(Currency::BTC, Currency::EUR), eur));
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn set_rate(&self, ticker: Ticker, amount: f32) {
|
||||
let mut cache = self.cache.write().await;
|
||||
info!("{}: {}", &ticker, amount);
|
||||
cache.insert(ticker, amount);
|
||||
}
|
||||
|
||||
pub async fn get_rate(&self, ticker: Ticker) -> Option<f32> {
|
||||
let cache = self.cache.read().await;
|
||||
cache.get(&ticker).cloned()
|
||||
}
|
||||
}
|
@ -6,3 +6,4 @@ pub mod nip98;
|
||||
pub mod provisioner;
|
||||
pub mod status;
|
||||
pub mod worker;
|
||||
pub mod exchange;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::exchange::{Currency, ExchangeRateCache, Ticker};
|
||||
use crate::host::proxmox::ProxmoxClient;
|
||||
use crate::provisioner::Provisioner;
|
||||
use anyhow::{bail, Result};
|
||||
@ -21,13 +22,15 @@ use std::time::Duration;
|
||||
pub struct LNVpsProvisioner {
|
||||
db: Box<dyn LNVpsDb>,
|
||||
lnd: Client,
|
||||
rates: ExchangeRateCache,
|
||||
}
|
||||
|
||||
impl LNVpsProvisioner {
|
||||
pub fn new<D: LNVpsDb + 'static>(db: D, lnd: Client) -> Self {
|
||||
pub fn new<D: LNVpsDb + 'static>(db: D, lnd: Client, rates: ExchangeRateCache) -> Self {
|
||||
Self {
|
||||
db: Box::new(db),
|
||||
lnd,
|
||||
rates,
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,22 +158,25 @@ impl Provisioner for LNVpsProvisioner {
|
||||
.add(Months::new((12 * cost_plan.interval_amount) as u32)),
|
||||
};
|
||||
|
||||
const BTC_MILLI_SATS: u64 = 100_000_000_000;
|
||||
const BTC_SATS: f64 = 100_000_000.0;
|
||||
const INVOICE_EXPIRE: i64 = 3600;
|
||||
|
||||
let cost = cost_plan.amount
|
||||
* match cost_plan.currency.as_str() {
|
||||
"EUR" => 1_100_000, //TODO: rates
|
||||
"BTC" => 1, // BTC amounts are always millisats
|
||||
c => bail!("Unknown currency {c}"),
|
||||
let ticker = Ticker::btc_rate(cost_plan.currency.as_str())?;
|
||||
let rate = if let Some(r) = self.rates.get_rate(ticker).await {
|
||||
r
|
||||
} else {
|
||||
bail!("No exchange rate found")
|
||||
};
|
||||
info!("Creating invoice for {vm_id} for {cost} mSats");
|
||||
|
||||
let cost_btc = cost_plan.amount as f32 / rate;
|
||||
let cost_msat = (cost_btc as f64 * BTC_SATS) as i64 * 1000;
|
||||
info!("Creating invoice for {vm_id} for {} sats", cost_msat / 1000);
|
||||
let mut lnd = self.lnd.clone();
|
||||
let invoice = lnd
|
||||
.lightning()
|
||||
.add_invoice(Invoice {
|
||||
memo: format!("VM renewal {vm_id} to {new_expire}"),
|
||||
value_msat: cost as i64,
|
||||
value_msat: cost_msat,
|
||||
expiry: INVOICE_EXPIRE,
|
||||
..Default::default()
|
||||
})
|
||||
@ -182,10 +188,11 @@ impl Provisioner for LNVpsProvisioner {
|
||||
vm_id,
|
||||
created: Utc::now(),
|
||||
expires: Utc::now().add(Duration::from_secs(INVOICE_EXPIRE as u64)),
|
||||
amount: cost,
|
||||
amount: cost_msat as u64,
|
||||
invoice: invoice.payment_request.clone(),
|
||||
time_value: (new_expire - vm.expires).num_seconds() as u64,
|
||||
is_paid: false,
|
||||
rate,
|
||||
..Default::default()
|
||||
};
|
||||
self.db.insert_vm_payment(&vm_payment).await?;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::exchange::ExchangeRateCache;
|
||||
use crate::host::proxmox::{CreateVm, ProxmoxClient, VmBios, VmStatus};
|
||||
use crate::provisioner::lnvps::LNVpsProvisioner;
|
||||
use crate::provisioner::Provisioner;
|
||||
@ -35,9 +36,10 @@ impl Worker {
|
||||
db: D,
|
||||
lnd: Client,
|
||||
vm_state_cache: VmStateCache,
|
||||
rates: ExchangeRateCache,
|
||||
) -> Self {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let p = LNVpsProvisioner::new(db.clone(), lnd.clone());
|
||||
let p = LNVpsProvisioner::new(db.clone(), lnd.clone(), rates);
|
||||
Self {
|
||||
read_only,
|
||||
db: Box::new(db),
|
||||
|
Loading…
x
Reference in New Issue
Block a user