use config::ConfigError::Message;
use farcaster_core::blockchain::Network;
use farcaster_core::swap::btcxmr::DealParameters;
use internet2::addr::InetSocketAddr;
use serde::{Deserialize, Serialize};
use serde_with::DisplayFromStr;
use std::convert::TryInto;
use std::fmt::Display;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::str::FromStr;
use crate::{AccordantBlockchain, ArbitratingBlockchain, Error};
pub const FARCASTER_MAINNET_ELECTRUM_SERVER: &str = "ssl://blockstream.info:700";
pub const FARCASTER_MAINNET_MONERO_DAEMON: &str = "http://node.community.rino.io:18081";
pub const FARCASTER_MAINNET_MONERO_RPC_WALLET: &str = "http://localhost:18083";
pub const FARCASTER_TESTNET_ELECTRUM_SERVER: &str = "ssl://blockstream.info:993";
pub const FARCASTER_TESTNET_MONERO_DAEMON: &str = "http://stagenet.community.rino.io:38081";
pub const FARCASTER_TESTNET_MONERO_RPC_WALLET: &str = "http://localhost:38083";
pub const FARCASTER_BIND_PORT: u16 = 7067;
pub const FARCASTER_BIND_IP: &str = "0.0.0.0";
pub const GRPC_BIND_IP_ADDRESS: &str = "127.0.0.1";
pub const SWAP_MAINNET_BITCOIN_SAFETY: u8 = 7;
pub const SWAP_MAINNET_BITCOIN_FINALITY: u8 = 6;
pub const SWAP_MAINNET_BITCOIN_MIN_BTC_AMOUNT: f64 = 0.00001;
pub const SWAP_MAINNET_BITCOIN_MAX_BTC_AMOUNT: f64 = 0.01;
pub const SWAP_MAINNET_MONERO_FINALITY: u8 = 20;
pub const SWAP_MAINNET_MONERO_MIN_XMR_AMOUNT: f64 = 0.001;
pub const SWAP_MAINNET_MONERO_MAX_XMR_AMOUNT: f64 = 2.0;
pub const SWAP_TESTNET_BITCOIN_SAFETY: u8 = 3;
pub const SWAP_TESTNET_BITCOIN_FINALITY: u8 = 1;
pub const SWAP_TESTNET_MONERO_FINALITY: u8 = 1;
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct Config {
pub farcasterd: Option<FarcasterdConfig>,
pub swap: Option<SwapConfig>,
pub grpc: Option<GrpcConfig>,
pub syncers: Option<Networked<Option<SyncerServers>>>,
}
impl Config {
pub fn is_auto_funding_enable(&self) -> bool {
match &self.farcasterd {
Some(FarcasterdConfig {
auto_funding: Some(AutoFundingConfig { enable, .. }),
..
}) => *enable,
_ => false,
}
}
pub fn is_grpc_enable(&self) -> bool {
match &self.grpc {
Some(GrpcConfig { enable, .. }) => *enable,
_ => false,
}
}
pub fn grpc_bind_ip(&self) -> String {
match &self.grpc {
Some(GrpcConfig {
bind_ip: Some(bind_ip),
..
}) => bind_ip.clone(),
_ => String::from(GRPC_BIND_IP_ADDRESS),
}
}
pub fn auto_restore_enable(&self) -> bool {
match &self.farcasterd {
Some(FarcasterdConfig {
auto_restore: Some(enable),
..
}) => *enable,
_ => true,
}
}
pub fn get_auto_funding_config(&self, network: Network) -> Option<AutoFundingServers> {
match &self.farcasterd {
Some(FarcasterdConfig {
auto_funding:
Some(AutoFundingConfig {
enable,
mainnet,
testnet,
local,
}),
..
}) if *enable => match network {
Network::Mainnet => mainnet.clone(),
Network::Testnet => testnet.clone(),
Network::Local => local.clone(),
},
_ => None,
}
}
pub fn get_bind_addr(&self) -> Result<InetSocketAddr, Error> {
let addr = if let Some(FarcasterdConfig {
bind_ip, bind_port, ..
}) = &self.farcasterd
{
format!(
"{}:{}",
bind_ip.as_ref().unwrap_or(&FARCASTER_BIND_IP.to_string()),
bind_port.unwrap_or(FARCASTER_BIND_PORT)
)
} else {
format!("{}:{}", FARCASTER_BIND_IP, FARCASTER_BIND_PORT)
};
Ok(InetSocketAddr::from_str(&addr)?)
}
pub fn get_syncer_servers(&self, network: Network) -> Option<SyncerServers> {
match network {
Network::Mainnet => self.syncers.as_ref()?.mainnet.clone(),
Network::Testnet => self.syncers.as_ref()?.testnet.clone(),
Network::Local => self.syncers.as_ref()?.local.clone(),
}
}
pub fn get_swap_config(
&self,
arb: ArbitratingBlockchain,
acc: AccordantBlockchain,
network: Network,
) -> Result<ParsedSwapConfig, config::ConfigError> {
match &self.swap {
Some(swap) => {
let arbitrating = match arb {
ArbitratingBlockchain::Bitcoin => swap
.bitcoin
.get_for_network(network)
.map(|c| c.temporality)
.or_else(|| ArbConfig::get(arb, network))
.ok_or_else(|| {
Message("No configuration nor defaults founds!".to_string())
})?,
};
let accordant = match acc {
AccordantBlockchain::Monero => swap
.monero
.get_for_network(network)
.map(|c| c.temporality)
.or_else(|| AccConfig::get(acc, network))
.ok_or_else(|| {
Message("No configuration nor defaults founds!".to_string())
})?,
};
Ok(ParsedSwapConfig {
arbitrating,
accordant,
})
}
None => {
let arbitrating = ArbConfig::get(arb, network)
.ok_or_else(|| Message("No defaults founds!".to_string()))?;
let accordant = AccConfig::get(acc, network)
.ok_or_else(|| Message("No defaults founds!".to_string()))?;
Ok(ParsedSwapConfig {
arbitrating,
accordant,
})
}
}
}
pub fn validate_deal_parameters(
&self,
deal: &DealParameters,
arb_addr: &bitcoin::Address,
acc_addr: &monero::Address,
) -> Result<(), Error> {
self.validate_deal_addresses(deal, arb_addr, acc_addr)?;
self.validate_deal_amounts(deal)
}
pub fn validate_deal_amounts(&self, deal: &DealParameters) -> Result<(), Error> {
self.swap
.as_ref()
.and_then(|swap| {
swap.bitcoin
.get_for_network(deal.network)
.map(|net| net.amounts)
})
.or_else(|| Self::btc_default_tradeable(deal.network))
.map(|tradeable| tradeable.validate_amount(deal.arbitrating_amount))
.transpose()?;
self.swap
.as_ref()
.and_then(|swap| {
swap.monero
.get_for_network(deal.network)
.map(|net| net.amounts)
})
.or_else(|| Self::xmr_default_tradeable(deal.network))
.map(|tradeable| tradeable.validate_amount(deal.accordant_amount))
.transpose()?;
Ok(())
}
pub fn validate_deal_addresses(
&self,
deal: &DealParameters,
arb_addr: &bitcoin::Address,
acc_addr: &monero::Address,
) -> Result<(), Error> {
match deal.arbitrating_blockchain.try_into()? {
ArbitratingBlockchain::Bitcoin => {
if deal.network != arb_addr.network.into() {
Err(Message(format!(
"{} address is not a {} address",
deal.arbitrating_blockchain, deal.network
)))
} else {
Ok(())
}
}
}?;
match deal.accordant_blockchain.try_into()? {
AccordantBlockchain::Monero => {
if deal.network != acc_addr.network.into() && deal.network != Network::Local {
Err(Message(format!(
"{} address is not a {} address",
deal.accordant_blockchain, deal.network
)))
} else {
Ok(())
}
}
}?;
Ok(())
}
fn btc_default_tradeable(network: Network) -> Option<TradeableAmounts<bitcoin::Amount>> {
match network {
Network::Mainnet => Some(Self::mainnet_btc_default_tradeable()),
_ => None,
}
}
fn mainnet_btc_default_tradeable() -> TradeableAmounts<bitcoin::Amount> {
use bitcoin::Amount;
TradeableAmounts {
min_amount: Amount::from_btc(SWAP_MAINNET_BITCOIN_MIN_BTC_AMOUNT).ok(),
max_amount: Amount::from_btc(SWAP_MAINNET_BITCOIN_MAX_BTC_AMOUNT).ok(),
}
}
fn xmr_default_tradeable(network: Network) -> Option<TradeableAmounts<monero::Amount>> {
match network {
Network::Mainnet => Some(Self::mainnet_xmr_default_tradeable()),
_ => None,
}
}
fn mainnet_xmr_default_tradeable() -> TradeableAmounts<monero::Amount> {
use monero::Amount;
TradeableAmounts {
min_amount: Amount::from_xmr(SWAP_MAINNET_MONERO_MIN_XMR_AMOUNT).ok(),
max_amount: Amount::from_xmr(SWAP_MAINNET_MONERO_MAX_XMR_AMOUNT).ok(),
}
}
}
impl Default for Config {
fn default() -> Self {
Config {
farcasterd: Some(FarcasterdConfig::default()),
swap: Some(SwapConfig::default()),
grpc: None,
syncers: Some(Networked {
mainnet: Some(SyncerServers {
electrum_server: FARCASTER_MAINNET_ELECTRUM_SERVER.into(),
monero_daemon: FARCASTER_MAINNET_MONERO_DAEMON.into(),
monero_rpc_wallet: FARCASTER_MAINNET_MONERO_RPC_WALLET.into(),
monero_lws: None,
monero_wallet_dir: None,
}),
testnet: Some(SyncerServers {
electrum_server: FARCASTER_TESTNET_ELECTRUM_SERVER.into(),
monero_daemon: FARCASTER_TESTNET_MONERO_DAEMON.into(),
monero_rpc_wallet: FARCASTER_TESTNET_MONERO_RPC_WALLET.into(),
monero_lws: None,
monero_wallet_dir: None,
}),
local: None,
}),
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct FarcasterdConfig {
pub auto_funding: Option<AutoFundingConfig>,
pub bind_port: Option<u16>,
pub bind_ip: Option<String>,
pub auto_restore: Option<bool>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct SwapConfig {
pub bitcoin: Networked<Option<ChainSwapConfig<ArbConfig, bitcoin::Amount>>>,
pub monero: Networked<Option<ChainSwapConfig<AccConfig, monero::Amount>>>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
#[serde(bound(serialize = "T: Serialize, A: Display"))]
#[serde(bound(deserialize = "T: Deserialize<'de>, A: FromStr, A::Err: Display"))]
pub struct ChainSwapConfig<T, A>
where
A: FromStr + Display,
A::Err: Display,
{
#[serde(flatten)]
pub temporality: T,
#[serde(flatten)]
pub amounts: TradeableAmounts<A>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct ParsedSwapConfig {
pub arbitrating: ArbConfig,
pub accordant: AccConfig,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct ArbConfig {
pub safety: u8,
pub finality: u8,
}
impl ArbConfig {
fn get(blockchain: ArbitratingBlockchain, network: Network) -> Option<Self> {
match blockchain {
ArbitratingBlockchain::Bitcoin => match network {
Network::Mainnet => Some(Self::btc_mainnet_default()),
Network::Testnet => Some(Self::btc_testnet_default()),
Network::Local => None,
},
}
}
fn btc_mainnet_default() -> Self {
ArbConfig {
safety: SWAP_MAINNET_BITCOIN_SAFETY,
finality: SWAP_MAINNET_BITCOIN_FINALITY,
}
}
fn btc_testnet_default() -> Self {
ArbConfig {
safety: SWAP_TESTNET_BITCOIN_SAFETY,
finality: SWAP_TESTNET_BITCOIN_FINALITY,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct AccConfig {
pub finality: u8,
}
impl AccConfig {
fn get(blockchain: AccordantBlockchain, network: Network) -> Option<Self> {
match blockchain {
AccordantBlockchain::Monero => match network {
Network::Mainnet => Some(Self::xmr_mainnet_default()),
Network::Testnet => Some(Self::xmr_testnet_default()),
Network::Local => None,
},
}
}
fn xmr_mainnet_default() -> Self {
AccConfig {
finality: SWAP_MAINNET_MONERO_FINALITY,
}
}
fn xmr_testnet_default() -> Self {
AccConfig {
finality: SWAP_TESTNET_MONERO_FINALITY,
}
}
}
impl From<ArbConfig> for AccConfig {
fn from(arb: ArbConfig) -> Self {
Self {
finality: arb.finality,
}
}
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
#[serde(bound(serialize = "T: Display"))]
#[serde(bound(deserialize = "T: FromStr, T::Err: Display"))]
pub struct TradeableAmounts<T>
where
T: FromStr + Display,
T::Err: Display,
{
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default)]
pub min_amount: Option<T>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default)]
pub max_amount: Option<T>,
}
impl<T> TradeableAmounts<T>
where
T: FromStr + Display,
T::Err: Display,
{
pub fn map<U>(self) -> TradeableAmounts<U>
where
T: Into<U>,
U: FromStr + Display,
U::Err: Display,
{
TradeableAmounts {
min_amount: self.min_amount.map(|min| min.into()),
max_amount: self.max_amount.map(|max| max.into()),
}
}
pub fn validate_amount(&self, amount: T) -> Result<(), Error>
where
T: PartialOrd<T> + Copy,
{
if let Some(min) = self.min_amount {
if amount < min {
return Err(Message(format!("{} is smaller than {}", amount, min)))?;
}
}
if let Some(max) = self.max_amount {
if amount > max {
return Err(Message(format!("{} is greater than {}", amount, max)))?;
}
}
Ok(())
}
pub fn none() -> Self {
Self {
min_amount: None,
max_amount: None,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct GrpcConfig {
pub enable: bool,
pub bind_port: u16,
pub bind_ip: Option<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct AutoFundingConfig {
pub enable: bool,
pub mainnet: Option<AutoFundingServers>,
pub testnet: Option<AutoFundingServers>,
pub local: Option<AutoFundingServers>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct AutoFundingServers {
pub bitcoin_rpc: String,
pub bitcoin_cookie_path: Option<String>,
pub bitcoin_rpc_user: Option<String>,
pub bitcoin_rpc_pass: Option<String>,
pub monero_rpc_wallet: String,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct SyncerServers {
pub electrum_server: String,
pub monero_daemon: String,
pub monero_rpc_wallet: String,
pub monero_lws: Option<String>,
pub monero_wallet_dir: Option<String>,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(crate = "serde_crate")]
pub struct Networked<T> {
pub mainnet: T,
pub testnet: T,
pub local: T,
}
impl<T> Networked<T>
where
T: Clone,
{
fn get_for_network(&self, network: Network) -> T {
match network {
Network::Mainnet => self.mainnet.clone(),
Network::Testnet => self.testnet.clone(),
Network::Local => self.local.clone(),
}
}
}
impl Default for SwapConfig {
fn default() -> Self {
SwapConfig {
bitcoin: Networked {
mainnet: Some(ChainSwapConfig {
temporality: ArbConfig::btc_mainnet_default(),
amounts: Config::mainnet_btc_default_tradeable(),
}),
testnet: Some(ChainSwapConfig {
temporality: ArbConfig::btc_testnet_default(),
amounts: TradeableAmounts::none(),
}),
local: None,
},
monero: Networked {
mainnet: Some(ChainSwapConfig {
temporality: AccConfig::xmr_mainnet_default(),
amounts: Config::mainnet_xmr_default_tradeable(),
}),
testnet: Some(ChainSwapConfig {
temporality: AccConfig::xmr_testnet_default(),
amounts: TradeableAmounts::none(),
}),
local: None,
},
}
}
}
impl Default for FarcasterdConfig {
fn default() -> Self {
FarcasterdConfig {
auto_funding: None,
auto_restore: Some(true),
bind_port: Some(FARCASTER_BIND_PORT),
bind_ip: Some(FARCASTER_BIND_IP.to_string()),
}
}
}
pub fn parse_config(path: &str) -> Result<Config, Error> {
if Path::new(path).exists() {
let config_file = path;
info!("Loading config file at: {}", &config_file);
let mut settings = config::Config::default();
settings.merge(config::File::with_name(config_file).required(true))?;
let conf = settings.try_into::<Config>().map_err(Into::into);
trace!("{:#?}", conf);
conf
} else {
info!("No configuration file found, generating default config");
let config = Config::default();
let mut file = File::create(path)?;
file.write_all(toml::to_vec(&config).unwrap().as_ref())?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::parse_config;
#[test]
fn config_example_parse() {
let config = parse_config("./farcasterd.toml").expect("correct config example");
dbg!(config);
}
}