From 47c19c5c945acd10e77b718109b7bb952ee84e10 Mon Sep 17 00:00:00 2001 From: domchan <31119455+domechn@users.noreply.github.com> Date: Sun, 29 Oct 2023 09:44:57 -0500 Subject: [PATCH] download crypto coins' logos (#133) * download crypto coins' logos * reorder page * fix param name --- src-tauri/Cargo.lock | 136 +++++++++++++++---- src-tauri/Cargo.toml | 5 +- src-tauri/src/info.rs | 161 +++++++++++++++++++++++ src-tauri/src/lib.rs | 2 +- src-tauri/src/main.rs | 33 ++++- src-tauri/src/price.rs | 82 ------------ src-tauri/tauri.conf.json | 9 +- src/components/historical-data/index.tsx | 47 ++++++- src/middlelayers/data.ts | 6 +- 9 files changed, 361 insertions(+), 120 deletions(-) create mode 100644 src-tauri/src/info.rs delete mode 100644 src-tauri/src/price.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2d987fe..3027216 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -158,7 +158,7 @@ dependencies = [ "hex", "lazy_static", "reqwest", - "ring", + "ring 0.16.20", "serde", "serde_json", "serde_qs 0.11.0", @@ -427,8 +427,7 @@ dependencies = [ [[package]] name = "coingecko" version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b24c2d346c689c0b8a27d4c921a35d8cac68cc266130bc82ba879155ca828da0" +source = "git+https://github.com/domechn/coingecko-rs.git?rev=41e819e#41e819e453fe3e5d1d6de791cfe79a25b7623a00" dependencies = [ "chrono", "reqwest", @@ -1584,15 +1583,16 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ + "futures-util", "http", "hyper", - "rustls 0.20.8", + "rustls 0.21.8", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", ] [[package]] @@ -2248,7 +2248,7 @@ dependencies = [ "log", "pin-project", "reqwest", - "ring", + "ring 0.16.20", "serde", "serde_json", "serde_qs 0.8.5", @@ -2859,9 +2859,9 @@ checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "reqwest" -version = "0.11.17" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64 0.21.0", "bytes", @@ -2882,20 +2882,21 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.8", + "rustls 0.21.8", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.22.6", + "webpki-roots 0.25.2", "winreg", ] @@ -2933,11 +2934,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom 0.2.9", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -2969,7 +2984,7 @@ checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64 0.13.1", "log", - "ring", + "ring 0.16.20", "sct 0.6.1", "webpki 0.21.4", ] @@ -2981,11 +2996,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", - "ring", + "ring 0.16.20", "sct 0.7.0", "webpki 0.22.0", ] +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring 0.17.3", + "rustls-webpki", + "sct 0.7.0", +] + [[package]] name = "rustls-pemfile" version = "1.0.2" @@ -2995,6 +3022,16 @@ dependencies = [ "base64 0.21.0", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.3", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -3055,8 +3092,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -3065,8 +3102,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -3617,6 +3654,27 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -4112,6 +4170,16 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.8", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -4290,6 +4358,7 @@ dependencies = [ "md5", "okex", "rand 0.3.23", + "reqwest", "serde", "serde_json", "sqlx", @@ -4410,6 +4479,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.3.1" @@ -4643,8 +4718,8 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -4653,8 +4728,8 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -4675,6 +4750,12 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + [[package]] name = "webview2-com" version = "0.19.1" @@ -5030,11 +5111,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 37734e7..85036e7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,7 +15,7 @@ tauri-build = {version = "1.1", features = [] } [dependencies] binance-rs-async = {version = "1.3.1", features = ["wallet_api", "futures_api", "savings_api"] } -coingecko = "1.0.1" +coingecko = {git = "https://github.com/domechn/coingecko-rs.git", rev = "41e819e"} lazy_static = "^1.4" magic-crypt = "3.1.12" okex = {git = "https://github.com/domechn/okex-rs.git", rev = "7126e51"} @@ -23,11 +23,12 @@ rand = "^0.3" serde = {version = "1.0", features = ["derive"] } serde_json = "1.0" sqlx = {version = "0.6", features = ["runtime-tokio-rustls", "sqlite"] } -tauri = {version = "1.2", features = ["app-all", "dialog-open", "dialog-save", "fs-read-file", "fs-write-file", "http-all", "process-relaunch", "updater"] } +tauri = {version = "1.2", features = ["app-all", "dialog-open", "dialog-save", "fs-read-file", "fs-write-file", "http-all", "path-all", "process-relaunch", "protocol-asset", "updater"] } tokio = {version = "1", features = ["sync"] } uuid = "1.3.3" tauri-plugin-aptabase = "0.3" md5 = "0.7.0" +reqwest = "0.11.22" [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/info.rs b/src-tauri/src/info.rs new file mode 100644 index 0000000..d33e8f9 --- /dev/null +++ b/src-tauri/src/info.rs @@ -0,0 +1,161 @@ +use std::{collections::HashMap, vec}; + +use coingecko::{ + CoinGeckoClient, +}; + +pub fn get_coin_info_provider() -> CoinGecko { + CoinGecko::new() +} + +pub struct CoinGecko { + client: CoinGeckoClient, +} + +#[derive(Debug, Clone)] +struct CoinGeckoCoin { + id: String, + symbol: String, +} + +// to return infos of currencies +impl CoinGecko { + pub fn new() -> CoinGecko { + let client = CoinGeckoClient::default(); + + CoinGecko { client } + } + + async fn list_all_coin_ids( + &self, + symbols: Vec, + ) -> Result, Box> { + let coins = self.client.coins_list(false).await?; + let coins_map = coins + .iter() + .filter(|c| symbols.contains(&c.symbol.to_uppercase())) + .map(|c| CoinGeckoCoin { + id: c.id.clone(), + symbol: c.symbol.to_uppercase(), + }) + .fold(HashMap::new(), |mut acc, coin| { + acc.entry(coin.symbol.clone()).or_insert(vec![]).push(coin); + acc + }); + + let mut res = vec![]; + for (_, coins) in coins_map { + if coins.len() > 1 { + if let Some(coin) = coins.iter().find(|c| c.id.to_uppercase() == c.symbol) { + res.push(coin.clone()); + } else { + res.push(coins[0].clone()); + } + } else { + res.push(coins[0].clone()); + } + } + return Ok(res); + } + + pub async fn query_coins_prices( + &self, + symbols: Vec, + ) -> Result, Box> { + let all_coins = self.list_all_coin_ids(symbols).await?; + // todo: if there are multi coins with same symbol, we should find by tokenAddress + let all_ids = all_coins.iter().map(|c| &c.id).collect::>(); + let all_prices = self + .client + .price(&all_ids, &["usd"], false, false, false, false) + .await?; + + let mut res = HashMap::new(); + + for coin in all_coins { + if let Some(price) = all_prices.get(&coin.id) { + if let Some(price) = price.usd { + res.insert(coin.symbol, price); + } + } + } + + return Ok(res); + } + + pub async fn download_coins_logos( + &self, + symbols: Vec, + base_dir: String, + ) -> Result<(), Box> { + let mut paths = vec![base_dir.clone()]; + paths.push("assets".to_string()); + paths.push("coins".to_string()); + let path_str = paths.join("/"); + let download_dir = std::path::Path::new(path_str.as_str()); + // mkdir download_dr if not exists + if !download_dir.exists() { + std::fs::create_dir_all(download_dir)?; + } + + let non_exists_symbols = symbols.into_iter().filter(|s| { + let path = download_dir.clone(); + let asset_path = path.join(format!("{}.png", s.to_lowercase())); + !asset_path.exists() + }).collect::>(); + + println!("non_exists_symbols: {:?}", non_exists_symbols); + + if non_exists_symbols.len() == 0 { + return Ok(()); + } + + let all_coins = self.list_all_coin_ids(non_exists_symbols.clone()).await?; + // key: symbol, value: id + let non_exists_ids = all_coins + .iter() + .map(|c| c.id.clone()) + .collect::>(); + + let page_size = std::cmp::min(non_exists_ids.len(), 250) as i64; + + let markets = self + .client + .coins_markets( + "usd", + &non_exists_ids, + None, + coingecko::params::MarketsOrder::MarketCapDesc, + page_size, + 1, + false, + &[], + ) + .await?; + + let markets_size = markets.len() as i64; + + for m in markets { + println!("downloading coin logo: {:?}", m.image); + let logo = reqwest::get(m.image).await?.bytes().await?; + + std::fs::write(download_dir.join(format!("{}.png", m.symbol.to_lowercase())), logo)?; + } + + if markets_size >= page_size { + // full load + + // check if file is not exists for each symbol + // if not exists, make an empty file + + for symbol in non_exists_symbols { + let path = download_dir.clone(); + let asset_path = path.join(format!("{}.png", symbol.to_lowercase())); + if !asset_path.exists() { + std::fs::write(asset_path, "")?; + } + } + } + Ok(()) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 514b98e..1100ac6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,5 +2,5 @@ pub mod binance; pub mod ent; pub mod migration; pub mod okex; -pub mod price; +pub mod info; pub mod types; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 98460e5..9c0e5e7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,7 +11,7 @@ use track3::{ migrate_from_v01_to_v02, migrate_from_v02_to_v03, prepare_required_data, }, okex::Okex, - price::get_price_querier, + info::get_coin_info_provider, }; lazy_static! { @@ -59,7 +59,7 @@ async fn query_okex_balance( )] #[tauri::command] async fn query_coins_prices(symbols: Vec) -> Result, String> { - let client = get_price_querier(); + let client = get_coin_info_provider(); // push "USDT" into symbols if not exists let symbols = if symbols.contains(&"USDT".to_string()) { symbols @@ -78,6 +78,28 @@ async fn query_coins_prices(symbols: Vec) -> Result } } +#[cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] +#[tauri::command] +async fn download_coins_logos( + handle: tauri::AppHandle, + symbols: Vec, +) -> Result<(), String> { + let resource_dir = handle.path_resolver().app_cache_dir().unwrap().to_str().unwrap().to_string(); + let client = get_coin_info_provider(); + + let res = client + .download_coins_logos(symbols, resource_dir) + .await; + + match res { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} + #[cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" @@ -149,7 +171,11 @@ fn main() { if is_first_run(app_dir.as_path()) { init_sqlite_file(app_dir.as_path()); - init_sqlite_tables(app_version.clone(), app_dir.as_path(), resource_dir.as_path()); + init_sqlite_tables( + app_version.clone(), + app_dir.as_path(), + resource_dir.as_path(), + ); } if is_from_v01_to_v02(app_dir.as_path()).unwrap() { @@ -172,6 +198,7 @@ fn main() { decrypt, md5, get_polybase_namespace, + download_coins_logos, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/price.rs b/src-tauri/src/price.rs deleted file mode 100644 index d642649..0000000 --- a/src-tauri/src/price.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::{collections::HashMap, vec}; - -use coingecko::CoinGeckoClient; - -pub fn get_price_querier() -> CoinGecko { - CoinGecko::new() -} - -pub struct CoinGecko { - client: CoinGeckoClient, -} - -#[derive(Debug, Clone)] -struct CoinGeckoCoin { - id: String, - symbol: String, -} - -impl CoinGecko { - pub fn new() -> CoinGecko { - let client = CoinGeckoClient::default(); - - CoinGecko { client } - } - - async fn list_all_coin_ids( - &self, - symbols: Vec, - ) -> Result, Box> { - let coins = self.client.coins_list(false).await?; - let coins_map = coins - .iter() - .filter(|c| symbols.contains(&c.symbol.to_uppercase())) - .map(|c| CoinGeckoCoin { - id: c.id.clone(), - symbol: c.symbol.to_uppercase(), - }) - .fold(HashMap::new(), |mut acc, coin| { - acc.entry(coin.symbol.clone()).or_insert(vec![]).push(coin); - acc - }); - - let mut res = vec![]; - for (_, coins) in coins_map { - if coins.len() > 1 { - if let Some(coin) = coins.iter().find(|c| c.id.to_uppercase() == c.symbol) { - res.push(coin.clone()); - } else { - res.push(coins[0].clone()); - } - } else { - res.push(coins[0].clone()); - } - } - return Ok(res); - } - - pub async fn query_coins_prices( - &self, - symbols: Vec, - ) -> Result, Box> { - let all_coins = self.list_all_coin_ids(symbols).await?; - // todo: if there are multi coins with same symbol, we should find by tokenAddress - let all_ids = all_coins.iter().map(|c| &c.id).collect::>(); - let all_prices = self - .client - .price(&all_ids, &["usd"], false, false, false, false) - .await?; - - let mut res = HashMap::new(); - - for coin in all_coins { - if let Some(price) = all_prices.get(&coin.id) { - if let Some(price) = price.usd { - res.insert(coin.symbol, price); - } - } - } - - return Ok(res); - } -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 54372f2..69667fd 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,7 +15,14 @@ "fs": { "readFile": true, "writeFile": true, - "scope": ["$APP/*", "$RESOURCE/*"] + "scope": ["$APP/**", "$RESOURCE/**", "$APPCACHE/**"] + }, + "path": { + "all": true + }, + "protocol": { + "asset": true, + "assetScope": ["$RESOURCE/**", "$APPCACHE/**"] }, "process": { "relaunch": true diff --git a/src/components/historical-data/index.tsx b/src/components/historical-data/index.tsx index dc16172..c5da819 100644 --- a/src/components/historical-data/index.tsx +++ b/src/components/historical-data/index.tsx @@ -1,11 +1,10 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { deleteHistoricalDataByUUID, queryHistoricalData, } from "../../middlelayers/charts"; import { CurrencyRateDetail, HistoricalData } from "../../middlelayers/types"; import deleteIcon from "../../assets/icons/delete-icon.png"; -import Table from "../common/table"; import _ from "lodash"; import "./index.css"; @@ -17,6 +16,9 @@ import { prettyNumberToLocaleString, } from "../../utils/currency"; import Modal from "../common/modal"; +import { downloadCoinLogos } from "../../middlelayers/data"; +import { appCacheDir as getAppCacheDir } from "@tauri-apps/api/path"; +import { convertFileSrc } from "@tauri-apps/api/tauri"; type RankData = { id: number; @@ -38,10 +40,25 @@ const App = ({ const [rankData, setRankData] = useState([] as RankData[]); const { setLoading } = useContext(LoadingContext); const [isModalOpen, setIsModalOpen] = useState(false); + const [appCacheDir, setAppCacheDir] = useState(""); const [pageNum, setPageNum] = useState(1); const pageSize = 10; + useEffect(() => { + getAppCacheDir().then((d) => setAppCacheDir(d)); + }, []); + + useEffect(() => { + const symbols = _(data) + .map((d) => d.assets) + .flatten() + .map((d) => d.symbol) + .uniq() + .value(); + downloadCoinLogos(symbols); + }, [data]); + const rankColumns = [ { key: "rank", @@ -243,6 +260,29 @@ const App = ({ .value(); } + function detailPage(data: RankData[]) { + return _(data) + .map((d) => { + const filePath = `${appCacheDir}assets/coins/${d.symbol.toLowerCase()}.png`; + const apiPath = convertFileSrc(filePath); + return ( +
+ {d.symbol} +
{d.rank}
+
{d.symbol}
+
{d.amount}
+
{d.value}
+
{d.price}
+
+ ); + }) + .value(); + } + return (

- + {/*
*/} + {detailPage(rankData)}
{ - return await invoke("query_coins_prices", { symbols }) + return invoke("query_coins_prices", { symbols }) +} + +export async function downloadCoinLogos(symbols: string[]): Promise { + return invoke("download_coins_logos", { symbols }) } export async function loadPortfolios(config: CexConfig & TokenConfig): Promise {