Fix compile errors
This commit is contained in:
+2
-2
@@ -5,7 +5,7 @@ use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, Method, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{delete, get, post, put},
|
||||
routing::{get, put},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -325,7 +325,7 @@ async fn list_tenant_invoices(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct UpdateTenantBillingRequest {
|
||||
tenant_nwc_url: String,
|
||||
}
|
||||
|
||||
+62
-4
@@ -1,15 +1,73 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use std::str::FromStr;
|
||||
|
||||
use nostr_sdk::nostr::key::PublicKey;
|
||||
use nostr_sdk::nostr::nips::nip98::{self, HttpMethod};
|
||||
use nostr_sdk::nostr::nips::nip98::HttpMethod;
|
||||
use nostr_sdk::nostr::types::time::Timestamp;
|
||||
use nostr_sdk::nostr::types::url::Url;
|
||||
use nostr_sdk::nostr::{Alphabet, Event, Kind, SingleLetterTag, TagKind, TagStandard};
|
||||
use nostr_sdk::JsonUtil;
|
||||
|
||||
pub fn verify_nip98(auth_header: &str, url: &str, method: &str) -> Result<PublicKey> {
|
||||
let url = Url::parse(url).map_err(|e| anyhow!(e))?;
|
||||
let method = HttpMethod::from_str(&method.to_uppercase()).map_err(|e| anyhow!(e))?;
|
||||
let url = Url::parse(url)?;
|
||||
let method = HttpMethod::from_str(&method.to_uppercase())?;
|
||||
let now = Timestamp::now();
|
||||
|
||||
nip98::verify_auth_header(auth_header, &url, method, now, None).map_err(|e| anyhow!(e))
|
||||
let event = decode_auth_event(auth_header)?;
|
||||
|
||||
if event.kind != Kind::HttpAuth {
|
||||
return Err(anyhow!("authorization event kind mismatch"));
|
||||
}
|
||||
|
||||
let authorized_url =
|
||||
match event
|
||||
.tags
|
||||
.find_standardized(TagKind::SingleLetter(SingleLetterTag::lowercase(
|
||||
Alphabet::U,
|
||||
))) {
|
||||
Some(TagStandard::AbsoluteURL(url)) => url,
|
||||
_ => return Err(anyhow!("authorization header missing url tag")),
|
||||
};
|
||||
|
||||
let authorized_method = match event.tags.find_standardized(TagKind::Method) {
|
||||
Some(TagStandard::Method(method)) => method,
|
||||
_ => return Err(anyhow!("authorization header missing method tag")),
|
||||
};
|
||||
|
||||
if authorized_url != &url || authorized_method != &method {
|
||||
return Err(anyhow!("authorization does not match request"));
|
||||
}
|
||||
|
||||
let created_at = event.created_at.as_u64();
|
||||
let now_secs = now.as_u64();
|
||||
if created_at > now_secs {
|
||||
if created_at - now_secs > 30 {
|
||||
return Err(anyhow!("authorization timestamp too far in future"));
|
||||
}
|
||||
} else if now_secs - created_at > 60 {
|
||||
return Err(anyhow!("authorization timestamp too old"));
|
||||
}
|
||||
|
||||
event.verify()?;
|
||||
Ok(event.pubkey)
|
||||
}
|
||||
|
||||
fn decode_auth_event(auth_header: &str) -> Result<Event> {
|
||||
if auth_header.trim().is_empty() {
|
||||
return Err(anyhow!("missing authorization header"));
|
||||
}
|
||||
|
||||
let (prefix, encoded) = auth_header
|
||||
.split_once(' ')
|
||||
.ok_or_else(|| anyhow!("malformed authorization header"))?;
|
||||
|
||||
if prefix != "Nostr" || encoded.is_empty() {
|
||||
return Err(anyhow!("malformed authorization header"));
|
||||
}
|
||||
|
||||
let decoded = general_purpose::STANDARD.decode(encoded)?;
|
||||
let json = String::from_utf8(decoded)?;
|
||||
Ok(Event::from_json(json)?)
|
||||
}
|
||||
|
||||
+42
-8
@@ -7,7 +7,8 @@ use crate::models::{Invoice, NewInvoice, NewInvoiceItem, Relay, Tenant};
|
||||
use crate::notifications::Nip17Notifier;
|
||||
use crate::repo::Repo;
|
||||
|
||||
use nostr_sdk::nwc::prelude::*;
|
||||
use nostr_sdk::nips::nip47::{self, MakeInvoiceRequest, NostrWalletConnectURI, PayInvoiceRequest};
|
||||
use nostr_sdk::{Client, Filter, Kind, Keys, Timestamp};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BillingService {
|
||||
@@ -140,20 +141,53 @@ impl BillingService {
|
||||
}
|
||||
|
||||
let uri = NostrWalletConnectURI::parse(&self.platform_nwc_url)?;
|
||||
let nwc = NWC::new(uri);
|
||||
let request = MakeInvoiceRequest::new(amount as u64, "Relay hosting");
|
||||
let response = nwc.make_invoice(request).await?;
|
||||
Ok(response.invoice)
|
||||
let request = nip47::Request::make_invoice(MakeInvoiceRequest {
|
||||
amount: (amount as u64) * 1_000,
|
||||
description: Some("Relay hosting".to_string()),
|
||||
description_hash: None,
|
||||
expiry: None,
|
||||
});
|
||||
let response = self.send_nwc_request(&uri, request).await?;
|
||||
Ok(response.to_make_invoice()?.invoice)
|
||||
}
|
||||
|
||||
async fn pay_invoice(&self, tenant_nwc_url: &str, invoice: &str) -> Result<()> {
|
||||
let uri = NostrWalletConnectURI::parse(tenant_nwc_url)?;
|
||||
let nwc = NWC::new(uri);
|
||||
let request = PayInvoiceRequest::new(invoice);
|
||||
nwc.pay_invoice(request).await?;
|
||||
let request = nip47::Request::pay_invoice(PayInvoiceRequest::new(invoice));
|
||||
self.send_nwc_request(&uri, request).await?.to_pay_invoice()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_nwc_request(
|
||||
&self,
|
||||
uri: &NostrWalletConnectURI,
|
||||
request: nip47::Request,
|
||||
) -> Result<nip47::Response> {
|
||||
let app_keys = Keys::new(uri.secret.clone());
|
||||
let app_pubkey = app_keys.public_key();
|
||||
let client = Client::new(app_keys);
|
||||
client.add_relay(uri.relay_url.clone()).await?;
|
||||
client.connect().await;
|
||||
|
||||
let started_at = Timestamp::now();
|
||||
let event = request.to_event(uri)?;
|
||||
client.send_event(event).await?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::WalletConnectResponse)
|
||||
.author(uri.public_key)
|
||||
.pubkey(app_pubkey)
|
||||
.since(started_at);
|
||||
|
||||
let events = client.fetch_events(filter, Duration::from_secs(10)).await?;
|
||||
let event = events
|
||||
.into_iter()
|
||||
.max_by_key(|event| event.created_at)
|
||||
.ok_or_else(|| anyhow!("no NWC response"))?;
|
||||
|
||||
Ok(nip47::Response::from_event(uri, &event)?)
|
||||
}
|
||||
|
||||
async fn send_invoice_dm(
|
||||
&self,
|
||||
tenant: &Tenant,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
@@ -21,6 +22,7 @@ impl Config {
|
||||
pub fn from_env() -> Self {
|
||||
let database_url =
|
||||
env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://data/caravel.db".to_string());
|
||||
let database_url = resolve_database_url(database_url);
|
||||
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let port = env::var("PORT")
|
||||
.ok()
|
||||
@@ -71,3 +73,20 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_database_url(database_url: String) -> String {
|
||||
const PREFIX: &str = "sqlite://";
|
||||
if !database_url.starts_with(PREFIX) {
|
||||
return database_url;
|
||||
}
|
||||
|
||||
let path = &database_url[PREFIX.len()..];
|
||||
if path.is_empty() || path.starts_with('/') || path == ":memory:" {
|
||||
return database_url;
|
||||
}
|
||||
|
||||
let base = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let absolute = base.join(path);
|
||||
let normalized = absolute.to_string_lossy();
|
||||
format!("sqlite:///{}", normalized.trim_start_matches('/'))
|
||||
}
|
||||
|
||||
+36
-3
@@ -1,15 +1,48 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::{migrate::Migrator, sqlite::SqlitePoolOptions, SqlitePool};
|
||||
use anyhow::{anyhow, Result};
|
||||
use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, sqlite::SqlitePoolOptions, SqlitePool};
|
||||
use std::str::FromStr;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
|
||||
pub async fn init_pool(database_url: &str) -> Result<SqlitePool> {
|
||||
ensure_sqlite_directory(database_url)?;
|
||||
let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(database_url)
|
||||
.connect_with(options)
|
||||
.await?;
|
||||
|
||||
sqlx::query("PRAGMA journal_mode = WAL;")
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
MIGRATOR.run(&pool).await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
fn ensure_sqlite_directory(database_url: &str) -> Result<()> {
|
||||
const PREFIX: &str = "sqlite://";
|
||||
if !database_url.starts_with(PREFIX) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = &database_url[PREFIX.len()..];
|
||||
if path.is_empty() || path == ":memory:" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let path = if let Some(stripped) = path.strip_prefix('/') {
|
||||
format!("/{}", stripped)
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
|
||||
let parent = Path::new(&path)
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("invalid sqlite path"))?;
|
||||
fs::create_dir_all(parent)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Nip17Notifier {
|
||||
return Err(anyhow!("PLATFORM_SECRET is required for NIP-17 notifications"));
|
||||
}
|
||||
|
||||
let keys = Keys::parse(platform_secret)?;
|
||||
let keys = Keys::parse(&platform_secret)?;
|
||||
let indexer_client = Client::new(keys.clone());
|
||||
|
||||
for relay in &relays {
|
||||
@@ -71,15 +71,15 @@ impl Nip17Notifier {
|
||||
let filter = Filter::new().kind(Kind::Custom(10050)).author(pubkey);
|
||||
let events = self
|
||||
.indexer_client
|
||||
.get_events_of(vec![filter], Some(Duration::from_secs(5)))
|
||||
.fetch_events(filter, Duration::from_secs(5))
|
||||
.await?;
|
||||
|
||||
let mut relays = Vec::new();
|
||||
if let Some(event) = events.into_iter().max_by_key(|event| event.created_at) {
|
||||
for tag in event.tags.iter() {
|
||||
if let Some(first) = tag.as_vec().get(0) {
|
||||
if let Some(first) = tag.as_slice().get(0) {
|
||||
if first == "relay" {
|
||||
if let Some(value) = tag.as_vec().get(1) {
|
||||
if let Some(value) = tag.as_slice().get(1) {
|
||||
relays.push(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ impl Provisioner {
|
||||
return Err(anyhow!("PLATFORM_SECRET is required"));
|
||||
}
|
||||
|
||||
let admin_keys = Keys::parse(admin_secret)?;
|
||||
let admin_keys = Keys::parse(&admin_secret)?;
|
||||
let client = Client::new();
|
||||
|
||||
Ok(Self {
|
||||
|
||||
Reference in New Issue
Block a user