From f4d38451dd4fb2ac814cef0590269666c0a5651d Mon Sep 17 00:00:00 2001 From: readme-bot Date: Sun, 30 Jul 2023 17:34:42 +0800 Subject: [PATCH 1/2] add wallet analyzer --- src/components/configuration/index.tsx | 5 +- src/components/index/index.tsx | 76 ++++++++---- src/components/wallet-analyzer/index.tsx | 37 +++++- .../wallet-assets-percentage/index.tsx | 116 ++++++++++++++++++ src/middlelayers/charts.ts | 42 +++++-- src/middlelayers/configuration.ts | 21 ++-- .../datafetch/coins/cex/binance.ts | 13 +- src/middlelayers/datafetch/coins/cex/cex.ts | 18 ++- src/middlelayers/datafetch/coins/cex/okex.ts | 13 +- .../datafetch/coins/cex/others.ts | 10 +- src/middlelayers/types.d.ts | 9 ++ src/middlelayers/wallet.ts | 70 +++++++++++ 12 files changed, 377 insertions(+), 53 deletions(-) create mode 100644 src/components/wallet-assets-percentage/index.tsx create mode 100644 src/middlelayers/wallet.ts diff --git a/src/components/configuration/index.tsx b/src/components/configuration/index.tsx index 966192e..c56c076 100644 --- a/src/components/configuration/index.tsx +++ b/src/components/configuration/index.tsx @@ -126,9 +126,7 @@ const Configuration = ({ setLoading(true); getConfiguration() .then((d) => { - const globalConfig = d?.data - ? (yaml.parse(d.data) as GlobalConfig) - : initialConfiguration; + const globalConfig = d ?? initialConfiguration; setGroupUSD(globalConfig.configs.groupUSD); setQuerySize(globalConfig.configs.querySize || 10); @@ -145,7 +143,6 @@ const Configuration = ({ })) .value() ); - console.log(globalConfig); setWallets( _(globalConfig) diff --git a/src/components/index/index.tsx b/src/components/index/index.tsx index cb79705..2f88269 100644 --- a/src/components/index/index.tsx +++ b/src/components/index/index.tsx @@ -1,5 +1,6 @@ import { Chart as ChartJS, + registerables, CategoryScale, LinearScale, PointElement, @@ -27,7 +28,7 @@ import { TopCoinsPercentageChangeData, TopCoinsRankData, } from "../../middlelayers/types"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { queryAssetChange, queryTopCoinsPercentageChangeData, @@ -45,8 +46,10 @@ import { } from "../../middlelayers/configuration"; import { autoSyncData } from "../../middlelayers/cloudsync"; import { getDefaultCurrencyRate } from "../../middlelayers/currency"; +import _ from "lodash"; ChartJS.register( + ...registerables, ArcElement, CategoryScale, LinearScale, @@ -70,7 +73,8 @@ const App = () => { ); const [showMenu, setShowMenu] = useState(false); - const [activeMenu, setActiveMenu] = useState("overview"); + // const [activeMenu, setActiveMenu] = useState("overview"); + const [activeMenu, setActiveMenu] = useState("wallets"); const [latestAssetsPercentageData, setLatestAssetsPercentageData] = useState( [] as LatestAssetsPercentageData @@ -117,7 +121,7 @@ const App = () => { if ( lastSize.width === windowSize.width && lastSize.height === windowSize.height && - activeMenu === "overview" + (activeMenu === "overview" || activeMenu === "wallets") ) { resizeAllCharts(); } @@ -133,10 +137,32 @@ const App = () => { }, [showMenu]); function resizeAllCharts() { + const overviewsCharts = [ + "Trend of Asset", + "Trend of Coin", + "Percentage of Assets", + "Change of Top Coins", + "Trend of Top Coins Rank", + ]; + const walletsCharts = ["Percentage of Wallet"]; + let chartsTitles: string[] = []; + if (activeMenu === "overview") { + chartsTitles = overviewsCharts; + } else if (activeMenu === "wallets") { + chartsTitles = walletsCharts; + } console.log("resizing all charts"); for (const id in Chart.instances) { - Chart.instances[id].resize(); + const text = Chart.instances[id].options.plugins?.title?.text as + | string + | undefined; + if ( + !text || + !!_(chartsTitles).find((x) => text === x || text.startsWith(x)) + ) { + Chart.instances[id].resize(); + } } } @@ -189,10 +215,10 @@ const App = () => { } useEffect(() => { - if (activeMenu === "overview") { + if (activeMenu === "overview" || activeMenu === "wallets") { setTimeout(() => { resizeAllCharts(); - }, resizeDelay); + }, resizeDelay / 2); } }, [activeMenu]); @@ -206,7 +232,7 @@ const App = () => { ); @@ -259,22 +285,24 @@ const App = () => {
-
- -
+ {activeMenu === "overview" && ( +
+ +
+ )} {activeMenu === "comparison" && (
@@ -284,7 +312,7 @@ const App = () => { {activeMenu === "wallets" && (
- +
)}
diff --git a/src/components/wallet-analyzer/index.tsx b/src/components/wallet-analyzer/index.tsx index bf4bea2..4418f66 100644 --- a/src/components/wallet-analyzer/index.tsx +++ b/src/components/wallet-analyzer/index.tsx @@ -1,8 +1,37 @@ -const App = ({}: {}) => { +import { useContext, useEffect, useState } from "react"; +import { queryWalletAssetsPercentage } from "../../middlelayers/charts"; +import WalletAssetsPercentage from "../wallet-assets-percentage"; +import { LoadingContext } from "../../App"; +import { + CurrencyRateDetail, + WalletAssetsPercentageData, +} from "../../middlelayers/types"; + +const App = ({ currency }: { currency: CurrencyRateDetail }) => { + const { setLoading } = useContext(LoadingContext); + const [walletAssetsPercentage, setWalletAssetsPercentage] = + useState([]); + + useEffect(() => { + setLoading(true); + loadAllDataAsync().finally(() => setLoading(false)); + }, []); + + async function loadAllDataAsync() { + console.log("loading all wallet data..."); + const wap = await queryWalletAssetsPercentage(); + setWalletAssetsPercentage(wap); + } + return ( -
-

TODO

-
+ <> +

Wallet Analyzer

+ +
+ ); }; diff --git a/src/components/wallet-assets-percentage/index.tsx b/src/components/wallet-assets-percentage/index.tsx new file mode 100644 index 0000000..9c6946a --- /dev/null +++ b/src/components/wallet-assets-percentage/index.tsx @@ -0,0 +1,116 @@ +import { Bar } from "react-chartjs-2"; +import { useWindowSize } from "../../utils/hook"; +import { + CurrencyRateDetail, + WalletAssetsPercentageData, +} from "../../middlelayers/types"; +import { currencyWrapper } from '../../utils/currency' + +const App = ({ + data, + currency, +}: { + data: WalletAssetsPercentageData; + currency: CurrencyRateDetail; +}) => { + const size = useWindowSize(); + + const options = { + maintainAspectRatio: false, + responsive: false, + indexAxis: "y", + barPercentage: 0.9, + // categoryPercentage: 0.7, + plugins: { + title: { display: true, text: "Percentage of Wallet" }, + legend: { + display: false, + }, + datalabels: { + display: "auto", + align: "top", + offset: 6, + formatter: ( + value: number, + context: { + chart: { data: { labels: { [x: string]: any } } }; + dataIndex: string | number; + } + ) => { + // const label = context.chart.data.labels[context.dataIndex]; + return `${currency.symbol}${value.toFixed(2)}`; + }, + }, + }, + }; + + function lineData() { + return { + labels: data.map((d) => d.walletAlias || d.wallet), + datasets: [ + { + alias: "y", + fill: false, + data: data.map((d) => currencyWrapper(currency)(d.value)), + borderColor: data.map((d) => d.chartColor), + backgroundColor: data.map((d) => d.chartColor), + borderWidth: 1, + }, + ], + }; + } + // const options = { + // maintainAspectRatio: false, + // responsive: false, + // plugins: { + // title: { display: true, text: "Percentage of Wallet" }, + // legend: { labels: { font: {} } }, + // datalabels: { + // color: "white", + // font: { + // weight: "bold", + // }, + // display: "auto", + // formatter: ( + // value: number, + // context: { + // chart: { data: { labels: { [x: string]: any } } }; + // dataIndex: string | number; + // } + // ) => { + // const label = context.chart.data.labels[context.dataIndex]; + // return `${label}: ${value.toLocaleString()}%`; + // }, + // }, + // }, + // }; + + // function lineData() { + // return { + // labels: data.map((d) => d.walletAlias || d.wallet), + // datasets: [ + // { + // data: data.map((d) => d.percentage), + // borderColor: data.map((d) => d.chartColor), + // backgroundColor: data.map((d) => d.chartColor), + // borderWidth: 1, + // }, + // ], + // }; + // } + + return ( +
+
+ {/* */} + +
+
+ ); +}; + +export default App; diff --git a/src/middlelayers/charts.ts b/src/middlelayers/charts.ts index e8adb04..6c623ee 100644 --- a/src/middlelayers/charts.ts +++ b/src/middlelayers/charts.ts @@ -1,14 +1,14 @@ import _ from 'lodash' -import yaml from 'yaml' import { generateRandomColors } from '../utils/color' import { getDatabase, saveCoinsToDatabase } from './database' -import { AssetChangeData, AssetModel, CoinData, CoinsAmountAndValueChangeData, HistoricalData, LatestAssetsPercentageData, TopCoinsPercentageChangeData, TopCoinsRankData, TotalValueData } from './types' +import { AssetChangeData, AssetModel, CoinData, CoinsAmountAndValueChangeData, HistoricalData, LatestAssetsPercentageData, WalletAssetsPercentageData, TopCoinsPercentageChangeData, TopCoinsRankData, TotalValueData } from './types' import { loadPortfolios, queryCoinPrices } from './data' import { getConfiguration } from './configuration' import { calculateTotalValue } from './datafetch/utils/coins' -import { CexConfig, TokenConfig, WalletCoin } from './datafetch/types' +import { WalletCoin } from './datafetch/types' import { timestampToDate } from '../utils/date' +import { listWalletAliases } from './wallet' const STABLE_COIN = ["USDT", "USDC", "BUSD", "DAI", "TUSD", "PAX"] @@ -23,11 +23,10 @@ async function queryCoinsData(): Promise<(WalletCoin & { price: number, usdValue: number, })[]> { - const configModel = await getConfiguration() - if (!configModel) { + const config = await getConfiguration() + if (!config) { throw new Error("no configuration found,\n please add configuration first") } - const config = yaml.parse(configModel.data) as CexConfig & TokenConfig const assets = await loadPortfolios(config) const priceMap = await queryCoinPrices(_(assets).map("symbol").push("USDT").uniq().value()) @@ -38,7 +37,7 @@ async function queryCoinsData(): Promise<(WalletCoin & { _(assets).groupBy('wallet').forEach((coins, wallet) => { const usdAmount = _(coins).filter(c => STABLE_COIN.includes(c.symbol)).map(c => c.amount).sum() const removedUSDCoins = _(coins).filter(c => !STABLE_COIN.includes(c.symbol)).value() - lastAssets = _(lastAssets).filter(a=>a.wallet !== wallet).concat(removedUSDCoins).value() + lastAssets = _(lastAssets).filter(a => a.wallet !== wallet).concat(removedUSDCoins).value() if (usdAmount > 0) { lastAssets.push({ symbol: "USDT", @@ -137,6 +136,35 @@ export async function queryTotalValue(): Promise { } } +export async function queryWalletAssetsPercentage(): Promise { + const assets = (await queryAssets(1))[0] + // check if there is wallet column + const hasWallet = _(assets).find(a => !!a.wallet) + if (!assets || !hasWallet) { + return [] + } + const walletAssets = _(assets).groupBy('wallet') + .map((walletAssets, wallet) => { + const total = _(walletAssets).sumBy("value") + return { + wallet, + total, + } + }).value() + const total = _(walletAssets).sumBy("total") || 0.0001 + const wallets = _(walletAssets).map('wallet').uniq().compact().value() + const backgroundColors = generateRandomColors(wallets.length) + const walletAliases = await listWalletAliases(wallets) + + return _(walletAssets).map((wa, idx) => ({ + wallet: wa.wallet, + walletAlias: walletAliases[wa.wallet], + chartColor: `rgba(${backgroundColors[idx].R}, ${backgroundColors[idx].G}, ${backgroundColors[idx].B}, 1)`, + percentage: wa.total / total * 100, + value: wa.total, + })).sortBy("percentage").reverse().value() +} + export async function queryTopCoinsRank(size = 10): Promise { const assets = groupAssetModelsListBySymbol(await queryAssets(size) || []) diff --git a/src/middlelayers/configuration.ts b/src/middlelayers/configuration.ts index 762b9c2..e421fa8 100644 --- a/src/middlelayers/configuration.ts +++ b/src/middlelayers/configuration.ts @@ -9,8 +9,14 @@ const prefix = "!ent:" const fixId = "1" const cloudSyncFixId = "2" -export async function getConfiguration(): Promise { - return getConfigurationById(fixId) +export async function getConfiguration(): Promise { + const model = await getConfigurationById(fixId) + if (!model) { + return + } + + const data = yaml.parse(model.data) + return data } export async function saveConfiguration(cfg: GlobalConfig) { @@ -73,17 +79,16 @@ export async function getQuerySize(): Promise { return 10 } - const data = yaml.parse(cfg.data) - return data.configs.querySize || 10 + return cfg.configs.querySize || 10 } -export async function getCurrentPreferCurrency() : Promise { - const model = await getConfiguration() - if (!model) { +export async function getCurrentPreferCurrency(): Promise { + const cfg = await getConfiguration() + if (!cfg) { return getDefaultCurrencyRate() } - const pc: string = yaml.parse(model.data).configs.preferCurrency + const pc: string = cfg.configs.preferCurrency if (!pc) { return getDefaultCurrencyRate() } diff --git a/src/middlelayers/datafetch/coins/cex/binance.ts b/src/middlelayers/datafetch/coins/cex/binance.ts index 134b26a..1c4b99b 100644 --- a/src/middlelayers/datafetch/coins/cex/binance.ts +++ b/src/middlelayers/datafetch/coins/cex/binance.ts @@ -6,20 +6,31 @@ export class BinanceExchange implements Exchanger { private readonly apiKey: string private readonly secret: string + private readonly alias?: string constructor( apiKey: string, - secret: string + secret: string, + alias?: string, ) { this.apiKey = apiKey this.secret = secret + this.alias = alias + } + + getExchangeName(): string { + return "Binance" } getIdentity(): string { return "binance-" + this.apiKey } + getAlias(): string | undefined { + return this.alias + } + async fetchTotalBalance(): Promise<{ [k: string]: number }> { return invoke("query_binance_balance", { apiKey: this.apiKey, apiSecret: this.secret }) } diff --git a/src/middlelayers/datafetch/coins/cex/cex.ts b/src/middlelayers/datafetch/coins/cex/cex.ts index f051b31..b83850e 100644 --- a/src/middlelayers/datafetch/coins/cex/cex.ts +++ b/src/middlelayers/datafetch/coins/cex/cex.ts @@ -7,7 +7,11 @@ import { OkexExchange } from './okex' import { CacheCenter } from '../../utils/cache' export interface Exchanger { + getExchangeName(): string + getIdentity(): string + + getAlias(): string | undefined // return all coins in exchange // key is coin symbol, value is amount fetchTotalBalance(): Promise<{ [k: string]: number }> @@ -26,15 +30,15 @@ export class CexAnalyzer implements Analyzer { console.log("loading exchange", exCfg.name) switch (exCfg.name) { case "binance": - return new BinanceExchange(exCfg.initParams.apiKey, exCfg.initParams.secret) + return new BinanceExchange(exCfg.initParams.apiKey, exCfg.initParams.secret, exCfg.alias) case "okex": case "okx": if (!exCfg.initParams.password) { throw new Error("okex password is required") } - return new OkexExchange(exCfg.initParams.apiKey, exCfg.initParams.secret, exCfg.initParams.password) + return new OkexExchange(exCfg.initParams.apiKey, exCfg.initParams.secret, exCfg.initParams.password, exCfg.alias) default: - return new OtherCexExchanges(exCfg.name, exCfg.initParams) + return new OtherCexExchanges(exCfg.name, exCfg.initParams, exCfg.alias) } }).compact().value() } @@ -70,6 +74,14 @@ export class CexAnalyzer implements Analyzer { return _(coinLists).flatten().value() } + + public listExchangeIdentities(): { exchangeName: string, identity: string, alias?: string }[] { + return _(this.exchanges).map(ex => ({ + exchangeName: ex.getExchangeName(), + identity: ex.getIdentity(), + alias: ex.getAlias() + })).value() + } } export function filterCoinsInPortfolio(wallet: string, portfolio: { [k: string]: number }): WalletCoin[] { diff --git a/src/middlelayers/datafetch/coins/cex/okex.ts b/src/middlelayers/datafetch/coins/cex/okex.ts index 4610a0f..cc62581 100644 --- a/src/middlelayers/datafetch/coins/cex/okex.ts +++ b/src/middlelayers/datafetch/coins/cex/okex.ts @@ -5,16 +5,27 @@ export class OkexExchange implements Exchanger { private readonly apiKey: string private readonly secret: string private readonly password: string - + private readonly alias?: string + constructor( apiKey: string, secret: string, password: string, + alias?: string, ) { this.apiKey = apiKey this.secret = secret this.password = password + this.alias = alias + } + + getExchangeName(): string { + return "Okex" + } + + getAlias(): string | undefined { + return this.alias } getIdentity(): string { diff --git a/src/middlelayers/datafetch/coins/cex/others.ts b/src/middlelayers/datafetch/coins/cex/others.ts index 918cf18..4051a22 100644 --- a/src/middlelayers/datafetch/coins/cex/others.ts +++ b/src/middlelayers/datafetch/coins/cex/others.ts @@ -6,11 +6,19 @@ export class OtherCexExchanges implements Exchanger { apiKey: string secret: string password?: string - }) { + }, alias?: string) { } + getExchangeName(): string { + throw new Error('Method not implemented.') + } + + getAlias(): string | undefined { + throw new Error('Method not implemented.') + } + getIdentity(): string { throw new Error('Method not implemented.') } diff --git a/src/middlelayers/types.d.ts b/src/middlelayers/types.d.ts index 0896ce0..3e8671a 100644 --- a/src/middlelayers/types.d.ts +++ b/src/middlelayers/types.d.ts @@ -80,6 +80,15 @@ export type LatestAssetsPercentageData = { chartColor: string }[] +// show usd value percentage of assets in each wallet +export type WalletAssetsPercentageData = { + wallet: string + walletAlias?: string + percentage: number + value: number + chartColor: string +}[] + export type CoinsAmountAndValueChangeData = { coin: string lineColor: string diff --git a/src/middlelayers/wallet.ts b/src/middlelayers/wallet.ts new file mode 100644 index 0000000..ac98671 --- /dev/null +++ b/src/middlelayers/wallet.ts @@ -0,0 +1,70 @@ +import _ from 'lodash' +import { getConfiguration } from './configuration' +import { CexAnalyzer } from './datafetch/coins/cex/cex' +import md5 from 'md5' +import { Addresses } from './datafetch/types' + +export async function listWalletAliases(wallets: string[]): Promise<{ [k: string]: string | undefined }> { + const config = await getConfiguration() + + if (!config) { + return {} + } + + const aliases: { + // wallet hash + walletType: string + wallet: string + alias: string + }[] = [] + + // cex exchanges + const cexAna = new CexAnalyzer(config) + _(cexAna.listExchangeIdentities()).forEach(x => { + aliases.push({ + walletType: x.exchangeName, + // need md5 here, because when we store it in database, it is md5 hashed + wallet: md5(x.identity), + alias: x.alias || x.identity, + }) + }) + + const handleWeb3Wallet = (addrs: Addresses, walletType: string) => { + _(addrs.addresses).forEach(x => { + const alias = _(x).isString() ? undefined : (x as { alias: string, address: string }).alias + const address = _(x).isString() ? x as string : (x as { alias: string, address: string }).address + aliases.push({ + walletType, + wallet: md5(address), + alias: alias || address, + }) + }) + + } + + // BTC + handleWeb3Wallet(config.btc, "BTC") + // ETH + handleWeb3Wallet(config.erc20, "ERC20") + // Doge + handleWeb3Wallet(config.doge, "DOGE") + // SOL + handleWeb3Wallet(config.sol, "SOL") + + const others = "others" + const Others = _(others).upperFirst() + + // Others + aliases.push({ + walletType: Others, + wallet: md5(others), + alias: Others, + }) + + return _(wallets).map(w => { + const alias = _(aliases).find(x => x.wallet === w) + return { + [w]: alias ? alias.walletType === Others ? alias.walletType : alias?.walletType + "-" + alias?.alias : undefined + } + }).reduce((a, b) => ({ ...a, ...b }), {}) +} From 215205fefcfa94b47871079716d7d01e5cfc289e Mon Sep 17 00:00:00 2001 From: readme-bot Date: Sun, 30 Jul 2023 17:36:38 +0800 Subject: [PATCH 2/2] remove useless code --- src/components/index/index.tsx | 4 +- .../wallet-assets-percentage/index.tsx | 40 ------------------- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/src/components/index/index.tsx b/src/components/index/index.tsx index 2f88269..3514059 100644 --- a/src/components/index/index.tsx +++ b/src/components/index/index.tsx @@ -73,8 +73,8 @@ const App = () => { ); const [showMenu, setShowMenu] = useState(false); - // const [activeMenu, setActiveMenu] = useState("overview"); - const [activeMenu, setActiveMenu] = useState("wallets"); + const [activeMenu, setActiveMenu] = useState("overview"); + // const [activeMenu, setActiveMenu] = useState("wallets"); const [latestAssetsPercentageData, setLatestAssetsPercentageData] = useState( [] as LatestAssetsPercentageData diff --git a/src/components/wallet-assets-percentage/index.tsx b/src/components/wallet-assets-percentage/index.tsx index 9c6946a..4c37a62 100644 --- a/src/components/wallet-assets-percentage/index.tsx +++ b/src/components/wallet-assets-percentage/index.tsx @@ -20,7 +20,6 @@ const App = ({ responsive: false, indexAxis: "y", barPercentage: 0.9, - // categoryPercentage: 0.7, plugins: { title: { display: true, text: "Percentage of Wallet" }, legend: { @@ -59,45 +58,6 @@ const App = ({ ], }; } - // const options = { - // maintainAspectRatio: false, - // responsive: false, - // plugins: { - // title: { display: true, text: "Percentage of Wallet" }, - // legend: { labels: { font: {} } }, - // datalabels: { - // color: "white", - // font: { - // weight: "bold", - // }, - // display: "auto", - // formatter: ( - // value: number, - // context: { - // chart: { data: { labels: { [x: string]: any } } }; - // dataIndex: string | number; - // } - // ) => { - // const label = context.chart.data.labels[context.dataIndex]; - // return `${label}: ${value.toLocaleString()}%`; - // }, - // }, - // }, - // }; - - // function lineData() { - // return { - // labels: data.map((d) => d.walletAlias || d.wallet), - // datasets: [ - // { - // data: data.map((d) => d.percentage), - // borderColor: data.map((d) => d.chartColor), - // backgroundColor: data.map((d) => d.chartColor), - // borderWidth: 1, - // }, - // ], - // }; - // } return (