use anyhow::{Context, Result}; use bdk::bitcoin::secp256k1::schnorrsig; use bdk::bitcoin::Amount; use bdk::{bitcoin, FeeRate}; use clap::{Parser, Subcommand}; use daemon::auth::{self, MAKER_USERNAME}; use daemon::db::load_all_cfds; use daemon::model::WalletInfo; use daemon::seed::Seed; use daemon::tokio_ext::FutureExt; use daemon::{ bitmex_price_feed, db, housekeeping, logger, maker_inc_connections, monitor, oracle, projection, wallet, wallet_sync, MakerActorSystem, Tasks, HEARTBEAT_INTERVAL, N_PAYOUTS, SETTLEMENT_INTERVAL, }; use sqlx::sqlite::SqliteConnectOptions; use sqlx::SqlitePool; use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; use std::task::Poll; use tokio::sync::watch; use tracing_subscriber::filter::LevelFilter; use xtra::Actor; mod routes_maker; #[derive(Parser)] struct Opts { /// The port to listen on for p2p connections. #[clap(long, default_value = "9999")] p2p_port: u16, /// The IP address to listen on for the HTTP API. #[clap(long, default_value = "127.0.0.1:8001")] http_address: SocketAddr, /// Where to permanently store data, defaults to the current working directory. #[clap(long)] data_dir: Option, /// If enabled logs will be in json format #[clap(short, long)] json: bool, /// Configure the log level, e.g.: one of Error, Warn, Info, Debug, Trace #[clap(short, long, default_value = "Debug")] log_level: LevelFilter, #[clap(subcommand)] network: Network, } #[derive(Parser)] enum Network { /// Run on mainnet. Mainnet { /// URL to the electrum backend to use for the wallet. #[clap(long, default_value = "ssl://electrum.blockstream.info:50002")] electrum: String, #[clap(subcommand)] withdraw: Option, }, /// Run on testnet. Testnet { /// URL to the electrum backend to use for the wallet. #[clap(long, default_value = "ssl://electrum.blockstream.info:60002")] electrum: String, #[clap(subcommand)] withdraw: Option, }, /// Run on signet Signet { /// URL to the electrum backend to use for the wallet. #[clap(long)] electrum: String, #[clap(subcommand)] withdraw: Option, }, } #[derive(Subcommand)] enum Withdraw { Withdraw { /// Optionally specify the amount of Bitcoin to be withdrawn. If not specified the wallet /// will be drained. Amount is to be specified with denomination, e.g. "0.1 BTC" #[clap(long)] amount: Option, /// Optionally specify the fee-rate for the transaction. The fee-rate is specified as sats /// per vbyte, e.g. 5.0 #[clap(long)] fee: Option, /// The address to receive the Bitcoin. #[clap(long)] address: bdk::bitcoin::Address, }, } impl Network { fn electrum(&self) -> &str { match self { Network::Mainnet { electrum, .. } => electrum, Network::Testnet { electrum, .. } => electrum, Network::Signet { electrum, .. } => electrum, } } fn bitcoin_network(&self) -> bitcoin::Network { match self { Network::Mainnet { .. } => bitcoin::Network::Bitcoin, Network::Testnet { .. } => bitcoin::Network::Testnet, Network::Signet { .. } => bitcoin::Network::Signet, } } fn data_dir(&self, base: PathBuf) -> PathBuf { match self { Network::Mainnet { .. } => base.join("mainnet"), Network::Testnet { .. } => base.join("testnet"), Network::Signet { .. } => base.join("signet"), } } fn withdraw(&self) -> &Option { match self { Network::Mainnet { withdraw, .. } => withdraw, Network::Testnet { withdraw, .. } => withdraw, Network::Signet { withdraw, .. } => withdraw, } } } #[rocket::main] async fn main() -> Result<()> { let opts = Opts::parse(); logger::init(opts.log_level, opts.json).context("initialize logger")?; tracing::info!("Running version: {}", env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT")); let data_dir = opts .data_dir .clone() .unwrap_or_else(|| std::env::current_dir().expect("unable to get cwd")); let data_dir = opts.network.data_dir(data_dir); if !data_dir.exists() { tokio::fs::create_dir_all(&data_dir).await?; } let seed = Seed::initialize(&data_dir.join("maker_seed")).await?; let bitcoin_network = opts.network.bitcoin_network(); let ext_priv_key = seed.derive_extended_priv_key(bitcoin_network)?; let (wallet, wallet_fut) = wallet::Actor::new( opts.network.electrum(), &data_dir.join("maker_wallet.sqlite"), ext_priv_key, ) .await? .create(None) .run(); let _wallet_handle = wallet_fut.spawn_with_handle(); // do this before withdraw to ensure the wallet is synced let wallet_info = wallet.send(wallet::Sync).await??; if let Some(Withdraw::Withdraw { amount, address, fee, }) = opts.network.withdraw() { wallet .send(wallet::Withdraw { amount: *amount, address: address.clone(), fee: fee.map(FeeRate::from_sat_per_vb), }) .await??; return Ok(()); } let auth_password = seed.derive_auth_password::(); let (identity_pk, identity_sk) = seed.derive_identity(); tracing::info!( "Authentication details: username='{}' password='{}', noise_public_key='{}'", MAKER_USERNAME, auth_password, hex::encode(identity_pk.to_bytes()) ); // TODO: Actually fetch it from Olivia let oracle = schnorrsig::PublicKey::from_str( "ddd4636845a90185991826be5a494cde9f4a6947b1727217afedc6292fa4caf7", )?; let (wallet_feed_sender, wallet_feed_receiver) = watch::channel::(wallet_info); let figment = rocket::Config::figment() .merge(("address", opts.http_address.ip())) .merge(("port", opts.http_address.port())); let p2p_socket = format!("0.0.0.0:{}", opts.p2p_port) .parse::() .unwrap(); let listener = tokio::net::TcpListener::bind(p2p_socket) .await .with_context(|| format!("Failed to listen on {}", p2p_socket))?; let local_addr = listener.local_addr().unwrap(); tracing::info!("Listening on {}", local_addr); let mut tasks = Tasks::default(); let db = SqlitePool::connect_with( SqliteConnectOptions::new() .create_if_missing(true) .filename(data_dir.join("maker.sqlite")), ) .await?; db::run_migrations(&db) .await .context("Db migrations failed")?; // Create actors housekeeping::new(&db, &wallet).await?; let (projection_actor, projection_context) = xtra::Context::new(None); let MakerActorSystem { cfd_actor_addr, inc_conn_addr: incoming_connection_addr, tasks: _tasks, } = MakerActorSystem::new( db.clone(), wallet.clone(), oracle, |cfds, channel| oracle::Actor::new(cfds, channel, SETTLEMENT_INTERVAL), { |channel, cfds| { let electrum = opts.network.electrum().to_string(); monitor::Actor::new(electrum, channel, cfds) } }, |channel0, channel1, channel2| { maker_inc_connections::Actor::new( channel0, channel1, channel2, identity_sk, HEARTBEAT_INTERVAL, ) }, SETTLEMENT_INTERVAL, N_PAYOUTS, projection_actor.clone(), ) .await?; let (task, init_quote) = bitmex_price_feed::new(projection_actor).await?; tasks.add(task); let cfds = { let mut conn = db.acquire().await?; load_all_cfds(&mut conn).await? }; let (proj_actor, projection_feeds) = projection::Actor::new(cfds, init_quote); tasks.add(projection_context.run(proj_actor)); let listener_stream = futures::stream::poll_fn(move |ctx| { let message = match futures::ready!(listener.poll_accept(ctx)) { Ok((stream, address)) => { maker_inc_connections::ListenerMessage::NewConnection { stream, address } } Err(e) => maker_inc_connections::ListenerMessage::Error { source: e }, }; Poll::Ready(Some(message)) }); tasks.add(incoming_connection_addr.attach_stream(listener_stream)); tasks.add(wallet_sync::new(wallet.clone(), wallet_feed_sender)); rocket::custom(figment) .manage(projection_feeds) .manage(cfd_actor_addr) .manage(wallet_feed_receiver) .manage(auth_password) .manage(bitcoin_network) .manage(wallet) .mount( "/api", rocket::routes![ routes_maker::maker_feed, routes_maker::post_sell_order, routes_maker::post_cfd_action, routes_maker::get_health_check, routes_maker::post_withdraw_request ], ) .register("/api", rocket::catchers![routes_maker::unauthorized]) .mount( "/", rocket::routes![routes_maker::dist, routes_maker::index], ) .register("/", rocket::catchers![routes_maker::unauthorized]) .launch() .await?; db.close().await; Ok(()) }