From 3bb667bb7a1af0994160ca7e95c225d6121c8095 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 15 Nov 2022 23:48:59 +0200 Subject: [PATCH] feat(Wallet) cache fetched balance history to DB for efficiency - Bump status-go head that include the required specific changes - fetch token balance (native or ERC20) and cache historical token quantity data - fetch FIAT currency - Extend presentation layer (NIM and QML) to account for API changes - Remove timed request and other optimizations from the time of fetching balance history every time instead of querying cache - Add C++ integration debugging tests and update network chain configuration (outdated) Closes: #8175 --- app/README.md | 9 +- .../Onboarding/Accounts/AccountsService.cpp | 34 +- .../src/StatusGo/Wallet/Transfer/Event.cpp | 7 +- .../src/StatusGo/Wallet/Transfer/Event.h | 7 +- .../src/StatusGo/Wallet/WalletApi.cpp | 45 ++- .../src/StatusGo/Wallet/WalletApi.h | 26 +- resources/default-networks.json | 82 ++--- resources/fleets.json | 43 ++- resources/node-config.json | 5 +- .../wallet_section/all_tokens/controller.nim | 8 +- .../all_tokens/io_interface.nim | 2 +- .../main/wallet_section/all_tokens/module.nim | 4 +- .../main/wallet_section/all_tokens/view.nim | 4 +- src/app_service/service/token/async_tasks.nim | 19 +- src/app_service/service/token/service.nim | 48 ++- .../service/wallet_account/service.nim | 17 +- src/backend/backend.nim | 7 +- test/libs/StatusGoQt/test_wallet.cpp | 312 +++++++++++++++--- ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml | 4 +- .../Wallet/views/AssetsDetailView.qml | 46 +-- ui/imports/shared/stores/ChartStoreBase.qml | 54 +-- ui/imports/shared/stores/RootStore.qml | 4 +- .../stores/TokenBalanceHistoryStore.qml | 66 ++-- ui/imports/shared/stores/qmldir | 2 +- vendor/status-go | 2 +- 25 files changed, 603 insertions(+), 254 deletions(-) diff --git a/app/README.md b/app/README.md index 4824221cbf..e6c488aeb6 100644 --- a/app/README.md +++ b/app/README.md @@ -60,8 +60,13 @@ ctest --test-dir ./build ### Build with QtCreator -If go is installed with brew use the following configuration otherwise adapt the configuration. -Go to QtCreator's preferences navigate to Environment -> System -> Environment -> Change and paste +Go to QtCreator's preferences navigate to Environment -> System -> Environment -> Change add GOBIN to the PATH + +### MacOS instructions + +If go is installed with `brew` use the following configuration otherwise adapt it to your environment. Also this will allow access to conan if installed by brew + +Use this in the Environment section of the QtCreator's preferences: ```ini GOBIN=${GOPATH}/bin diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp index 53abffb8ce..806feea1d6 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp @@ -359,6 +359,7 @@ QJsonArray getNodes(const QJsonObject& fleet, const QString& nodeType) return result; } +/// Mirrors and in sync with getDefaultNodeConfig@service.nim QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId) const { try @@ -391,22 +392,14 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId) QJsonObject nodeConfigJson = QJsonDocument::fromJson(nodeConfigJsonStr.toUtf8()).object(); - QJsonObject clusterConfig = nodeConfigJson["ClusterConfig"].toObject(); - QJsonObject fleetsJson = QJsonDocument::fromJson(fleetJson.toUtf8()).object()["fleets"].toObject(); + auto clusterConfig = nodeConfigJson["ClusterConfig"].toObject(); + auto fleetsJson = QJsonDocument::fromJson(fleetJson.toUtf8()).object()["fleets"].toObject(); auto fleet = fleetsJson[Constants::Fleet::Prod].toObject(); clusterConfig["Fleet"] = Constants::Fleet::Prod; clusterConfig["BootNodes"] = getNodes(fleet, Constants::FleetNodes::Bootnodes); clusterConfig["TrustedMailServers"] = getNodes(fleet, Constants::FleetNodes::Mailservers); clusterConfig["StaticNodes"] = getNodes(fleet, Constants::FleetNodes::Whisper); clusterConfig["RendezvousNodes"] = getNodes(fleet, Constants::FleetNodes::Rendezvous); - clusterConfig["DiscV5BootstrapNodes"] = QJsonArray(); - clusterConfig["RelayNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); - clusterConfig["StoreNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); - clusterConfig["FilterNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); - clusterConfig["LightpushNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); - - nodeConfigJson["ClusterConfig"] = clusterConfig; - nodeConfigJson["NetworkId"] = defaultNetworksJson[0].toObject()["chainId"]; nodeConfigJson["DataDir"] = "ethereum"; auto upstreamConfig = nodeConfigJson["UpstreamConfig"].toObject(); @@ -417,6 +410,27 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId) shhextConfig["InstallationID"] = installationId; nodeConfigJson["ShhextConfig"] = shhextConfig; nodeConfigJson["Networks"] = defaultNetworksJson; + nodeConfigJson["NoDiscovery"] = true; + nodeConfigJson["Rendezvous"] = false; + QJsonArray dnsDiscoveryURL = {"enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@prod.nodes.status.im"}; + clusterConfig["WakuNodes"] = dnsDiscoveryURL; + clusterConfig["DiscV5BootstrapNodes"] = dnsDiscoveryURL; + + auto wakuv2Config = nodeConfigJson["WakuV2Config"].toObject(); + wakuv2Config["EnableDiscV5"] = true; + wakuv2Config["DiscoveryLimit"] = 20; + wakuv2Config["Rendezvous"] = true; + wakuv2Config["Enabled"] = true; + + wakuv2Config["Enabled"] = false; + nodeConfigJson["WakuV2Config"] = wakuv2Config; + + clusterConfig["RelayNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); + clusterConfig["StoreNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); + clusterConfig["FilterNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); + clusterConfig["LightpushNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); + + nodeConfigJson["ClusterConfig"] = clusterConfig; nodeConfigJson["KeyStoreDir"] = toQString(fs::relative(m_keyStoreDir, m_statusgoDataDir)); return nodeConfigJson; diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp index 03fd7ff848..004f1d68e9 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp +++ b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp @@ -9,4 +9,9 @@ const EventType Events::RecentHistoryReady{"recent-history-ready"}; const EventType Events::FetchingHistoryError{"fetching-history-error"}; const EventType Events::NonArchivalNodeDetected{"non-archival-node-detected"}; -} // namespace Status::StatusGo::Wallet +const EventType Events::WalletTickReload{"wallet-tick-reload"}; +const EventType Events::EventBalanceHistoryUpdateStarted{"wallet-balance-history-update-started"}; +const EventType Events::EventBalanceHistoryUpdateFinished{"wallet-balance-history-update-finished"}; +const EventType Events::EventBalanceHistoryUpdateFinishedWithError{"wallet-balance-history-update-finished-with-error"}; + +} // namespace Status::StatusGo::Wallet::Transfer diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h index 0efba539b5..536e5bf54a 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h @@ -22,6 +22,11 @@ struct Events static const EventType RecentHistoryReady; static const EventType FetchingHistoryError; static const EventType NonArchivalNodeDetected; + + static const EventType WalletTickReload; + static const EventType EventBalanceHistoryUpdateStarted; + static const EventType EventBalanceHistoryUpdateFinished; + static const EventType EventBalanceHistoryUpdateFinishedWithError; }; /// \see status-go's Event@events.go in services/wallet/transfer module @@ -36,4 +41,4 @@ struct Event NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Event, type, blockNumber, accounts, message); -} // namespace Status +} // namespace Status::StatusGo::Wallet::Transfer diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp index 45b90f15a9..5e917060b9 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp @@ -108,10 +108,12 @@ TokenBalances getTokensBalancesForChainIDs(const std::vector& chainIds, return resultData; } -std::vector -getBalanceHistory(const ChainID& chainID, Accounts::EOAddress account, BalanceHistoryTimeInterval timeInterval) +std::vector getBalanceHistory(const std::vector& chainIds, + Accounts::EOAddress account, + const QString& currency, + BalanceHistoryTimeInterval timeInterval) { - std::vector params = {chainID, account, timeInterval}; + std::vector params = {chainIds, account, currency, timeInterval}; json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getBalanceHistory"}, {"params", params}}; auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); @@ -121,6 +123,18 @@ getBalanceHistory(const ChainID& chainID, Accounts::EOAddress account, BalanceHi return resultJson.get().result; } +bool updateBalanceHistoryForAllEnabledNetworks() +{ + json inputJson = { + {"jsonrpc", "2.0"}, {"method", "wallet_updateBalanceHistoryForAllEnabledNetworks"}, {"params", {}}}; + + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); + + return resultJson.get().result; +} + void checkRecentHistory(const std::vector& accounts) { std::vector params = {accounts}; @@ -130,4 +144,29 @@ void checkRecentHistory(const std::vector& accounts) checkPrivateRpcCallResultAndReportError(resultJson); } +void startWallet() +{ + json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_startWallet"}, {"params", {}}}; + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); +} + +void stopWallet() +{ + json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_stopWallet"}, {"params", {}}}; + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); +} + +void updateVisibleTokens(std::vector symbols) +{ + json inputJson = { + {"jsonrpc", "2.0"}, {"method", "wallet_updateVisibleTokens"}, {"params", std::vector{symbols}}}; + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); +} + } // namespace Status::StatusGo::Wallet diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h index 780df35138..bd111e2b9a 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h @@ -66,9 +66,10 @@ struct TokenBalanceHistory { BigInt value; QDateTime time; + BigInt blockNumber; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TokenBalanceHistory, value, time) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TokenBalanceHistory, value, time, blockNumber) /// @see status-go's services/wallet/transfer/controller.go BalanceHistoryTimeInterval enum BalanceHistoryTimeInterval @@ -86,11 +87,30 @@ enum BalanceHistoryTimeInterval /// \see checkRecentHistory /// \note status-go's API -> GetBalanceHistory@api.go /// \throws \c CallPrivateRpcError -std::vector -getBalanceHistory(const ChainID& chainID, Accounts::EOAddress account, BalanceHistoryTimeInterval timeInterval); +std::vector getBalanceHistory(const std::vector& chainIds, + Accounts::EOAddress account, + const QString& currency, + BalanceHistoryTimeInterval timeInterval); + +/// \note status-go's API -> updateBalanceHistoryForAllEnabledNetworks@api.go +/// +/// \throws \c CallPrivateRpcError +bool updateBalanceHistoryForAllEnabledNetworks(); /// \note status-go's API -> CheckRecentHistory@api.go /// \throws \c CallPrivateRpcError void checkRecentHistory(const std::vector& accounts); +/// \note status-go's API -> StartWallet@api.go +/// \throws \c CallPrivateRpcError +void startWallet(); + +/// \note status-go's API -> StopWallet@api.go +/// \throws \c CallPrivateRpcError +void stopWallet(); + +/// \note status-go's API -> UpdateVisibleTokens@api.go +/// \throws \c CallPrivateRpcError +void updateVisibleTokens(std::vector symbols); + } // namespace Status::StatusGo::Wallet diff --git a/resources/default-networks.json b/resources/default-networks.json index 6ec815dd63..e17402a15a 100644 --- a/resources/default-networks.json +++ b/resources/default-networks.json @@ -1,7 +1,7 @@ [ { "chainId": 1, - "chainName": "Mainnet", + "chainName": "Ethereum Mainnet", "rpcUrl": "https://mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%", "blockExplorerUrl": "https://etherscan.io/", "iconUrl": "network/Network=Ethereum", @@ -15,49 +15,19 @@ "enabled": true }, { - "chainId": 3, - "chainName": "Ropsten", - "rpcUrl": "https://ropsten.infura.io/v3%INFURA_TOKEN_RESOLVED%", - "blockExplorerUrl": "https://ropsten.etherscan.io/", - "iconUrl": "network/Network=Tetnet", - "chainColor": "#939BA1", - "shortName": "ropEth", - "nativeCurrencyName": "Ether", - "nativeCurrencySymbol": "ETH", - "nativeCurrencyDecimals": 18, - "isTest": true, - "layer": 1, - "enabled": false - }, - { - "chainId": 4, - "chainName": "Rinkeby", - "rpcUrl": "https://rinkeby.infura.io/v3/%INFURA_TOKEN_RESOLVED%", - "blockExplorerUrl": "https://rinkeby.etherscan.io/", - "iconUrl": "network/Network=Tetnet", - "chainColor": "#939BA1", - "shortName": "rinEth", - "nativeCurrencyName": "Ether", - "nativeCurrencySymbol": "ETH", - "nativeCurrencyDecimals": 18, - "isTest": true, - "layer": 1, - "enabled": false - }, - { - "chainId": 5, - "chainName": "Goerli", - "rpcUrl": "http://goerli.blockscout.com/", - "blockExplorerUrl": "https://goerli.etherscan.io/", - "iconUrl": "network/Network=Tetnet", - "chainColor": "#939BA1", - "shortName": "goeEth", - "nativeCurrencyName": "Ether", - "nativeCurrencySymbol": "ETH", - "nativeCurrencyDecimals": 18, - "isTest": true, - "layer": 1, - "enabled": true + "chainId": 5, + "chainName": "Goerli", + "rpcUrl": "https://goerli.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://goerli.etherscan.io/", + "iconUrl": "network/Network=Testnet", + "chainColor": "#939BA1", + "shortName": "goeEth", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": true, + "layer": 1, + "enabled": true }, { "chainId": 10, @@ -75,13 +45,13 @@ "enabled": true }, { - "chainId": 69, - "chainName": "Optimism Kovan", - "rpcUrl": "https://optimism-kovan.infura.io/v3/%INFURA_TOKEN_RESOLVED%", - "blockExplorerUrl": "https://kovan-optimistic.etherscan.io", - "iconUrl": "network/Network=Tetnet", + "chainId": 420, + "chainName": "Optimism Goerli Testnet", + "rpcUrl": "https://optimism-goerli.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://goerli-optimism.etherscan.io/", + "iconUrl": "network/Network=Testnet", "chainColor": "#939BA1", - "shortName": "kovOpt", + "shortName": "goerOpt", "nativeCurrencyName": "Ether", "nativeCurrencySymbol": "ETH", "nativeCurrencyDecimals": 18, @@ -105,11 +75,11 @@ "enabled": true }, { - "chainId": 421611, - "chainName": "Arbitrum Rinkeby", - "rpcUrl": "https://arbitrum-rinkeby.infura.io/v3/%INFURA_TOKEN_RESOLVED%", - "blockExplorerUrl": " https://testnet.arbiscan.io", - "iconUrl": "network/Network=Tetnet", + "chainId": 421613, + "chainName": "Arbitrum Goerli", + "rpcUrl": "https://arbitrum-goerli.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://goerli.arbiscan.io/", + "iconUrl": "network/Network=Testnet", "chainColor": "#939BA1", "shortName": "rinArb", "nativeCurrencyName": "Ether", @@ -119,4 +89,4 @@ "layer": 2, "enabled": false } -] \ No newline at end of file +] diff --git a/resources/fleets.json b/resources/fleets.json index c31a3362a8..1605307bce 100644 --- a/resources/fleets.json +++ b/resources/fleets.json @@ -111,6 +111,35 @@ "node-01.gc-us-central1-a.go-waku.test": "/dns4/node-01.gc-us-central1-a.go-waku.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPz63Xc6AuVkDeujz7YeZta18rcdau3Y1BzaxKAfDrBqz" } }, + "status.prod": { + "tcp/p2p/waku": { + "node-01.ac-cn-hongkong-c.status.prod": "/dns4/node-01.ac-cn-hongkong-c.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAkvEZgh3KLwhLwXg95e5ojM8XykJ4Kxi2T7hk22rnA7pJC", + "node-01.do-ams3.status.prod": "/dns4/node-01.do-ams3.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAm6HZZr7aToTvEBPpiys4UxajCTU97zj5v7RNR2gbniy1D", + "node-01.gc-us-central1-a.status.prod": "/dns4/node-01.gc-us-central1-a.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAkwBp8T6G77kQXSNMnxgaMky1JeyML5yqoTHRM8dbeCBNb", + "node-02.ac-cn-hongkong-c.status.prod": "/dns4/node-02.ac-cn-hongkong-c.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmFy8BrJhCEmCYrUfBdSNkrPw6VHExtv4rRp1DSBnCPgx8", + "node-02.do-ams3.status.prod": "/dns4/node-02.do-ams3.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmSve7tR5YZugpskMv2dmJAsMUKmfWYEKRXNUxRaTCnsXV", + "node-02.gc-us-central1-a.status.prod": "/dns4/node-02.gc-us-central1-a.status.prod.statusim.net/tcp/30303/p2p/16Uiu2HAmDQugwDHM3YeUp86iGjrUvbdw3JPRgikC7YoGBsT2ymMg" + } + }, + "status.test": { + "tcp/p2p/waku": { + "node-01.ac-cn-hongkong-c.status.test": "/dns4/node-01.ac-cn-hongkong-c.status.test.statusim.net/tcp/30303/p2p/16Uiu2HAm2BjXxCp1sYFJQKpLLbPbwd5juxbsYofu3TsS3auvT9Yi", + "node-01.do-ams3.status.test": "/dns4/node-01.do-ams3.status.test.statusim.net/tcp/30303/p2p/16Uiu2HAkukebeXjTQ9QDBeNDWuGfbaSg79wkkhK4vPocLgR6QFDf", + "node-01.gc-us-central1-a.status.test": "/dns4/node-01.gc-us-central1-a.status.test.statusim.net/tcp/30303/p2p/16Uiu2HAmGDX3iAFox93PupVYaHa88kULGqMpJ7AEHGwj3jbMtt76" + } + }, + "waku.connect": { + "tcp/p2p/waku": { + "nim-01.ac-cn-hongkong-c.waku.connect": "/ip4/47.242.185.35/tcp/30303/p2p/16Uiu2HAm75XUMGev2Ti74G3wUzhyxCtbaDKVWzNwbq3tn5WfzRd4", + "nim-01.do-ams3.waku.connect": "/ip4/206.189.242.0/tcp/30303/p2p/16Uiu2HAm9VLETt1xBwDAwfKxj2XvAZDw73Bn4HQf11U26JGDxqZD", + "nim-01.gc-us-central1-a.waku.connect": "/ip4/35.193.87.35/tcp/30303/p2p/16Uiu2HAmMi8xaj9W22a67shGg5wtw1nZDNtfrTPHkgKA5Uhvnvbn" + }, + "wss/p2p/waku": { + "nim-01.ac-cn-hongkong-c.waku.connect": "/dns4/nim-01.ac-cn-hongkong-c.waku.connect.statusim.net/tcp/443/wss/p2p/16Uiu2HAm75XUMGev2Ti74G3wUzhyxCtbaDKVWzNwbq3tn5WfzRd4", + "nim-01.do-ams3.waku.connect": "/dns4/nim-01.do-ams3.waku.connect.statusim.net/tcp/443/wss/p2p/16Uiu2HAm9VLETt1xBwDAwfKxj2XvAZDw73Bn4HQf11U26JGDxqZD", + "nim-01.gc-us-central1-a.waku.connect": "/dns4/nim-01.gc-us-central1-a.waku.connect.statusim.net/tcp/443/wss/p2p/16Uiu2HAmMi8xaj9W22a67shGg5wtw1nZDNtfrTPHkgKA5Uhvnvbn" + } + }, "wakuv2.prod": { "waku": { "node-01.ac-cn-hongkong-c.wakuv2.prod": "/ip4/8.210.222.231/tcp/30303/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrD", @@ -125,19 +154,19 @@ }, "wakuv2.test": { "waku": { - "node-01.ac-cn-hongkong-c.wakuv2.test": "/ip4/47.242.210.73/tcp/30303/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm", - "node-01.do-ams3.wakuv2.test": "/ip4/134.209.139.210/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ", - "node-01.gc-us-central1-a.wakuv2.test": "/ip4/104.154.239.128/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS" + "node-01.ac-cn-hongkong-c.wakuv2.test": "/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm", + "node-01.do-ams3.wakuv2.test": "/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ", + "node-01.gc-us-central1-a.wakuv2.test": "/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS" }, "waku-websocket": { - "node-01.ac-cn-hongkong-c.wakuv2.test": "/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm", - "node-01.do-ams3.wakuv2.test": "/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ", - "node-01.gc-us-central1-a.wakuv2.test": "/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS" + "node-01.ac-cn-hongkong-c.wakuv2.test": "/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm", + "node-01.do-ams3.wakuv2.test": "/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ", + "node-01.gc-us-central1-a.wakuv2.test": "/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS" } } }, "meta": { "hostname": "node-01.do-ams3.sites.misc", - "timestamp": "2021-10-19T00:00:15.465044" + "timestamp": "2022-03-10T11:32:40.427357", } } diff --git a/resources/node-config.json b/resources/node-config.json index 46a82d22dd..7495d04911 100644 --- a/resources/node-config.json +++ b/resources/node-config.json @@ -10,7 +10,7 @@ "KeyStoreDir": "./keystore", "IPFSDir": "./ipfs", "LogEnabled": true, - "LogFile": "./geth.log", + "LogFile": "geth.log", "LogLevel": "INFO", "MailserversConfig": { "Enabled": true @@ -76,9 +76,6 @@ "Enabled": true, "OpenseaAPIKey": "" }, - "EnsConfig": { - "Enabled": true - }, "Networks": [], "TorrentConfig": { "Enabled": false, diff --git a/src/app/modules/main/wallet_section/all_tokens/controller.nim b/src/app/modules/main/wallet_section/all_tokens/controller.nim index 37f9e95704..614771c1a9 100644 --- a/src/app/modules/main/wallet_section/all_tokens/controller.nim +++ b/src/app/modules/main/wallet_section/all_tokens/controller.nim @@ -33,11 +33,15 @@ proc init*(self: Controller) = let args = TokenHistoricalDataArgs(e) self.delegate.tokenHistoricalDataResolved(args.result) + self.events.on(SIGNAL_BALANCE_HISTORY_DATA_READY) do(e:Args): + let args = TokenBalanceHistoryDataArgs(e) + self.delegate.tokenBalanceHistoryDataResolved(args.result) + method findTokenSymbolByAddress*(self: Controller, address: string): string = return self.walletAccountService.findTokenSymbolByAddress(address) method getHistoricalDataForToken*(self: Controller, symbol: string, currency: string, range: int) = self.tokenService.getHistoricalDataForToken(symbol, currency, range) -method fetchHistoricalBalanceForTokenAsJson*(self: Controller, address: string, symbol: string, timeIntervalEnum: int) = - self.tokenService.fetchHistoricalBalanceForTokenAsJson(address, symbol, BalanceHistoryTimeInterval(timeIntervalEnum)) \ No newline at end of file +method fetchHistoricalBalanceForTokenAsJson*(self: Controller, address: string, tokenSymbol: string, currencySymbol: string, timeIntervalEnum: int) = + self.tokenService.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, BalanceHistoryTimeInterval(timeIntervalEnum)) \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/all_tokens/io_interface.nim b/src/app/modules/main/wallet_section/all_tokens/io_interface.nim index b700b95854..88689706bc 100644 --- a/src/app/modules/main/wallet_section/all_tokens/io_interface.nim +++ b/src/app/modules/main/wallet_section/all_tokens/io_interface.nim @@ -20,7 +20,7 @@ method getHistoricalDataForToken*(self: AccessInterface, symbol: string, currenc method tokenHistoricalDataResolved*(self: AccessInterface, tokenDetails: string) {.base.} = raise newException(ValueError, "No implementation available") -method fetchHistoricalBalanceForTokenAsJson*(self: AccessInterface, address: string, symbol: string, timeIntervalEnum: int) {.base.} = +method fetchHistoricalBalanceForTokenAsJson*(self: AccessInterface, address: string, tokenSymbol: string, currencySymbol: string, timeIntervalEnum: int) {.base.} = raise newException(ValueError, "No implementation available") method tokenBalanceHistoryDataResolved*(self: AccessInterface, balanceHistoryJson: string) {.base.} = diff --git a/src/app/modules/main/wallet_section/all_tokens/module.nim b/src/app/modules/main/wallet_section/all_tokens/module.nim index 5436936c23..000f02f4c3 100644 --- a/src/app/modules/main/wallet_section/all_tokens/module.nim +++ b/src/app/modules/main/wallet_section/all_tokens/module.nim @@ -63,8 +63,8 @@ method tokenHistoricalDataResolved*(self: Module, tokenDetails: string) = self.view.setTokenHistoricalDataReady(tokenDetails) -method fetchHistoricalBalanceForTokenAsJson*(self: Module, address: string, symbol: string, timeIntervalEnum: int) = - self.controller.fetchHistoricalBalanceForTokenAsJson(address, symbol, timeIntervalEnum) +method fetchHistoricalBalanceForTokenAsJson*(self: Module, address: string, tokenSymbol: string, currencySymbol: string, timeIntervalEnum: int) = + self.controller.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol,timeIntervalEnum) method tokenBalanceHistoryDataResolved*(self: Module, balanceHistoryJson: string) = self.view.setTokenBalanceHistoryDataReady(balanceHistoryJson) diff --git a/src/app/modules/main/wallet_section/all_tokens/view.nim b/src/app/modules/main/wallet_section/all_tokens/view.nim index 2c77b243c8..1e035a1145 100644 --- a/src/app/modules/main/wallet_section/all_tokens/view.nim +++ b/src/app/modules/main/wallet_section/all_tokens/view.nim @@ -65,9 +65,9 @@ QtObject: self.setMarketHistoryIsLoading(false) self.tokenHistoricalDataReady(tokenDetails) - proc fetchHistoricalBalanceForTokenAsJson*(self: View, address: string, symbol: string, timeIntervalEnum: int) {.slot.} = + proc fetchHistoricalBalanceForTokenAsJson*(self: View, address: string, tokenSymbol: string, currencySymbol: string, timeIntervalEnum: int) {.slot.} = self.setBalanceHistoryIsLoading(true) - self.delegate.fetchHistoricalBalanceForTokenAsJson(address, symbol, timeIntervalEnum) + self.delegate.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, timeIntervalEnum) proc tokenBalanceHistoryDataReady*(self: View, balanceHistoryJson: string) {.signal.} diff --git a/src/app_service/service/token/async_tasks.nim b/src/app_service/service/token/async_tasks.nim index 1c03ff05d9..b5d0091d84 100644 --- a/src/app_service/service/token/async_tasks.nim +++ b/src/app_service/service/token/async_tasks.nim @@ -66,9 +66,10 @@ type type GetTokenBalanceHistoryDataTaskArg = ref object of QObjectTaskArg - chainId: int + chainIds: seq[int] address: string - symbol: string + tokenSymbol: string + currencySymbol: string timeInterval: BalanceHistoryTimeInterval const getTokenBalanceHistoryDataTask*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = @@ -76,12 +77,13 @@ const getTokenBalanceHistoryDataTask*: Task = proc(argEncoded: string) {.gcsafe, var response = %*{} try: # status-go time intervals are starting from 1 - response = backend.getBalanceHistory(arg.chainId, arg.address, int(arg.timeInterval) + 1).result + response = backend.getBalanceHistory(arg.chainIds, arg.address, arg.tokenSymbol, arg.currencySymbol, int(arg.timeInterval) + 1).result let output = %* { - "chainId": arg.chainId, + "chainIds": arg.chainIds, "address": arg.address, - "symbol": arg.symbol, + "tokenSymbol": arg.tokenSymbol, + "currencySymbol": arg.currencySymbol, "timeInterval": int(arg.timeInterval), "historicalData": response } @@ -90,10 +92,11 @@ const getTokenBalanceHistoryDataTask*: Task = proc(argEncoded: string) {.gcsafe, return except Exception as e: let output = %* { - "chainId": arg.chainId, + "chainIds": arg.chainIds, "address": arg.address, - "symbol": arg.symbol, + "tokenSymbol": arg.tokenSymbol, + "currencySymbol": arg.currencySymbol, "timeInterval": int(arg.timeInterval), - "error": "Balance history value not found", + "error": e.msg, } arg.finish(output) \ No newline at end of file diff --git a/src/app_service/service/token/service.nim b/src/app_service/service/token/service.nim index f8096dbbd7..c68eaff3de 100644 --- a/src/app_service/service/token/service.nim +++ b/src/app_service/service/token/service.nim @@ -117,7 +117,7 @@ QtObject: return token except Exception as e: error "Error finding token by symbol", msg = e.msg - + proc findTokenByAddress*(self: Service, network: NetworkDto, address: Address): TokenDto = for token in self.tokens[network.chainId]: if token.address == address: @@ -242,31 +242,43 @@ QtObject: ) self.threadpool.start(arg) -# Historical Balance + # Callback to process the response of fetchHistoricalBalanceForTokenAsJson call proc tokenBalanceHistoryDataResolved*(self: Service, response: string) {.slot.} = - # TODO let responseObj = response.parseJson if (responseObj.kind != JObject): - info "blance history response is not a json object" + warn "blance history response is not a json object" return self.events.emit(SIGNAL_BALANCE_HISTORY_DATA_READY, TokenBalanceHistoryDataArgs( result: response )) - proc fetchHistoricalBalanceForTokenAsJson*(self: Service, address: string, symbol: string, timeInterval: BalanceHistoryTimeInterval) = + proc fetchHistoricalBalanceForTokenAsJson*(self: Service, address: string, tokenSymbol: string, currencySymbol: string, timeInterval: BalanceHistoryTimeInterval) = + # create an empty list of chain ids + var chainIds: seq[int] = @[] let networks = self.networkService.getNetworks() for network in networks: - if network.enabled and network.nativeCurrencySymbol == symbol: - let arg = GetTokenBalanceHistoryDataTaskArg( - tptr: cast[ByteAddress](getTokenBalanceHistoryDataTask), - vptr: cast[ByteAddress](self.vptr), - slot: "tokenBalanceHistoryDataResolved", - chainId: network.chainId, - address: address, - symbol: symbol, - timeInterval: timeInterval - ) - self.threadpool.start(arg) - return - error "faild to find a network with the symbol", symbol \ No newline at end of file + if network.enabled: + if network.nativeCurrencySymbol == tokenSymbol: + chainIds.add(network.chainId) + else: + for token in self.tokens[network.chainId]: + if token.symbol == tokenSymbol: + chainIds.add(network.chainId) + + if chainIds.len == 0: + error "faild to find a network with the symbol", tokenSymbol + return + + let arg = GetTokenBalanceHistoryDataTaskArg( + tptr: cast[ByteAddress](getTokenBalanceHistoryDataTask), + vptr: cast[ByteAddress](self.vptr), + slot: "tokenBalanceHistoryDataResolved", + chainIds: chainIds, + address: address, + tokenSymbol: tokenSymbol, + currencySymbol: currencySymbol, + timeInterval: timeInterval + ) + self.threadpool.start(arg) + return \ No newline at end of file diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim index adfe2ba5bc..108d20b76a 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -75,8 +75,6 @@ type AccountDeleted* = ref object of Args type CurrencyUpdated = ref object of Args -type TokenVisibilityToggled = ref object of Args - type NetwordkEnabledToggled = ref object of Args type WalletAccountUpdated* = ref object of Args @@ -237,7 +235,7 @@ QtObject: self.checkRecentHistory() of "wallet-tick-check-connected": self.checkConnected() - + proc getWalletAccount*(self: Service, accountIndex: int): WalletAccountDto = let accounts = self.getWalletAccounts() if accountIndex < 0 or accountIndex >= accounts.len: @@ -259,7 +257,7 @@ QtObject: proc checkConnected(self: Service) = if(not singletonInstance.localAccountSensitiveSettings.getIsWalletEnabled()): return - + try: # TODO: add event for UI (Waiting for design) discard backend.checkConnected() @@ -272,7 +270,7 @@ QtObject: proc checkRecentHistory*(self: Service) = if(not singletonInstance.localAccountSensitiveSettings.getIsWalletEnabled()): return - + try: let addresses = self.getWalletAccounts().map(a => a.address) let chainIds = self.networkService.getNetworks().map(a => a.chainId) @@ -523,6 +521,9 @@ QtObject: proc onAllTokensBuilt*(self: Service, response: string) {.slot.} = try: + var visibleSymbols: seq[string] + let chainIds = self.networkService.getNetworks().map(n => n.chainId) + let responseObj = response.parseJson var data = TokensPerAccountArgs() if responseObj.kind == JObject: @@ -534,7 +535,13 @@ QtObject: data.accountsTokens[wAddress] = tokens self.storeTokensForAccount(wAddress, tokens) self.tokenService.updateTokenPrices(tokens) # For efficiency. Will be removed when token info fetching gets moved to the tokenService + # Gather symbol for visible tokens + for token in tokens: + if token.getVisibleForNetworkWithPositiveBalance(chainIds) and find(visibleSymbols, token.symbol) == -1: + visibleSymbols.add(token.symbol) self.events.emit(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT, data) + + discard backend.updateVisibleTokens(visibleSymbols) except Exception as e: error "error: ", procName="onAllTokensBuilt", errName = e.name, errDesription = e.msg diff --git a/src/backend/backend.nim b/src/backend/backend.nim index bfc78579e9..4c2318bc9d 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -97,6 +97,9 @@ rpc(getWalletToken, "wallet"): rpc(startWallet, "wallet"): discard +rpc(updateVisibleTokens, "wallet"): + symbols: seq[string] + rpc(getTransactionEstimatedTime, "wallet"): chainId: int maxFeePerGas: float @@ -288,8 +291,10 @@ rpc(getName, "ens"): address: string rpc(getBalanceHistory, "wallet"): - chainId: int + chainIds: seq[int] address: string + tokenSymbol: string + currencySymbol: string timeInterval: int rpc(isCurrencyFiat, "wallet"): diff --git a/test/libs/StatusGoQt/test_wallet.cpp b/test/libs/StatusGoQt/test_wallet.cpp index 9b460f1b65..2293c4f4e3 100644 --- a/test/libs/StatusGoQt/test_wallet.cpp +++ b/test/libs/StatusGoQt/test_wallet.cpp @@ -207,7 +207,7 @@ TEST(WalletApi, TestGetTokens) auto networks = Wallet::getEthereumChains(false); ASSERT_GT(networks.size(), 0); auto mainNetIt = - std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; }); + std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Ethereum Mainnet"; }); ASSERT_NE(mainNetIt, networks.end()); const auto& mainNet = *mainNetIt; @@ -228,7 +228,7 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs) ASSERT_GT(networks.size(), 1); auto mainNetIt = - std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; }); + std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Ethereum Mainnet"; }); ASSERT_NE(mainNetIt, networks.end()); const auto& mainNet = *mainNetIt; @@ -265,22 +265,69 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs) ASSERT_EQ(toQString(addressBalance.at(sntTest.address)), "0"); } +struct TestNetwork +{ + QString name; + bool isTest; +}; + +struct TestParams +{ + Accounts::EOAddress walletAddress; + std::vector networks; + QString token; + QString newTestAccountName; +}; + +static const std::vector allTestParams{ + // [0] - Goerli ERC20 token test account set + TestParams{Accounts::EOAddress("0x586e3bd2c40b0f243162ea563a1f43ae9ef25ef9"), + {{QString("Goerli"), true}}, + QString("USDC"), + u"test_watch_only-name"_qs}, + // [1] - Main net ERC20 token test account set + TestParams{Accounts::EOAddress("0xae0c364acb9b105766fea91cfa5aaea31a1821c1"), + {{QString("Ethereum Mainnet"), false}}, + QString("SNT"), + u"test_watch_only-name"_qs}, + // [2] - Main net native token account set + TestParams{Accounts::EOAddress("0x0182e671dfd5f7d21b13714cbe9f92d26b59eeb9"), + {{QString("Ethereum Mainnet"), false}}, + QString("USDC"), + u"test_watch_only-name"_qs}, + // [3] - Main net ERC20 test account set + TestParams{Accounts::EOAddress("0x473780deAF4a2Ac070BBbA936B0cdefe7F267dFc"), + {{QString("Ethereum Mainnet"), false}}, + QString("ETH"), + u"test_watch_only-name"_qs}, + // [4] - Arbitrum Goerli native token test account set + TestParams{Accounts::EOAddress("0xE2d622C817878dA5143bBE06866ca8E35273Ba8a"), + {{QString("Arbitrum Goerli"), true}, {QString("Goerli"), true}}, + QString("ETH"), + u"test_watch_only-name"_qs}, +}; + +TestParams getTestParams() +{ + return allTestParams[4]; +} + TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount) { ScopedTestAccount testAccount(test_info_->name()); - const auto newTestAccountName = u"test_watch_only-name"_qs; + const auto params = getTestParams(); + Accounts::addAccountWatch(Accounts::EOAddress("0xdb5ac1a559b02e12f29fc0ec0e37be8e046def49"), - newTestAccountName, + params.newTestAccountName, QColor("fuchsia"), u""_qs); const auto updatedAccounts = Accounts::getAccounts(); ASSERT_EQ(updatedAccounts.size(), 3); - const auto newAccountIt = - std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [&newTestAccountName](const auto& a) { - return a.name == newTestAccountName; - }); + const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [¶ms](const auto& a) { + return a.name == params.newTestAccountName; + }); ASSERT_NE(newAccountIt, updatedAccounts.end()); const auto& newAccount = *newAccountIt; @@ -288,7 +335,7 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount) ASSERT_GT(networks.size(), 1); auto mainNetIt = - std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; }); + std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Ethereum Mainnet"; }); ASSERT_NE(mainNetIt, networks.end()); const auto& mainNet = *mainNetIt; @@ -309,24 +356,24 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount) ASSERT_GT(addressBalance.at(sntMain.address), 0); } -// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test +// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable integration test TEST(WalletApi, TestCheckRecentHistory) { ScopedTestAccount testAccount(test_info_->name()); + const auto params = getTestParams(); + // Add watch account - const auto newTestAccountName = u"test_watch_only-name"_qs; Accounts::addAccountWatch(Accounts::EOAddress("0xe74E17D586227691Cb7b64ed78b1b7B14828B034"), - newTestAccountName, + params.newTestAccountName, QColor("fuchsia"), u""_qs); const auto updatedAccounts = Accounts::getAccounts(); ASSERT_EQ(updatedAccounts.size(), 3); - const auto newAccountIt = - std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [newTestAccountName](const auto& a) { - return a.name == newTestAccountName; - }); + const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [¶ms](const auto& a) { + return a.name == params.newTestAccountName; + }); ASSERT_NE(newAccountIt, updatedAccounts.end()); const auto& newAccount = *newAccountIt; @@ -352,33 +399,44 @@ TEST(WalletApi, TestCheckRecentHistory) ASSERT_TRUE(historyReady); } -// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test +// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable integration test TEST(WalletApi, TestGetBalanceHistory) { ScopedTestAccount testAccount(test_info_->name()); - // Add watch account - const auto newTestAccountName = u"test_watch_only-name"_qs; - Accounts::addAccountWatch(Accounts::EOAddress("0x473780deAF4a2Ac070BBbA936B0cdefe7F267dFc"), - newTestAccountName, - QColor("fuchsia"), - u""_qs); + const auto params = getTestParams(); + + Accounts::addAccountWatch(params.walletAddress, params.newTestAccountName, QColor("fuchsia"), u""_qs); const auto updatedAccounts = Accounts::getAccounts(); ASSERT_EQ(updatedAccounts.size(), 3); auto networks = Wallet::getEthereumChains(false); ASSERT_GT(networks.size(), 0); - auto mainNetIt = - std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; }); - ASSERT_NE(mainNetIt, networks.end()); - const auto& mainNet = *mainNetIt; - auto tokens = Wallet::getTokens(mainNet.chainId); - - const auto newAccountIt = - std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [newTestAccountName](const auto& a) { - return a.name == newTestAccountName; + std::vector chainIDs; + for(const auto& net : params.networks) + { + auto netIt = std::find_if(networks.begin(), networks.end(), [&net](const auto& n) { + return n.chainName == net.name && n.isTest == net.isTest; }); + ASSERT_NE(netIt, networks.end()); + chainIDs.push_back(netIt->chainId); + + auto nativeIt = std::find_if(networks.begin(), networks.end(), [¶ms](const auto& n) { + return n.nativeCurrencySymbol == params.token; + }); + if(nativeIt == networks.end()) + { + auto tokens = Wallet::getTokens(netIt->chainId); + auto tokenIt = std::find_if( + tokens.begin(), tokens.end(), [¶ms](const auto& t) { return t.symbol == params.token; }); + ASSERT_NE(tokenIt, tokens.end()); + } + } + + const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [¶ms](const auto& a) { + return a.name == params.newTestAccountName; + }); ASSERT_NE(newAccountIt, updatedAccounts.end()); const auto& newAccount = *newAccountIt; @@ -395,11 +453,59 @@ TEST(WalletApi, TestGetBalanceHistory) {Wallet::BalanceHistoryTimeInterval::BalanceHistory1Year, "1Y"}, {Wallet::BalanceHistoryTimeInterval::BalanceHistoryAllTime, "All"}}; + // Fetch first and watch for finished signal of newAccount.address + int bhStartedReceivedCount = 0; + int bhEndedReceivedCount = 0; + int bhErrorReceivedCount = 0; + bool targetAccountDone = false; + QObject::connect( + StatusGo::SignalsManager::instance(), + &StatusGo::SignalsManager::wallet, + testAccount.app(), + [&targetAccountDone, &bhStartedReceivedCount, &bhEndedReceivedCount, &bhErrorReceivedCount, &newAccount]( + QSharedPointer data) { + Wallet::Transfer::Event event = data->eventInfo(); + if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateStarted) + { + bhStartedReceivedCount++; + } + else if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateFinished) + { + bhEndedReceivedCount++; + if(event.accounts) + { + auto found = std::find_if(event.accounts.value().begin(), + event.accounts.value().end(), + [&event, &newAccount](const auto& a) { return a == newAccount.address; }); + if(found != event.accounts.value().end()) + { + targetAccountDone = true; + } + } + } + else if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateFinishedWithError) + { + bhErrorReceivedCount++; + } + }); + + std::vector symbols{params.token}; + Wallet::updateVisibleTokens(symbols); + + Wallet::startWallet(); + + testAccount.processMessages(2400000, [&targetAccountDone, &bhEndedReceivedCount, &bhErrorReceivedCount]() { + return !targetAccountDone && bhEndedReceivedCount < 2 && bhErrorReceivedCount < 1; + }); + ASSERT_GT(bhStartedReceivedCount, 0); + ASSERT_EQ(bhErrorReceivedCount, 0); + ASSERT_GT(bhEndedReceivedCount, 0); + ASSERT_TRUE(targetAccountDone); + for(const auto& historyInterval : testIntervals) { - // TODO: next `mainNet.nativeCurrencySymbol`, later `tokens.symbol` - auto balanceHistory = Wallet::getBalanceHistory(mainNet.chainId, newAccount.address, historyInterval); - ASSERT_TRUE(balanceHistory.size() > 0); + auto balanceHistory = Wallet::getBalanceHistory(chainIDs, newAccount.address, params.token, historyInterval); + ASSERT_GT(balanceHistory.size(), 0); auto weiToEth = [](const StatusGo::Wallet::BigInt& wei) -> double { StatusGo::Wallet::BigInt q; // wei / eth @@ -413,19 +519,145 @@ TEST(WalletApi, TestGetBalanceHistory) return q.convert_to() + (qSzabos.convert_to() / ((weiD / szaboD).convert_to())); }; - QFile file(QString("/tmp/balance_history-%s.csv").arg(testIntervalsStrs[historyInterval])); + auto fileInfo = QFileInfo( + QString("/tmp/StatusTests/balance/balance_history-%1.csv").arg(testIntervalsStrs[historyInterval])); + QFile file(fileInfo.absoluteFilePath()); + QDir().mkpath(fileInfo.absolutePath()); if(file.open(QIODevice::WriteOnly | QIODevice::Text)) { QTextStream out(&file); - out << "Balance, Timestamp" << Qt::endl; + out << "Balance, Timestamp, Block Number, Formatted Date" << Qt::endl; for(int i = 0; i < balanceHistory.size(); ++i) { - out << weiToEth(balanceHistory[i].value) << "," << balanceHistory[i].time.toSecsSinceEpoch() - << Qt::endl; + out << weiToEth(balanceHistory[i].value) << "," << balanceHistory[i].time.toSecsSinceEpoch() << "," + << balanceHistory[i].blockNumber.str().c_str() << "," + << balanceHistory[i].time.toString("dd.MM.yyyy hh:mm:ss") << Qt::endl; } + file.close(); } - file.close(); } } +// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable integration test +TEST(WalletApi, TestStartWallet) +{ + ScopedTestAccount testAccount(test_info_->name()); + + auto params = getTestParams(); + + // Add watch account + Accounts::addAccountWatch(Accounts::EOAddress("0xF38D2CD3C6Ad02dD6f8B68E0A7b2f959819954b6"), + params.newTestAccountName, + QColor("fuchsia"), + u""_qs); + const auto updatedAccounts = Accounts::getAccounts(); + ASSERT_EQ(updatedAccounts.size(), 3); + + int bhStartedReceivedCount = 0; + int bhEndedReceivedCount = 0; + int bhErrorReceivedCount = 0; + QObject::connect(StatusGo::SignalsManager::instance(), + &StatusGo::SignalsManager::wallet, + testAccount.app(), + [&bhStartedReceivedCount, &bhEndedReceivedCount, &bhErrorReceivedCount]( + QSharedPointer data) { + Wallet::Transfer::Event event = data->eventInfo(); + if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateStarted) + { + bhStartedReceivedCount++; + } + else if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateFinished) + { + bhEndedReceivedCount++; + } + else if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateFinishedWithError) + { + bhErrorReceivedCount++; + } + }); + + std::vector symbols{params.token}; + Wallet::updateVisibleTokens(symbols); + + Wallet::startWallet(); + + testAccount.processMessages(240000, [&bhEndedReceivedCount, &bhErrorReceivedCount]() { + return bhEndedReceivedCount < 2 && bhErrorReceivedCount < 1; + }); + ASSERT_EQ(bhStartedReceivedCount, 2); + ASSERT_EQ(bhErrorReceivedCount, 0); + ASSERT_EQ(bhEndedReceivedCount, 2); +} + +// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable integration test +TEST(WalletApi, TestStopBalanceHistory) +{ + ScopedTestAccount testAccount(test_info_->name()); + + const auto updatedAccounts = Accounts::getAccounts(); + ASSERT_EQ(updatedAccounts.size(), 2); + + int bhStartedReceivedCount = 0; + int bhEndedReceivedCount = 0; + int bhErrorReceivedCount = 0; + QString stopMessage; + QObject::connect(StatusGo::SignalsManager::instance(), + &StatusGo::SignalsManager::wallet, + testAccount.app(), + [&stopMessage, &bhStartedReceivedCount, &bhEndedReceivedCount, &bhErrorReceivedCount]( + QSharedPointer data) { + Wallet::Transfer::Event event = data->eventInfo(); + if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateStarted) + { + bhStartedReceivedCount++; + } + else if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateFinished) + { + stopMessage = event.message; + bhEndedReceivedCount++; + } + else if(event.type == Wallet::Transfer::Events::EventBalanceHistoryUpdateFinishedWithError) + { + bhErrorReceivedCount++; + } + }); + Wallet::updateVisibleTokens({QString("ETH")}); + + Wallet::startWallet(); + + bool stopCalled = false; + testAccount.processMessages(5000, + [&stopCalled, &bhStartedReceivedCount, &bhEndedReceivedCount, &bhErrorReceivedCount]() { + if(bhStartedReceivedCount == 1 && !stopCalled) + { + stopCalled = true; + std::this_thread::sleep_for(std::chrono::seconds(1)); + Wallet::stopWallet(); + } + return bhEndedReceivedCount < 1 && bhErrorReceivedCount < 1; + }); + ASSERT_EQ(bhStartedReceivedCount, 1); + ASSERT_EQ(bhErrorReceivedCount, 0); + ASSERT_EQ(bhEndedReceivedCount, 1); + ASSERT_EQ("Service canceled", stopMessage); + + stopMessage = ""; + + // Do an empty run + Wallet::updateVisibleTokens({}); + + Wallet::stopWallet(); + + stopCalled = false; + testAccount.processMessages(1000, + [&stopCalled, &bhStartedReceivedCount, &bhEndedReceivedCount, &bhErrorReceivedCount]() { + return bhEndedReceivedCount < 2 && bhErrorReceivedCount < 2; + }); + + ASSERT_EQ(bhStartedReceivedCount, 2); + ASSERT_EQ(bhErrorReceivedCount, 0); + ASSERT_EQ(bhEndedReceivedCount, 2); + ASSERT_EQ("", stopMessage); +} + } // namespace Status::Testing diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index 4efb20920a..48caeca301 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -10,7 +10,9 @@ QtObject { if (Number.isInteger(num)) return 0 - return num.toString().split('.')[1].length + let parts = num.toString().split('.') + // Decimal trick doesn't work for numbers represented in scientific notation, hence the hardcoded fallback + return (parts.length > 1 && parts[1].indexOf("e") == -1) ? parts[1].length : 2 } function stripTrailingZeroes(numStr, locale) { diff --git a/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml b/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml index a27fbdd683..731c19eedf 100644 --- a/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/AssetsDetailView.qml @@ -20,18 +20,12 @@ import shared.stores 1.0 Item { id: root - property var token + property var token: {} /*required*/ property string address: "" - function createStore(address) { - return balanceHistoryComponent.createObject(null, {address: address}) - } - QtObject { id: d property var marketValueStore : RootStore.marketValueStore - // TODO: Should be temporary until non native tokens are supported by balance history - property bool isNativeToken: typeof token !== "undefined" && token ? token.symbol === "ETH" : false } Connections { @@ -115,11 +109,7 @@ Item { graphsModel: [ {text: qsTr("Price"), enabled: true, id: AssetsDetailView.GraphType.Price}, - { - text: qsTr("Balance"), - enabled: false, // TODO: Enable after adding ECR20 token support and DB cache. Current prototype implementation works only for d.isNativeToken - id: AssetsDetailView.GraphType.Balance - }, + {text: qsTr("Balance"), enabled: true, id: AssetsDetailView.GraphType.Balance}, ] defaultTimeRangeIndexShown: ChartStoreBase.TimeRange.All timeRangeModel: dataReady() && selectedStore.timeRangeTabsModel @@ -129,11 +119,7 @@ Item { } if(graphDetail.selectedGraphType === AssetsDetailView.GraphType.Balance) { - let selectedTimeRangeEnum = balanceStore.timeRangeStrToEnum(graphDetail.selectedTimeRange) - if(balanceStore.isTimeToRequest(selectedTimeRangeEnum)) { - RootStore.fetchHistoricalBalanceForTokenAsJson(root.address, token.symbol, selectedTimeRangeEnum) - balanceStore.updateRequestTime(selectedTimeRangeEnum) - } + graphDetail.updateBalanceStore() } if(!isTimeRange) { @@ -241,17 +227,33 @@ Item { active: RootStore.marketHistoryIsLoading } + function updateBalanceStore() { + let selectedTimeRangeEnum = balanceStore.timeRangeStrToEnum(graphDetail.selectedTimeRange) + + let currencySymbol = RootStore.currencyStore.currentCurrency + if(!balanceStore.hasData(root.address, token.symbol, currencySymbol, selectedTimeRangeEnum)) { + RootStore.fetchHistoricalBalanceForTokenAsJson(root.address, token.symbol, currencySymbol, selectedTimeRangeEnum) + } + } + TokenBalanceHistoryStore { id: balanceStore - address: root.address - - onNewDataReady: (timeRange) => { - let selectedTimeRange = timeRangeStrToEnum(graphDetail.selectedTimeRange) - if (timeRange === selectedTimeRange && address === root.address) { + onNewDataReady: (address, tokenSymbol, currencySymbol, timeRange) => { + if (timeRange === timeRangeStrToEnum(graphDetail.selectedTimeRange)) { chart.updateToNewData() } } + + Connections { + target: root + function onAddressChanged() { graphDetail.updateBalanceStore() } + } + + Connections { + target: token + function onSymbolChanged() { graphDetail.updateBalanceStore() } + } } } } diff --git a/ui/imports/shared/stores/ChartStoreBase.qml b/ui/imports/shared/stores/ChartStoreBase.qml index d9a6ee305a..afbf7725f7 100644 --- a/ui/imports/shared/stores/ChartStoreBase.qml +++ b/ui/imports/shared/stores/ChartStoreBase.qml @@ -15,11 +15,11 @@ Item { } readonly property var timeRangeTabsModel: [ - {text: qsTr("7D"), enabled: true, timeRange: ChartStoreBase.TimeRange.Weekly}, - {text: qsTr("1M"), enabled: true, timeRange: ChartStoreBase.TimeRange.Monthly}, - {text: qsTr("6M"), enabled: true, timeRange: ChartStoreBase.TimeRange.HalfYearly}, - {text: qsTr("1Y"), enabled: true, timeRange: ChartStoreBase.TimeRange.Yearly}, - {text: qsTr("ALL"), enabled: true, timeRange: ChartStoreBase.TimeRange.All}] + {text: qsTr("7D"), enabled: true, timeRange: ChartStoreBase.TimeRange.Weekly, timeIndex: 0}, + {text: qsTr("1M"), enabled: true, timeRange: ChartStoreBase.TimeRange.Monthly, timeIndex: 1}, + {text: qsTr("6M"), enabled: true, timeRange: ChartStoreBase.TimeRange.HalfYearly, timeIndex: 2}, + {text: qsTr("1Y"), enabled: true, timeRange: ChartStoreBase.TimeRange.Yearly, timeIndex: 3}, + {text: qsTr("ALL"), enabled: true, timeRange: ChartStoreBase.TimeRange.All, timeIndex: 4}] property var weeklyData: [] property var monthlyData: [] @@ -66,61 +66,39 @@ Item { ] /// \timeRange is the time range of the data that was updated - signal newDataReady(int timeRange) + signal newDataReady(string address, string tokenSymbol, string currencySymbol, int timeRange) function timeRangeEnumToStr(enumVal) { - return d.timeRangeTabsModel.get(enumVal) + return d.timeRangeEnumToPropertiesMap.get(enumVal).text + } + function timeRangeEnumToTimeIndex(enumVal) { + return d.timeRangeEnumToPropertiesMap.get(enumVal).timeIndex } function timeRangeStrToEnum(str) { return d.timeRangeStrToEnumMap.get(str) } - /// \arg timeRange: of type ChartStoreBase.TimeRange - function updateRequestTime(timeRange) { - d.requestTimes.set(timeRange, new Date()) - } - - function resetRequestTime() { - d.requestTimes.set(timeRange, new Date(0)) - } - - /// \arg timeRange: of type ChartStoreBase.TimeRange - function isTimeToRequest(timeRange) { - if(d.requestTimes.has(timeRange)) { - const hoursToIgnore = 12 - let existing = d.requestTimes.get(timeRange) - let willBeMs = new Date(existing.getTime() + (hoursToIgnore * 3600000)) - return new Date(willBeMs) < new Date() - } - else - return true - } - QtObject { id: d readonly property int hoursInADay: 24 readonly property int avgLengthOfMonth: 30 - property var timeRangeEnumToStrMap: null + property var timeRangeEnumToPropertiesMap: null property var timeRangeStrToEnumMap: null - property var requestTimes: null } Component.onCompleted: { - if(d.timeRangeEnumToStrMap === null) { - d.timeRangeEnumToStrMap = new Map() + if(d.timeRangeEnumToPropertiesMap === null) { + d.timeRangeEnumToPropertiesMap = new Map() for (const x of timeRangeTabsModel) { - d.timeRangeEnumToStrMap.set(x.timeRange, x.text) + d.timeRangeEnumToPropertiesMap.set(x.timeRange, x) } d.timeRangeStrToEnumMap = new Map() - for (const x of d.timeRangeEnumToStrMap.entries()) { + for (const x of d.timeRangeEnumToPropertiesMap.entries()) { let key = x[0] let val = x[1] - d.timeRangeStrToEnumMap.set(val, key) + d.timeRangeStrToEnumMap.set(val.text, key) } } - if(d.requestTimes === null) { - d.requestTimes = new Map() - } } } \ No newline at end of file diff --git a/ui/imports/shared/stores/RootStore.qml b/ui/imports/shared/stores/RootStore.qml index 59b0a79c81..93e473d0cb 100644 --- a/ui/imports/shared/stores/RootStore.qml +++ b/ui/imports/shared/stores/RootStore.qml @@ -229,8 +229,8 @@ QtObject { property bool marketHistoryIsLoading: walletSectionAllTokens.marketHistoryIsLoading // TODO: range until we optimize to cache the data and abuse the requests - function fetchHistoricalBalanceForTokenAsJson(address, symbol, timeIntervalEnum) { - walletSectionAllTokens.fetchHistoricalBalanceForTokenAsJson(address, symbol, timeIntervalEnum) + function fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, timeIntervalEnum) { + walletSectionAllTokens.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, timeIntervalEnum) } property bool balanceHistoryIsLoading: walletSectionAllTokens.balanceHistoryIsLoading diff --git a/ui/imports/shared/stores/TokenBalanceHistoryStore.qml b/ui/imports/shared/stores/TokenBalanceHistoryStore.qml index 0042ae5e7c..7400ebc52c 100644 --- a/ui/imports/shared/stores/TokenBalanceHistoryStore.qml +++ b/ui/imports/shared/stores/TokenBalanceHistoryStore.qml @@ -7,10 +7,27 @@ import utils 1.0 ChartStoreBase { id: root - /*required*/ property string address: "" + readonly property alias address: d.address + readonly property alias tokenSymbol: d.tokenSymbol + readonly property alias currencySymbol: d.currencySymbol + + QtObject { + id: d + + // Data identity received from backend + property var chainIds: [] + property string address + property string tokenSymbol + property string currencySymbol + } + + function hasData(address, tokenSymbol, currencySymbol, timeRangeEnum) { + return address === d.address && tokenSymbol === d.tokenSymbol && currencySymbol === d.currencySymbol + && root.dataRange[root.timeRangeEnumToTimeIndex(timeRangeEnum)][root.timeRangeEnumToStr(timeRangeEnum)].length > 0 + } /// \arg timeRange: of type ChartStoreBase.TimeRange - function setData(timeRange, timeRangeData, balanceData) { + function setData(address, tokenSymbol, currencySymbol, timeRange, timeRangeData, balanceData) { switch(timeRange) { case ChartStoreBase.TimeRange.Weekly: root.weeklyData = balanceData @@ -39,38 +56,42 @@ ChartStoreBase { break; default: console.warn("Invalid or unsupported time range") - break; + return } - root.newDataReady(timeRange) + + d.address = address + d.tokenSymbol = tokenSymbol + d.currencySymbol = currencySymbol + + root.newDataReady(address, tokenSymbol, currencySymbol, timeRange) } - /// \arg timeRange: of type ChartStoreBase.TimeRange - function resetData(timeRange) { - root.setData(timeRange, [], []) + function resetAllData(address, tokenSymbol, currencySymbol) { + for (let tR = ChartStoreBase.TimeRange.Weekly; tR <= ChartStoreBase.TimeRange.All; tR++) { + root.setData(address, tokenSymbol, currencySymbol, tR, [], []) + } } Connections { target: walletSectionAllTokens - function onTokenBalanceHistoryDataReady(balanceHistory: string) { - // chainId, address, symbol, timeInterval - let response = JSON.parse(balanceHistory) - if (response === null) { - console.warn("error parsing balance history json message data") - root.resetRequestTime() + + function onTokenBalanceHistoryDataReady(balanceHistoryJson: string) { + // chainIds, address, tokenSymbol, currencySymbol, timeInterval + let response = JSON.parse(balanceHistoryJson) + if(typeof response.error !== "undefined") { + console.warn("error in balance history: " + response.error) return } + if(d.address != response.address || d.tokenSymbol != response.tokenSymbol || d.currencySymbol != response.currencySymbol) { + root.resetAllData(response.address, response.tokenSymbol, response.currencySymbol) + } + if(typeof response.historicalData === "undefined" || response.historicalData === null || response.historicalData.length == 0) { - console.warn("error no data in balance history. Must be an error from status-go") - root.resetRequestTime() - return - } else if(response.address !== root.address) { - // Ignore data for other addresses. Will be handled by other instances of this store + console.info("no data in balance history") return } - root.resetData(response.timeInterval) - var tmpTimeRange = [] var tmpDataValues = [] for(let i = 0; i < response.historicalData.length; i++) { @@ -81,11 +102,10 @@ ChartStoreBase { : LocaleUtils.getMonthYear(dataEntry.time * 1000) tmpTimeRange.push(dateString) - tmpDataValues.push(parseFloat(globalUtils.wei2Eth(dataEntry.value, 18))) + tmpDataValues.push(dataEntry.value) } - root.setData(response.timeInterval, tmpTimeRange, tmpDataValues) - root.updateRequestTime(response.timeInterval) + root.setData(response.address, response.tokenSymbol, response.currencySymbol, response.timeInterval, tmpTimeRange, tmpDataValues) } } } diff --git a/ui/imports/shared/stores/qmldir b/ui/imports/shared/stores/qmldir index d3db0cb72f..fdf3ca8af4 100644 --- a/ui/imports/shared/stores/qmldir +++ b/ui/imports/shared/stores/qmldir @@ -2,6 +2,6 @@ singleton RootStore 1.0 RootStore.qml CurrenciesStore 1.0 CurrenciesStore.qml TransactionStore 1.0 TransactionStore.qml BIP39_en 1.0 BIP39_en.qml +ChartStoreBase 1.0 ChartStoreBase.qml TokenBalanceHistoryStore 1.0 TokenBalanceHistoryStore.qml TokenMarketValuesStore 1.0 TokenMarketValuesStore.qml -ChartStoreBase 1.0 ChartStoreBase.qml diff --git a/vendor/status-go b/vendor/status-go index f6b4721c4a..f4f6b25302 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit f6b4721c4a74f05049b1309c3250ed803d936c59 +Subproject commit f4f6b253022275dbe08f27d3a29b6c86da21b35b