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
This commit is contained in:
Stefan 2022-11-15 23:48:59 +02:00 committed by Stefan Dunca
parent 7ebe5488bd
commit 3bb667bb7a
25 changed files with 603 additions and 254 deletions

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -108,10 +108,12 @@ TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
return resultData;
}
std::vector<TokenBalanceHistory>
getBalanceHistory(const ChainID& chainID, Accounts::EOAddress account, BalanceHistoryTimeInterval timeInterval)
std::vector<TokenBalanceHistory> getBalanceHistory(const std::vector<ChainID>& chainIds,
Accounts::EOAddress account,
const QString& currency,
BalanceHistoryTimeInterval timeInterval)
{
std::vector<json> params = {chainID, account, timeInterval};
std::vector<json> 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<CallPrivateRpcResponse>().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<CallPrivateRpcResponse>().result;
}
void checkRecentHistory(const std::vector<Accounts::EOAddress>& accounts)
{
std::vector<json> params = {accounts};
@ -130,4 +144,29 @@ void checkRecentHistory(const std::vector<Accounts::EOAddress>& 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<QString> symbols)
{
json inputJson = {
{"jsonrpc", "2.0"}, {"method", "wallet_updateVisibleTokens"}, {"params", std::vector<json>{symbols}}};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
const auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
}
} // namespace Status::StatusGo::Wallet

View File

@ -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<TokenBalanceHistory>
getBalanceHistory(const ChainID& chainID, Accounts::EOAddress account, BalanceHistoryTimeInterval timeInterval);
std::vector<TokenBalanceHistory> getBalanceHistory(const std::vector<ChainID>& 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::EOAddress>& 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<QString> symbols);
} // namespace Status::StatusGo::Wallet

View File

@ -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",

View File

@ -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",
}
}

View File

@ -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,

View File

@ -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))
method fetchHistoricalBalanceForTokenAsJson*(self: Controller, address: string, tokenSymbol: string, currencySymbol: string, timeIntervalEnum: int) =
self.tokenService.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, BalanceHistoryTimeInterval(timeIntervalEnum))

View File

@ -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.} =

View File

@ -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)

View File

@ -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.}

View File

@ -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)

View File

@ -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
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

View File

@ -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
@ -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

View File

@ -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"):

View File

@ -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<TestNetwork> networks;
QString token;
QString newTestAccountName;
};
static const std::vector<TestParams> 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(), [&params](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(), [&params](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<Wallet::ChainID> 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(), [&params](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(), [&params](const auto& t) { return t.symbol == params.token; });
ASSERT_NE(tokenIt, tokens.end());
}
}
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [&params](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<StatusGo::EventData> 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<QString> 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<double>() + (qSzabos.convert_to<double>() / ((weiD / szaboD).convert_to<double>()));
};
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<StatusGo::EventData> 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<QString> 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<StatusGo::EventData> 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

View File

@ -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) {

View File

@ -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() }
}
}
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit f6b4721c4a74f05049b1309c3250ed803d936c59
Subproject commit f4f6b253022275dbe08f27d3a29b6c86da21b35b