diff --git a/Cargo.lock b/Cargo.lock index d55c24d..72a18f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -673,6 +673,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -740,6 +761,7 @@ dependencies = [ "async-tungstenite", "binance-rs-async", "chrono", + "directories-next", "futures", "iced", "iced_futures 0.12.0", @@ -747,6 +769,8 @@ dependencies = [ "itertools 0.11.0", "lazy_static", "once_cell", + "serde", + "serde_json", "tokio", "uuid", "warp", @@ -1817,6 +1841,17 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall 0.4.1", +] + [[package]] name = "libredox" version = "0.0.2" @@ -2202,7 +2237,7 @@ version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" dependencies = [ - "libredox", + "libredox 0.0.2", ] [[package]] @@ -2616,6 +2651,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox 0.0.1", + "thiserror", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 80f9ed3..7dfaa33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,13 @@ iced_native = "0.10.3" itertools = "0.11.0" lazy_static = "1.4.0" once_cell = "1.18.0" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" warp = "0.3.5" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +directories-next = "2.0" + [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { version = "1.0", features = ["js"] } web-sys = { features = ["Window", "Storage"] } diff --git a/src/api.rs b/src/api.rs index ca08294..c3cfd07 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,21 +1,12 @@ -use std::env; - use binance::account::Account; use binance::api::Binance; use binance::errors::Error; use binance::rest_model::{Balance, Order, OrderSide, Transaction}; use binance::wallet::Wallet; use futures::future::join_all; -use lazy_static::lazy_static; - -lazy_static! { - pub static ref PUB: Option = env::var_os("DYN_PUB").map(|s| s.into_string().unwrap()); - static ref SEC: Option = env::var_os("DYN_SEC").map(|s| s.into_string().unwrap()); - static ref B: Account = Binance::new(PUB.clone(), SEC.clone()); - static ref W: Wallet = Binance::new(PUB.clone(), SEC.clone()); -} -pub async fn orders_history() -> Vec { +pub async fn orders_history(public: String, secret: String) -> Vec { + let b: Account = Binance::new(Some(public), Some(secret)); let now = chrono::offset::Local::now(); let ago = now .checked_sub_signed(chrono::Duration::try_weeks(8).unwrap()) @@ -29,7 +20,7 @@ pub async fn orders_history() -> Vec { "SYNUSDT", ]; let mut os: Vec = join_all(assets.iter().map(|a: &&str| { - B.get_all_orders(binance::account::OrdersQuery { + b.get_all_orders(binance::account::OrdersQuery { symbol: a.to_string(), order_id: None, start_time: Some(ago.timestamp_millis() as u64), @@ -48,12 +39,15 @@ pub async fn orders_history() -> Vec { } pub async fn trade_spot( + public: String, + secret: String, pair: String, price: f64, amt: f64, side: OrderSide, ) -> Result { - B.place_order(binance::account::OrderRequest { + let b: Account = Binance::new(Some(public), Some(secret)); + b.place_order(binance::account::OrderRequest { symbol: pair, side, order_type: binance::rest_model::OrderType::Limit, @@ -70,9 +64,10 @@ pub async fn trade_spot( .await } -pub async fn balances() -> Vec { +pub async fn balances(public: String, secret: String) -> Vec { + let b: Account = Binance::new(Some(public), Some(secret)); let assets = ["LINK", "UNI", "ARB", "OP", "SYN", "USDT", "OP"]; - join_all(assets.iter().map(|a| B.get_balance(a.to_string()))) + join_all(assets.iter().map(|a| b.get_balance(a.to_string()))) .await .into_iter() .flatten() diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..af9f3e7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,104 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + pub api_key: String, + pub api_secret_key: String, +} + +#[derive(Debug, Clone)] +pub enum LoadError { + File, + Format, +} + +#[derive(Debug, Clone)] +pub enum SaveError { + File, + Write, + Format, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Config { + fn path() -> std::path::PathBuf { + let mut path = if let Some(project_dirs) = + directories_next::ProjectDirs::from("rs", "x86y", "Dynasty") + { + project_dirs.data_dir().into() + } else { + std::env::current_dir().unwrap_or_default() + }; + path.push("config.json"); + path + } + + pub async fn load() -> Result { + use tokio::fs::File; + use tokio::io::AsyncReadExt; + + let mut contents = String::new(); + let mut file = File::open(Self::path()) + .await + .map_err(|_| LoadError::File)?; + file.read_to_string(&mut contents) + .await + .map_err(|_| LoadError::File)?; + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + pub async fn save(self) -> Result<(), SaveError> { + use tokio::fs::File; + use tokio::io::AsyncWriteExt; + + let json = serde_json::to_string_pretty(&self).map_err(|_| SaveError::Format)?; + let path = Self::path(); + if let Some(dir) = path.parent() { + tokio::fs::create_dir_all(dir) + .await + .map_err(|_| SaveError::File)?; + } + { + let mut file = File::create(path).await.map_err(|_| SaveError::File)?; + file.write_all(json.as_bytes()) + .await + .map_err(|_| SaveError::Write)?; + } + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +impl Config { + fn storage() -> Option { + let window = web_sys::window()?; + + window.local_storage().ok()? + } + + async fn load() -> Result { + let storage = Self::storage().ok_or(LoadError::File)?; + + let contents = storage + .get_item("state") + .map_err(|_| LoadError::File)? + .ok_or(LoadError::File)?; + + serde_json::from_str(&contents).map_err(|_| LoadError::Format) + } + + async fn save(self) -> Result<(), SaveError> { + let storage = Self::storage().ok_or(SaveError::File)?; + + let json = serde_json::to_string_pretty(&self).map_err(|_| SaveError::Format)?; + + storage + .set_item("state", &json) + .map_err(|_| SaveError::Write)?; + + let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 5b01ca3..265ba26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,15 @@ mod api; +mod config; mod theme; mod views; mod ws; +use binance::api::Binance; use binance::rest_model::OrderStatus; use binance::ws_model::TradesEvent; +use config::Config; +use config::LoadError; +use config::SaveError; use iced::font; use iced::widget::button; use iced::widget::responsive; @@ -29,7 +34,6 @@ use ws::prices; use ws::trades; use ws::user; -use api::trade_spot; use binance::rest_model::{Balance, Order}; use iced::executor; use iced::widget::{column, container, row, text}; @@ -71,8 +75,7 @@ struct App { filter_string: String, data: AppData, current_view: ViewState, - api_key: String, - api_secret_key: String, + config: Config, } #[derive(PartialEq)] @@ -93,6 +96,9 @@ struct AppData { #[derive(Debug, Clone)] pub enum Message { + SaveConfig(String, String), + ConfigLoaded(Result), + Saved(Result<(), SaveError>), SettingsApiKeyChanged(String), SettingsApiSecretChanged(String), SetDashboardView, @@ -197,16 +203,10 @@ impl Application for App { pair_submitted: true, data: Default::default(), current_view: ViewState::Dashboard, - api_key: env::var_os("DYN_PUB") - .map(|s| s.into_string().unwrap()) - .unwrap(), - api_secret_key: env::var_os("DYN_SEC") - .map(|s| s.into_string().unwrap()) - .unwrap(), + config: Default::default(), }, Command::batch(vec![ - Command::perform(api::orders_history(), Message::OrdersRecieved), - Command::perform(api::balances(), Message::BalancesRecieved), + Command::perform(Config::load(), Message::ConfigLoaded), font::load(include_bytes!("../fonts/icons.ttf").as_slice()) .map(Message::FontsLoaded), ]), @@ -219,12 +219,40 @@ impl Application for App { fn update(&mut self, message: Message) -> Command { match message { + Message::Saved(_) => Command::none(), + Message::SaveConfig(pub_k, sec_k) => Command::perform( + Config { + api_key: pub_k, + api_secret_key: sec_k, + } + .save(), + Message::Saved, + ), + Message::ConfigLoaded(c) => { + self.config = c.unwrap_or_default(); + Command::batch(vec![ + Command::perform( + api::orders_history( + self.config.api_key.clone(), + self.config.api_secret_key.clone(), + ), + Message::OrdersRecieved, + ), + Command::perform( + api::balances( + self.config.api_key.clone(), + self.config.api_secret_key.clone(), + ), + Message::BalancesRecieved, + ), + ]) + } Message::SettingsApiKeyChanged(value) => { - self.api_key = value; + self.config.api_key = value; Command::none() } Message::SettingsApiSecretChanged(value) => { - self.api_secret_key = value; + self.config.api_secret_key = value; Command::none() } Message::SetSettingsView => { @@ -330,7 +358,9 @@ impl Application for App { Command::perform(async {}, Message::MarketPairSet2) } Message::BuyPressed => Command::perform( - trade_spot( + api::trade_spot( + self.config.api_key.clone(), + self.config.api_secret_key.clone(), self.new_pair.clone(), self.new_price.clone().parse().unwrap(), self.new_amt.parse().unwrap(), @@ -342,7 +372,9 @@ impl Application for App { }, ), Message::SellPressed => Command::perform( - trade_spot( + api::trade_spot( + self.config.api_key.clone(), + self.config.api_secret_key.clone(), self.new_pair.clone(), self.new_price.clone().parse().unwrap(), self.new_amt.parse().unwrap(), @@ -503,7 +535,7 @@ impl Application for App { fn subscription(&self) -> Subscription { Subscription::batch([ prices::connect().map(Message::PriceEcho), - user::connect().map(Message::UserEcho), + user::connect(self.config.api_key.clone()).map(Message::UserEcho), if self.pair_submitted { trades::connect(self.new_pair.to_lowercase()).map(Message::TradeEcho) } else { @@ -606,26 +638,34 @@ impl Application for App { ] .align_items(iced::Alignment::Center); - let api_key_input = text_input("API Key", &self.api_key) + let api_key_input = text_input("API Key", &self.config.api_key) .secure(true) .width(Length::Fill) .on_input(Message::SettingsApiKeyChanged); - let api_secret_key_input = text_input("API Secret Key", &self.api_secret_key) + let api_secret_key_input = text_input("API Secret Key", &self.config.api_secret_key) .secure(true) .width(Length::Fill) .on_input(Message::SettingsApiSecretChanged); - let settings = column![ - row![text("API Key:").width(Length::Fixed(100.0)), api_key_input].spacing(10), - row![ - text("API Secret Key:").width(Length::Fixed(100.0)), - api_secret_key_input, + let settings = container( + column![ + row![text("API Key:").width(Length::Fixed(100.0)), api_key_input].spacing(10), + row![ + text("API Secret Key:").width(Length::Fixed(100.0)), + api_secret_key_input, + ] + .spacing(10), + button("SAVE!").on_press(Message::SaveConfig( + self.config.api_key.clone(), + self.config.api_secret_key.clone() + )), ] - .spacing(10), - ] - .spacing(10) - .width(Length::Fill) - .align_items(iced::Alignment::Center); + .spacing(10) + .width(Length::Fill) + .align_items(iced::Alignment::Center), + ) + .center_x() + .center_y(); let message_log: Element<_> = if self.data.prices.is_empty() { container(text("Loading...").style(Color::from_rgb8(0x88, 0x88, 0x88))) @@ -641,11 +681,7 @@ impl Application for App { if self.current_view == ViewState::Dashboard { container(pane_grid) } else { - container( - column![settings] - .width(Length::Fill) - .height(Length::Fill) - ) + container(column![settings].width(Length::Fill).height(Length::Fill)) } ] .spacing(8) diff --git a/src/views/panes/balances.rs b/src/views/panes/balances.rs index 664c202..058d237 100644 --- a/src/views/panes/balances.rs +++ b/src/views/panes/balances.rs @@ -11,46 +11,51 @@ use crate::{theme::h2c, Message}; use crate::views::components::unstyled_btn::UnstyledBtn; pub fn balances_view<'a>(bs: &[Balance], ps: &'a HashMap) -> Element<'a, Message> { - scrollable(Column::with_children( - bs.iter() - .map(|b| { - let ticker = &b.asset.split("USDT").next().unwrap(); - let handle = svg::Handle::from_path( - format!("{}/assets/logos/{}.svg", env!("CARGO_MANIFEST_DIR"), ticker) - ); + scrollable( + Column::with_children( + bs.iter() + .map(|b| { + let ticker = &b.asset.split("USDT").next().unwrap(); + let handle = svg::Handle::from_path(format!( + "{}/assets/logos/{}.svg", + env!("CARGO_MANIFEST_DIR"), + ticker + )); - let svg = svg(handle) - .width(Length::Fixed(16.0)) - .height(Length::Fixed(16.0)); - container(row![ - row![ - svg, + let svg = svg(handle) + .width(Length::Fixed(16.0)) + .height(Length::Fixed(16.0)); + container(row![ + row![ + svg, + button( + text(&b.asset) + .font(iced::Font { + weight: iced::font::Weight::Bold, + ..Default::default() + }) + .size(14) + .style(h2c("B7BDB7").unwrap()) + ) + .style(iced::theme::Button::Custom(Box::new(UnstyledBtn {}))) + .on_press(Message::AssetSelected(b.asset.clone())), + ] + .spacing(4) + .align_items(iced::Alignment::Center), + Space::new(Length::Fill, 1.0), button( - text(&b.asset) - .font(iced::Font { - weight: iced::font::Weight::Bold, - ..Default::default() - }) + text(format!("{}", (b.free * 10.0).round() / 10.0)) .size(14) .style(h2c("B7BDB7").unwrap()) ) .style(iced::theme::Button::Custom(Box::new(UnstyledBtn {}))) .on_press(Message::AssetSelected(b.asset.clone())), - ] - .spacing(4) - .align_items(iced::Alignment::Center), - Space::new(Length::Fill, 1.0), - button( - text(format!("{}", (b.free * 10.0).round() / 10.0)) - .size(14) - .style(h2c("B7BDB7").unwrap()) - ) - .style(iced::theme::Button::Custom(Box::new(UnstyledBtn {}))) - .on_press(Message::AssetSelected(b.asset.clone())), - ]) - .width(Length::Fill) - }) - .map(Element::from), - ).padding(8)) + ]) + .width(Length::Fill) + }) + .map(Element::from), + ) + .padding(8), + ) .into() } diff --git a/src/views/panes/book.rs b/src/views/panes/book.rs index 69c6ffc..306fe04 100644 --- a/src/views/panes/book.rs +++ b/src/views/panes/book.rs @@ -12,13 +12,12 @@ use super::orders::{t, tb}; pub fn book_view( book: &(String, BTreeMap, BTreeMap), ) -> Element<'_, Message> { - let header = - row![ - tb("Price").width(Length::FillPortion(1)), - tb("Amount").width(Length::FillPortion(1)), - tb("Total").width(Length::FillPortion(1)), - ] - .spacing(10); + let header = row![ + tb("Price").width(Length::FillPortion(1)), + tb("Amount").width(Length::FillPortion(1)), + tb("Total").width(Length::FillPortion(1)), + ] + .spacing(10); let ask_rows = Column::with_children( book.1 diff --git a/src/ws/user.rs b/src/ws/user.rs index d28ecf9..d29c505 100644 --- a/src/ws/user.rs +++ b/src/ws/user.rs @@ -6,9 +6,7 @@ use futures::FutureExt; use std::sync::atomic::AtomicBool; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use crate::api::PUB; - -pub fn connect() -> Subscription { +pub fn connect(public: String) -> Subscription { struct Connect; subscription::channel( @@ -16,7 +14,7 @@ pub fn connect() -> Subscription { 100, |mut output| async move { let keep_running = AtomicBool::new(true); - let user_stream: UserStream = Binance::new(PUB.clone(), None); + let user_stream: UserStream = Binance::new(Some(public), None); let (s, mut r): ( UnboundedSender, UnboundedReceiver, diff --git a/src/ws/util.rs b/src/ws/util.rs index 0f5f3b2..9e3214c 100644 --- a/src/ws/util.rs +++ b/src/ws/util.rs @@ -43,5 +43,4 @@ pub mod m { } }; } - }