use std::str::FromStr;
use std::time::Duration;
use anyhow::{anyhow, Result};
use nostr::nips;
use nostr::prelude::*;
use serde::{Deserialize, Serialize};
use crate::bot::ons::ons1::Ons1Config;
// use crate::bot::ons::ons2::Ons2Config;
use crate::bot::ons::nip47::Nip47Config;
use crate::bot::ons::ons3::Ons3Config;
use crate::bot::ons::ons4::Ons4Config;
/// Can be specified as env variables with an OAK_ prefix
/// For example: OAK_LND_POLLING_INTERVAL_SECONDS=10 cargo run
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StartupParamsOptional {
/// The directory where all DB files will be stored
/// If not specified, the DB files will be saved in the same folder from which the node is started
pub data_dir: Option<String>,
/// The timeout used when connecting to any HTTP endpoint.
///
/// Note this also applies when calling a LNURL-pay endpoint via tor, so the value should be
/// large enough to accommodate for that.
pub http_request_timeout_seconds: u64,
/// The interval in which LND will be polled for the newest block height
pub lnd_polling_interval_seconds: u64,
pub onion_socks5_host: Option<String>,
pub onion_socks5_port: Option<u16>,
/// The Nostr bot private key
pub nostr_bot_prv_key: Option<SecretKey>,
pub nostr_bot_owner_nip05: Option<String>,
pub nostr_bot_owner_nprofile: Option<String>,
/// The Nostr bot owner pubkey
pub nostr_bot_owner_pub_key: Option<String>,
/// The Nostr websocket endpoint (format wss://host.com)
///
/// Default value. It will be overwritten during pairing.
pub nostr_ws_endpoint: String,
pub ons1: Ons1Config,
// pub ons2: Ons2Config,
pub ons3: Ons3Config,
pub ons4: Ons4Config,
pub nip47: Nip47Config,
pub nostr_pow_provider_automine_p_min: Option<f64>,
pub nostr_pow_provider_automine_t_max_millis: Option<u64>,
pub nostr_pow_provider_automine_hashrate: Option<u64>,
pub nostr_pow_provider_automine_baseline_price: Option<u64>,
/// How long back in time to look for unpaid delivered PoWs
///
/// The amount of unpaid delivered PoWs in this timeframe sets the minimum level of PoW required for new Client Asks.
/// Incoming Asks below this level are ignored.
///
/// This effect decays with times, so after this amount of time, an unpaid delivered PoW doesn't count anymore.
/// After this amount of time of no unpaid delivered PoWs, all new Client Asks will be considered for automining.
pub nostr_pow_provider_dos_watchtower_span_minutes: u32,
pub nostr_pow_client_prvkey: Option<SecretKey>,
pub nostr_pow_provider_prvkey: Option<SecretKey>,
}
impl StartupParamsOptional {
/// Attempts to construct a profile from the available owner identifier, in this order:
///
/// - [`nostr_bot_owner_nprofile`]
/// - [`nostr_bot_owner_nip05`]
/// - [`nostr_bot_owner_pub_key`] and [`nostr_ws_endpoint`]
///
/// A [Profile] will be generated from the first identifier found.
async fn try_get_bot_owner_nprofile(&self) -> Result<Option<Profile>> {
if let Some(nprofile) = &self.nostr_bot_owner_nprofile {
info!("Found owner nprofile");
return Profile::from_bech32(nprofile)
.map_err(|e| anyhow!(e))
.map(Some);
}
if let Some(nip05) = &self.nostr_bot_owner_nip05 {
info!("Found owner NIP-05");
let profile = nips::nip05::get_profile(nip05, None).await?;
return Ok(Some(profile));
}
if let Some(pubkey) = &self.nostr_bot_owner_pub_key {
info!("Found owner pubkey");
return Ok(Some(Profile {
public_key: XOnlyPublicKey::from_str(pubkey)?,
relays: vec![self.nostr_ws_endpoint.clone()],
}));
}
Ok(None)
}
/// Attempts to construct a profile using [Self::try_get_bot_owner_nprofile].
///
/// It then treats the relays as seed relays, which it queries for a NIP-65 Relay Metadata
/// event. If found, the relay list from this event is merged with the seed relay list and
/// an updated profile is returned.
pub async fn try_get_bot_owner_nprofile_with_nip65_relays(&self) -> Result<Option<Profile>> {
match self.try_get_bot_owner_nprofile().await? {
None => Ok(None),
Some(base_profile) => {
// Query seed relays from the initial profile for the NIP-65 Relay List Metadata
let mut nip65_relays = get_nip65_relays_from_profile(&base_profile).await;
let mut merged_relay_list = base_profile.relays;
merged_relay_list.append(&mut nip65_relays);
merged_relay_list.sort();
merged_relay_list.dedup();
info!("Merged relay list: {merged_relay_list:?}");
Ok(Some(Profile {
public_key: base_profile.public_key,
relays: merged_relay_list,
}))
}
}
}
}
/// Query relays in the arg profile (seed relays) for the NIP-65 Relay List Metadata.
async fn get_nip65_relays_from_profile(profile: &Profile) -> Vec<String> {
info!("Querying for NIP-65 relays");
let seed_relays: Vec<(String, Option<_>)> = profile
.relays
.clone()
.into_iter()
.map(|r| (r, None))
.collect();
let mut nip65_relays: Vec<String> = vec![];
let client = nostr_sdk::Client::new(&Keys::generate());
match client.add_relays(seed_relays).await {
Ok(_) => {
client.connect().await;
match client
.get_events_of(
vec![Filter::new()
.authors(vec![profile.public_key])
.kind(Kind::RelayList)],
Some(Duration::from_secs(5)),
)
.await
{
Ok(events) => {
for event in events {
let list = nips::nip65::extract_relay_list(&event);
info!("Found NIP-65 relay list metadata: {list:?}");
let mut list: Vec<String> =
list.into_iter().map(|(url, _rw)| url.to_string()).collect();
nip65_relays.append(&mut list);
}
}
Err(e) => warn!("Failed to lookup NIP-65 relays: {e}"),
};
}
Err(e) => warn!("Failed to add relays relays: {e}"),
};
nip65_relays.sort();
nip65_relays.dedup();
nip65_relays
}
/// Default values for the optional startup params
impl Default for StartupParamsOptional {
fn default() -> StartupParamsOptional {
StartupParamsOptional {
// Defaults to the current directory (path from which the executable is started)
data_dir: None,
http_request_timeout_seconds: 60,
lnd_polling_interval_seconds: 60,
onion_socks5_port: None,
onion_socks5_host: None,
nostr_ws_endpoint: "wss://nostr-relay.wlvs.space".to_string(),
nostr_bot_prv_key: None,
nostr_bot_owner_nip05: None,
nostr_bot_owner_nprofile: None,
nostr_bot_owner_pub_key: None,
ons1: Ons1Config::default(),
// ons2: Ons2Config::default(),
ons3: Ons3Config::default(),
ons4: Ons4Config::default(),
nip47: Nip47Config::default(),
nostr_pow_provider_automine_p_min: None,
nostr_pow_provider_automine_t_max_millis: None,
nostr_pow_provider_automine_hashrate: None,
nostr_pow_provider_automine_baseline_price: None,
nostr_pow_provider_dos_watchtower_span_minutes: 60,
nostr_pow_client_prvkey: None,
nostr_pow_provider_prvkey: None,
}
}
}
/// Can be specified as env variables with an OAK_ prefix
#[derive(Debug, Deserialize, Serialize)]
pub struct StartupParamsRequired {
/// REST API endpoint for LND
pub lnd_rest_api_url: String,
/// Full path to LND macaroon file
pub lnd_macaroon_path: String,
/// Full path to LND certificate file
pub lnd_cert_path: String,
}