From 3a3b2c9dc9d9473d7306999f6b31892160bcabfb Mon Sep 17 00:00:00 2001 From: Sale Djenic Date: Tue, 24 May 2022 10:46:59 +0200 Subject: [PATCH] fix(@desktop/wallet): show all tokens in wallet that have balance > 0 Fixes #5859 --- .../current_account/module.nim | 27 +- .../browser_section/current_account/view.nim | 25 +- .../main/wallet_section/accounts/module.nim | 4 +- .../wallet_section/current_account/module.nim | 30 +- .../wallet_section/current_account/view.nim | 39 +-- src/app_service/service/accounts/service.nim | 1 - .../service/wallet_account/async_tasks.nim | 203 +++++++++++++ .../service/wallet_account/dto.nim | 55 +++- .../service/wallet_account/service.nim | 269 +++++++----------- 9 files changed, 436 insertions(+), 217 deletions(-) diff --git a/src/app/modules/main/browser_section/current_account/module.nim b/src/app/modules/main/browser_section/current_account/module.nim index 46f553e55e..2dd41dd99b 100644 --- a/src/app/modules/main/browser_section/current_account/module.nim +++ b/src/app/modules/main/browser_section/current_account/module.nim @@ -1,8 +1,10 @@ -import NimQml +import NimQml, Tables import ../../../../global/global_singleton import ../../../../core/eventemitter import ../../../../../app_service/service/wallet_account/service as wallet_account_service +import ../../../shared_models/token_model as token_model +import ../../../shared_models/token_item as token_item import ./io_interface, ./view, ./controller import ../io_interface as delegate_interface @@ -18,6 +20,8 @@ type moduleLoaded: bool currentAccountIndex: int +proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[WalletTokenDto]]) + proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, @@ -34,10 +38,21 @@ proc newModule*( method delete*(self: Module) = self.view.delete +proc setAssets(self: Module, tokens: seq[WalletTokenDto]) = + var items: seq[Item] + for t in tokens: + if(t.totalBalance.balance == 0): + continue + let item = token_item.initItem(t.name, t.symbol, t.totalBalance.balance, t.address, t.totalBalance.currencyBalance) + items.add(item) + + self.view.getAssetsModel().setItems(items) + method switchAccount*(self: Module, accountIndex: int) = self.currentAccountIndex = accountIndex let walletAccount = self.controller.getWalletAccount(accountIndex) self.view.setData(walletAccount) + self.setAssets(walletAccount.tokens) method load*(self: Module) = singletonInstance.engine.setRootContextProperty("browserSectionCurrentAccount", newQVariant(self.view)) @@ -47,6 +62,10 @@ method load*(self: Module) = self.switchAccount(0) self.view.connectedAccountDeleted() + self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT) do(e:Args): + let arg = TokensPerAccountArgs(e) + self.onTokensRebuilt(arg.accountsTokens) + self.controller.init() self.view.load() self.switchAccount(0) @@ -60,3 +79,9 @@ method viewDidLoad*(self: Module) = method switchAccountByAddress*(self: Module, address: string) = let accountIndex = self.controller.getIndex(address) self.switchAccount(accountIndex) + +proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[WalletTokenDto]]) = + let walletAccount = self.controller.getWalletAccount(self.currentAccountIndex) + if not accountsTokens.contains(walletAccount.address): + return + self.setAssets(accountsTokens[walletAccount.address]) \ No newline at end of file diff --git a/src/app/modules/main/browser_section/current_account/view.nim b/src/app/modules/main/browser_section/current_account/view.nim index 776a74f800..16ac65cc15 100644 --- a/src/app/modules/main/browser_section/current_account/view.nim +++ b/src/app/modules/main/browser_section/current_account/view.nim @@ -25,12 +25,14 @@ QtObject: self.QObject.setup proc delete*(self: View) = + self.assets.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = new(result, delete) - result.delegate = delegate result.setup() + result.delegate = delegate + result.assets = token_model.newModel() proc load*(self: View) = self.delegate.viewDidLoad() @@ -107,11 +109,12 @@ QtObject: read = getCurrencyBalance notify = currencyBalanceChanged - proc getAssets(self: View): QVariant {.slot.} = - return newQVariant(self.assets) + proc getAssetsModel*(self: View): token_model.Model = + return self.assets proc assetsChanged(self: View) {.signal.} - + proc getAssets*(self: View): QVariant {.slot.} = + return newQVariant(self.assets) QtProperty[QVariant] assets: read = getAssets notify = assetsChanged @@ -151,20 +154,6 @@ proc setData*(self: View, dto: wallet_account_service.WalletAccountDto) = self.emoji = dto.emoji self.emojiChanged() - let assets = token_model.newModel() - - assets.setItems( - dto.tokens.map(t => token_item.initItem( - t.name, - t.symbol, - t.balance.chainBalance, - t.address, - t.balance.currencyBalance, - )) - ) - self.assets = assets - self.assetsChanged() - proc isAddressCurrentAccount*(self: View, address: string): bool = return self.address == address diff --git a/src/app/modules/main/wallet_section/accounts/module.nim b/src/app/modules/main/wallet_section/accounts/module.nim index e7fda92a4a..71665441e9 100644 --- a/src/app/modules/main/wallet_section/accounts/module.nim +++ b/src/app/modules/main/wallet_section/accounts/module.nim @@ -46,9 +46,9 @@ method refreshWalletAccounts*(self: Module) = w.tokens.map(t => token_item.initItem( t.name, t.symbol, - t.balance.chainBalance, + t.totalBalance.balance, t.address, - t.balance.currencyBalance, + t.totalBalance.currencyBalance, )) ) diff --git a/src/app/modules/main/wallet_section/current_account/module.nim b/src/app/modules/main/wallet_section/current_account/module.nim index c63cfdf982..e10c4e6bcb 100644 --- a/src/app/modules/main/wallet_section/current_account/module.nim +++ b/src/app/modules/main/wallet_section/current_account/module.nim @@ -1,8 +1,10 @@ -import NimQml +import NimQml, Tables import ../../../../global/global_singleton import ../../../../core/eventemitter import ../../../../../app_service/service/wallet_account/service as wallet_account_service +import ../../../shared_models/token_model as token_model +import ../../../shared_models/token_item as token_item import ./io_interface, ./view, ./controller import ../io_interface as delegate_interface @@ -18,6 +20,8 @@ type moduleLoaded: bool currentAccountIndex: int +proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[WalletTokenDto]]) + proc newModule*( delegate: delegate_interface.AccessInterface, events: EventEmitter, @@ -50,6 +54,10 @@ method load*(self: Module) = self.events.on(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED) do(e: Args): self.switchAccount(self.currentAccountIndex) + self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT) do(e:Args): + let arg = TokensPerAccountArgs(e) + self.onTokensRebuilt(arg.accountsTokens) + self.controller.init() self.view.load() @@ -60,10 +68,30 @@ method viewDidLoad*(self: Module) = self.moduleLoaded = true self.delegate.currentAccountModuleDidLoad() +proc setAssetsAndBalance(self: Module, tokens: seq[WalletTokenDto]) = + var totalCurrencyBalanceForAllAssets = 0.0 + var items: seq[Item] + for t in tokens: + if(t.totalBalance.balance == 0): + continue + let item = token_item.initItem(t.name, t.symbol, t.totalBalance.balance, t.address, t.totalBalance.currencyBalance) + items.add(item) + totalCurrencyBalanceForAllAssets += t.totalBalance.currencyBalance + + self.view.getAssetsModel().setItems(items) + self.view.setCurrencyBalance(totalCurrencyBalanceForAllAssets) + method switchAccount*(self: Module, accountIndex: int) = self.currentAccountIndex = accountIndex let walletAccount = self.controller.getWalletAccount(accountIndex) self.view.setData(walletAccount) + self.setAssetsAndBalance(walletAccount.tokens) method update*(self: Module, address: string, accountName: string, color: string, emoji: string) = self.controller.update(address, accountName, color, emoji) + +proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[WalletTokenDto]]) = + let walletAccount = self.controller.getWalletAccount(self.currentAccountIndex) + if not accountsTokens.contains(walletAccount.address): + return + self.setAssetsAndBalance(accountsTokens[walletAccount.address]) \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/current_account/view.nim b/src/app/modules/main/wallet_section/current_account/view.nim index 926c654168..c9d10e3de2 100644 --- a/src/app/modules/main/wallet_section/current_account/view.nim +++ b/src/app/modules/main/wallet_section/current_account/view.nim @@ -25,12 +25,14 @@ QtObject: self.QObject.setup proc delete*(self: View) = + self.assets.delete self.QObject.delete proc newView*(delegate: io_interface.AccessInterface): View = new(result, delete) - result.delegate = delegate result.setup() + result.delegate = delegate + result.assets = token_model.newModel() proc load*(self: View) = self.delegate.viewDidLoad() @@ -103,20 +105,22 @@ QtObject: read = getIsChat notify = isChatChanged - proc getCurrencyBalance(self: View): QVariant {.slot.} = - return newQVariant(self.currencyBalance) - proc currencyBalanceChanged(self: View) {.signal.} - + proc getCurrencyBalance*(self: View): QVariant {.slot.} = + return newQVariant(self.currencyBalance) + proc setCurrencyBalance*(self: View, value: float) = + self.currencyBalance = value + self.currencyBalanceChanged() QtProperty[QVariant] currencyBalance: read = getCurrencyBalance notify = currencyBalanceChanged - proc getAssets(self: View): QVariant {.slot.} = - return newQVariant(self.assets) + proc getAssetsModel*(self: View): token_model.Model = + return self.assets proc assetsChanged(self: View) {.signal.} - + proc getAssets*(self: View): QVariant {.slot.} = + return newQVariant(self.assets) QtProperty[QVariant] assets: read = getAssets notify = assetsChanged @@ -158,23 +162,6 @@ QtObject: if(self.isChat != dto.isChat): self.isChat = dto.isChat self.isChatChanged() - if(self.currencyBalance != dto.getCurrencyBalance()): - self.currencyBalance = dto.getCurrencyBalance() - self.currencyBalanceChanged() if(self.emoji != dto.emoji): self.emoji = dto.emoji - self.emojiChanged() - - let assets = token_model.newModel() - - assets.setItems( - dto.tokens.map(t => token_item.initItem( - t.name, - t.symbol, - t.balance.chainBalance, - t.address, - t.balance.currencyBalance, - )) - ) - self.assets = assets - self.assetsChanged() + self.emojiChanged() \ No newline at end of file diff --git a/src/app_service/service/accounts/service.nim b/src/app_service/service/accounts/service.nim index 51e3e66294..0b9fa43415 100644 --- a/src/app_service/service/accounts/service.nim +++ b/src/app_service/service/accounts/service.nim @@ -1,6 +1,5 @@ import os, json, sequtils, strutils, uuids import json_serialization, chronicles -import times as times import ./dto/accounts as dto_accounts import ./dto/generated_accounts as dto_generated_accounts diff --git a/src/app_service/service/wallet_account/async_tasks.nim b/src/app_service/service/wallet_account/async_tasks.nim index e5fe7b5935..8a1c53aefb 100644 --- a/src/app_service/service/wallet_account/async_tasks.nim +++ b/src/app_service/service/wallet_account/async_tasks.nim @@ -72,3 +72,206 @@ const getDerivedAddressForPrivateKeyTask*: Task = proc(argEncoded: string) {.gcs } arg.finish(output) +################################################# +# Async timer +################################################# + +type + TimerTaskArg = ref object of QObjectTaskArg + timeoutInMilliseconds: int + +const timerTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[TimerTaskArg](argEncoded) + sleep(arg.timeoutInMilliseconds) + arg.finish("") + +################################################# +# Async building token +################################################# + +type + BuildTokensTaskArg = ref object of QObjectTaskArg + walletAddresses: seq[string] + currency: string + networks: seq[NetworkDto] + +proc getCustomTokens(): seq[TokenDto] = + try: + let responseCustomTokens = backend.getCustomTokens() + result = map(responseCustomTokens.result.getElems(), proc(x: JsonNode): TokenDto = x.toTokenDto(@[])) + except Exception as e: + error "error fetching custom tokens: ", message = e.msg + +proc getTokensForChainId(chainId: int): seq[TokenDto] = + try: + let responseTokens = backend.getTokens(chainId) + let defaultTokens = map(responseTokens.result.getElems(), + proc(x: JsonNode): TokenDto = x.toTokenDto(@[], hasIcon=true, isCustom=false) + ) + result.add(defaultTokens) + except Exception as e: + error "error fetching tokens: ", message = e.msg, chainId=chainId + +proc prepareSymbols(networkSymbols: seq[string], allTokens: seq[TokenDto]): seq[seq[string]] = + # we have to use up to 300 characters in a single request when we're fetching prices + let charsMaxLenght = 300 + result.add(@[]) + var networkSymbolsIndex = 0 + var tokenSymbolsIndex = 0 + while networkSymbolsIndex < networkSymbols.len or tokenSymbolsIndex < allTokens.len: + var currentCharsLen = 0 + var reachTheEnd = false + while networkSymbolsIndex < networkSymbols.len: + if(currentCharsLen + networkSymbols[networkSymbolsIndex].len >= charsMaxLenght): + reachTheEnd = true + result.add(@[]) + break + else: + currentCharsLen += networkSymbols[networkSymbolsIndex].len + 1 # we add one for ',' + result[result.len - 1].add(networkSymbols[networkSymbolsIndex]) + networkSymbolsIndex.inc + while not reachTheEnd and tokenSymbolsIndex < allTokens.len: + if(currentCharsLen + allTokens[tokenSymbolsIndex].symbol.len >= charsMaxLenght): + reachTheEnd = true + result.add(@[]) + break + else: + currentCharsLen += allTokens[tokenSymbolsIndex].symbol.len + 1 # we add one for ',' + result[result.len - 1].add(allTokens[tokenSymbolsIndex].symbol) + tokenSymbolsIndex.inc + +proc fetchNativeChainBalance(chainId: int, nativeCurrencyDecimals: int, accountAddress: string): float64 = + result = 0.0 + try: + let nativeBalanceResponse = status_go_eth.getNativeChainBalance(chainId, accountAddress) + result = parsefloat(hex2Balance(nativeBalanceResponse.result.getStr, nativeCurrencyDecimals)) + except Exception as e: + error "error getting balance", message = e.msg + +proc fetchPrices(networkSymbols: seq[string], allTokens: seq[TokenDto], currency: string): Table[string, float] = + let allSymbols = prepareSymbols(networkSymbols, allTokens) + for symbols in allSymbols: + try: + let response = backend.fetchPrices(symbols, currency) + for (symbol, value) in response.result.pairs: + result[symbol] = value.getFloat + except Exception as e: + error "error fetching prices: ", message = e.msg + +proc getTokensBalances(walletAddresses: seq[string], allTokens: seq[TokenDto]): JsonNode = + try: + result = newJObject() + let tokensAddresses = allTokens.map(t => t.addressAsString()) + # We need to check, we should use `chainIdsFromSettings` instead `chainIds` deduced from the allTokens list? + let chainIds = deduplicate(allTokens.map(t => t.chainId)) + let tokensBalancesResponse = backend.getTokensBalancesForChainIDs(chainIds, walletAddresses, tokensAddresses) + result = tokensBalancesResponse.result + except Exception as e: + error "error fetching tokens balances: ", message = e.msg + +proc groupNetworksBySymbol(networks: seq[NetworkDto]): Table[string, seq[NetworkDto]] = + for network in networks: + if not result.hasKey(network.nativeCurrencySymbol): + result[network.nativeCurrencySymbol] = @[] + result[network.nativeCurrencySymbol].add(network) + +proc getNetworkByCurrencySymbol(networks: seq[NetworkDto], networkNativeCurrencySymbol: string): NetworkDto = + for network in networks: + if network.nativeCurrencySymbol != networkNativeCurrencySymbol: + continue + return network + +proc groupTokensBySymbol(tokens: seq[TokenDto]): Table[string, seq[TokenDto]] = + for token in tokens: + if not result.hasKey(token.symbol): + result[token.symbol] = @[] + result[token.symbol].add(token) + +proc getTokenForSymbol(tokens: seq[TokenDto], symbol: string): TokenDto = + for token in tokens: + if token.symbol != symbol: + continue + return token + +const prepareTokensTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[BuildTokensTaskArg](argEncoded) + + var networkSymbols: seq[string] + var chainIdsFromSettings: seq[int] + for network in arg.networks: + networkSymbols.add(network.nativeCurrencySymbol) + chainIdsFromSettings.add(network.chainId) + + var allTokens: seq[TokenDto] + allTokens.add(getCustomTokens()) + for chainId in chainIdsFromSettings: + allTokens.add(getTokensForChainId(chainId)) + allTokens = deduplicate(allTokens) + + var prices = fetchPrices(networkSymbols, allTokens, arg.currency) + let tokenBalances = getTokensBalances(arg.walletAddresses, allTokens) + + var builtTokensPerAccount = %*{ } + for address in arg.walletAddresses: + var builtTokens: seq[WalletTokenDto] + + let groupedNetworks = groupNetworksBySymbol(arg.networks) + for networkNativeCurrencySymbol, networks in groupedNetworks.pairs: + var balancesPerChain = initTable[int, BalanceDto]() + for network in networks: + let chainBalance = fetchNativeChainBalance(network.chainId, network.nativeCurrencyDecimals, address) + balancesPerChain[network.chainId] = BalanceDto( + balance: chainBalance, + currencyBalance: chainBalance * prices[network.nativeCurrencySymbol] + ) + let networkDto = getNetworkByCurrencySymbol(arg.networks, networkNativeCurrencySymbol) + var totalTokenBalance: BalanceDto + totalTokenBalance.balance = toSeq(balancesPerChain.values).map(x => x.balance).foldl(a + b) + totalTokenBalance.currencyBalance = totalTokenBalance.balance * prices[networkDto.nativeCurrencySymbol] + builtTokens.add(WalletTokenDto( + name: networkDto.nativeCurrencyName, + address: "0x0000000000000000000000000000000000000000", + symbol: networkDto.nativeCurrencySymbol, + decimals: networkDto.nativeCurrencyDecimals, + hasIcon: true, + color: "blue", + isCustom: false, + totalBalance: totalTokenBalance, + balancesPerChain: balancesPerChain + ) + ) + + let groupedTokens = groupTokensBySymbol(allTokens) + for symbol, tokens in groupedTokens.pairs: + var balancesPerChain = initTable[int, BalanceDto]() + for token in tokens: + let balanceForToken = tokenBalances{address}{token.addressAsString()}.getStr + let chainBalanceForToken = parsefloat(hex2Balance(balanceForToken, token.decimals)) + balancesPerChain[token.chainId] = BalanceDto( + balance: chainBalanceForToken, + currencyBalance: chainBalanceForToken * prices[token.symbol] + ) + let tokenDto = getTokenForSymbol(allTokens, symbol) + var totalTokenBalance: BalanceDto + totalTokenBalance.balance = toSeq(balancesPerChain.values).map(x => x.balance).foldl(a + b) + totalTokenBalance.currencyBalance = totalTokenBalance.balance * prices[tokenDto.symbol] + builtTokens.add(WalletTokenDto( + name: tokenDto.name, + address: $tokenDto.address, + symbol: tokenDto.symbol, + decimals: tokenDto.decimals, + hasIcon: tokenDto.hasIcon, + color: tokenDto.color, + isCustom: tokenDto.isCustom, + totalBalance: totalTokenBalance, + balancesPerChain: balancesPerChain + ) + ) + + var tokensJArray = newJArray() + for wtDto in builtTokens: + tokensJarray.add(walletTokenDtoToJson(wtDto)) + builtTokensPerAccount[address] = tokensJArray + + arg.finish(builtTokensPerAccount) + diff --git a/src/app_service/service/wallet_account/dto.nim b/src/app_service/service/wallet_account/dto.nim index a13059afef..8a608f1051 100644 --- a/src/app_service/service/wallet_account/dto.nim +++ b/src/app_service/service/wallet_account/dto.nim @@ -1,13 +1,13 @@ -import tables, json, sequtils, sugar +import tables, json, sequtils, sugar, strutils include ../../common/json_utils -type BalanceDto* = ref object of RootObj - chainBalance*: float64 +type BalanceDto* = object + balance*: float64 currencyBalance*: float64 type - WalletTokenDto* = ref object of RootObj + WalletTokenDto* = object name*: string address*: string symbol*: string @@ -15,8 +15,8 @@ type hasIcon*: bool color*: string isCustom*: bool - balance*: BalanceDto - balances*: Table[int, BalanceDto] + totalBalance*: BalanceDto + balancesPerChain*: Table[int, BalanceDto] type WalletAccountDto* = ref object of RootObj @@ -73,4 +73,45 @@ proc toWalletAccountDto*(jsonObj: JsonNode): WalletAccountDto = discard jsonObj.getProp("derived-from", result.derivedfrom) proc getCurrencyBalance*(self: WalletAccountDto): float64 = - return self.tokens.map(t => t.balance.currencyBalance).foldl(a + b, 0.0) + return self.tokens.map(t => t.totalBalance.currencyBalance).foldl(a + b, 0.0) + +proc toBalanceDto*(jsonObj: JsonNode): BalanceDto = + result = BalanceDto() + discard jsonObj.getProp("balance", result.balance) + discard jsonObj.getProp("currencyBalance", result.currencyBalance) + +proc toWalletTokenDto*(jsonObj: JsonNode): WalletTokenDto = + result = WalletTokenDto() + discard jsonObj.getProp("name", result.name) + discard jsonObj.getProp("address", result.address) + discard jsonObj.getProp("symbol", result.symbol) + discard jsonObj.getProp("decimals", result.decimals) + discard jsonObj.getProp("hasIcon", result.hasIcon) + discard jsonObj.getProp("color", result.color) + discard jsonObj.getProp("isCustom", result.isCustom) + + var totalBalanceObj: JsonNode + if(jsonObj.getProp("totalBalance", totalBalanceObj)): + result.totalBalance = toBalanceDto(totalBalanceObj) + + var balancesPerChainObj: JsonNode + if(jsonObj.getProp("balancesPerChain", balancesPerChainObj)): + for chainId, balanceObj in balancesPerChainObj: + result.balancesPerChain[parseInt(chainId)] = toBalanceDto(balanceObj) + +proc walletTokenDtoToJson*(dto: WalletTokenDto): JsonNode = + var balancesPerChainJsonObj = newJObject() + for k, v in dto.balancesPerChain.pairs: + balancesPerChainJsonObj[$k] = %* v + + result = %* { + "name": dto.name, + "address": dto.address, + "symbol": dto.symbol, + "decimals": dto.decimals, + "hasIcon": dto.hasIcon, + "color": dto.color, + "isCustom": dto.isCustom, + "totalBalance": %* dto.totalBalance, + "balancesPerChain": balancesPerChainJsonObj + } \ 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 905b027c19..6782bf8e0d 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -1,4 +1,4 @@ -import Tables, NimQml, json, sequtils, sugar, chronicles, strformat, stint, httpclient, net, strutils +import NimQml, Tables, json, sequtils, sugar, chronicles, strformat, stint, httpclient, net, strutils, os import web3/[ethtypes, conversions] import ../settings/service as settings_service @@ -6,6 +6,7 @@ import ../accounts/service as accounts_service import ../token/service as token_service import ../network/service as network_service import ../../common/account_constants +import ../../../app/global/global_singleton import dto import derived_address @@ -23,8 +24,6 @@ export derived_address logScope: topics = "wallet-account-service" -include async_tasks - const SIGNAL_WALLET_ACCOUNT_SAVED* = "walletAccount/accountSaved" const SIGNAL_WALLET_ACCOUNT_DELETED* = "walletAccount/accountDeleted" const SIGNAL_WALLET_ACCOUNT_CURRENCY_UPDATED* = "walletAccount/currencyUpdated" @@ -32,6 +31,7 @@ const SIGNAL_WALLET_ACCOUNT_TOKEN_VISIBILITY_UPDATED* = "walletAccount/tokenVisi const SIGNAL_WALLET_ACCOUNT_UPDATED* = "walletAccount/walletAccountUpdated" const SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED* = "walletAccount/networkEnabledUpdated" const SIGNAL_WALLET_ACCOUNT_DERIVED_ADDRESS_READY* = "walletAccount/derivedAddressesReady" +const SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT* = "walletAccount/tokensRebuilt" var balanceCache {.threadvar.}: Table[string, float64] @@ -56,19 +56,6 @@ proc fetchAccounts(): seq[WalletAccountDto] = x => x.toWalletAccountDto() ).filter(a => not a.isChat) -proc fetchNativeChainBalance(network: NetworkDto, accountAddress: string): float64 = - let key = "0x0" & accountAddress & $network.chainId - if balanceCache.hasKey(key): - return balanceCache[key] - - try: - let nativeBalanceResponse = status_go_eth.getNativeChainBalance(network.chainId, accountAddress) - result = parsefloat(hex2Balance(nativeBalanceResponse.result.getStr, network.nativeCurrencyDecimals)) - balanceCache[key] = result - except Exception as e: - error "Error getting balance", message = e.msg - result = 0.0 - type AccountSaved = ref object of Args account: WalletAccountDto @@ -88,20 +75,33 @@ type DerivedAddressesArgs* = ref object of Args derivedAddresses*: seq[DerivedAddressDto] error*: string +type TokensPerAccountArgs* = ref object of Args + accountsTokens*: OrderedTable[string, seq[WalletTokenDto]] # [wallet address, list of tokens] + +const CheckBalanceIntervalInMilliseconds = 15 * 60 * 1000 # 15 mins + +include async_tasks +include ../../common/json_utils + QtObject: type Service* = ref object of QObject - events: EventEmitter - threadpool: ThreadPool - settingsService: settings_service.Service - accountsService: accounts_service.Service - tokenService: token_service.Service - networkService: network_service.Service - accounts: OrderedTable[string, WalletAccountDto] + closingApp: bool + ignoreTimeInitiatedTokensBuild: bool + events: EventEmitter + threadpool: ThreadPool + settingsService: settings_service.Service + accountsService: accounts_service.Service + tokenService: token_service.Service + networkService: network_service.Service + walletAccounts: OrderedTable[string, WalletAccountDto] - priceCache: TimedCache + priceCache: TimedCache + # Forward declaration + proc buildAllTokens(self: Service, calledFromTimerOrInit = false) proc delete*(self: Service) = + self.closingApp = true self.QObject.delete proc newService*( @@ -114,88 +114,17 @@ QtObject: ): Service = new(result, delete) result.QObject.setup + result.closingApp = false + result.ignoreTimeInitiatedTokensBuild = false result.events = events result.threadpool = threadpool result.settingsService = settingsService result.accountsService = accountsService result.tokenService = tokenService result.networkService = networkService - result.accounts = initOrderedTable[string, WalletAccountDto]() + result.walletAccounts = initOrderedTable[string, WalletAccountDto]() result.priceCache = newTimedCache() - proc buildTokens( - self: Service, - account: WalletAccountDto, - prices: Table[string, float], - tokenBalances: JsonNode - ): seq[WalletTokenDto] = - var groupedNetwork = initTable[string, seq[NetworkDto]]() - for network in self.networkService.getEnabledNetworks(): - if not groupedNetwork.hasKey(network.nativeCurrencyName): - groupedNetwork[network.nativeCurrencyName] = @[] - - groupedNetwork[network.nativeCurrencyName].add(network) - for currencyName, networks in groupedNetwork.pairs: - var balances = initTable[int, BalanceDto]() - for network in networks: - let chainBalance = fetchNativeChainBalance(network, account.address) - balances[network.chainId] = BalanceDto( - chainBalance: chainBalance, - currencyBalance: chainBalance * prices[network.nativeCurrencySymbol] - ) - - let totalChainBalance = toSeq(balances.values).map(x => x.chainBalance).foldl(a + b) - let balance = BalanceDto( - chainBalance: totalChainBalance, - currencyBalance: totalChainBalance * prices[networks[0].nativeCurrencySymbol] - ) - result.add(WalletTokenDto( - name: currencyName, - address: "0x0000000000000000000000000000000000000000", - symbol: networks[0].nativeCurrencySymbol, - decimals: networks[0].nativeCurrencyDecimals, - hasIcon: true, - color: "blue", - isCustom: false, - balance: balance, - balances: balances - )) - - var groupedToken = initTable[string, seq[TokenDto]]() - for token in self.tokenService.getVisibleTokens(): - if not groupedToken.hasKey(token.symbol): - groupedToken[token.symbol] = @[] - groupedToken[token.symbol].add(token) - - for symbol, tokens in groupedToken.pairs: - var balances = initTable[int, BalanceDto]() - for token in tokens: - let chainBalance = parsefloat(hex2Balance(tokenBalances{token.addressAsString()}.getStr, token.decimals)) - balances[token.chainId] = BalanceDto( - chainBalance: chainBalance, - currencyBalance: chainBalance * prices[symbol] - ) - - let totalChainBalance = toSeq(balances.values).map(x => x.chainBalance).foldl(a + b) - let balance = BalanceDto( - chainBalance: totalChainBalance, - currencyBalance: totalChainBalance * prices[symbol] - ) - - result.add( - WalletTokenDto( - name: tokens[0].name, - address: $tokens[0].address, - symbol: symbol, - decimals: tokens[0].decimals, - hasIcon: tokens[0].hasIcon, - color: tokens[0].color, - isCustom: tokens[0].isCustom, - balance: balance, - balances: balances - ) - ) - proc getPrice*(self: Service, crypto: string, fiat: string): float64 = let cacheKey = crypto & fiat if self.priceCache.isCached(cacheKey): @@ -214,68 +143,27 @@ QtObject: error "error: ", errDesription return 0.0 - - proc fetchPrices(self: Service): Table[string, float] = - let currency = self.settingsService.getCurrency() - - var symbols: seq[string] = @[] - - for network in self.networkService.getEnabledNetworks(): - symbols.add(network.nativeCurrencySymbol) - - for token in self.tokenService.getVisibleTokens(): - symbols.add(token.symbol) - - var prices = initTable[string, float]() - if symbols.len == 0: - return prices - - try: - let response = backend.fetchPrices(symbols, currency) - for (symbol, value) in response.result.pairs: - prices[symbol] = value.getFloat - - except Exception as e: - let errDesription = e.msg - error "error: ", errDesription - - return prices - - proc fetchBalances(self: Service, accounts: seq[string]): JsonNode = - let visibleTokens = self.tokenService.getVisibleTokens() - let tokens = visibleTokens.map(t => t.addressAsString()) - let chainIds = visibleTokens.map(t => t.chainId) - return backend.getTokensBalancesForChainIDs(chainIds, accounts, tokens).result - - proc refreshBalances(self: Service) = - let prices = self.fetchPrices() - let accounts = toSeq(self.accounts.keys) - let balances = self.fetchBalances(accounts) - - for account in toSeq(self.accounts.values): - account.tokens = self.buildTokens(account, prices, balances{account.address}) - proc init*(self: Service) = + signalConnect(singletonInstance.localAccountSensitiveSettings, "isWalletEnabledChanged()", self, "onIsWalletEnabledChanged()", 2) + try: let accounts = fetchAccounts() - for account in accounts: - self.accounts[account.address] = account + self.walletAccounts[account.address] = account - self.refreshBalances() + self.buildAllTokens(true) except Exception as e: let errDesription = e.msg error "error: ", errDesription return proc getAccountByAddress*(self: Service, address: string): WalletAccountDto = - if not self.accounts.hasKey(address): + if not self.walletAccounts.hasKey(address): return - - return self.accounts[address] + return self.walletAccounts[address] proc getWalletAccounts*(self: Service): seq[WalletAccountDto] = - return toSeq(self.accounts.values) + return toSeq(self.walletAccounts.values) proc getWalletAccount*(self: Service, accountIndex: int): WalletAccountDto = if(accountIndex < 0 or accountIndex >= self.getWalletAccounts().len): @@ -293,17 +181,13 @@ QtObject: proc addNewAccountToLocalStore(self: Service) = let accounts = fetchAccounts() - let prices = self.fetchPrices() - var newAccount = accounts[0] for account in accounts: - if not self.accounts.haskey(account.address): + if not self.walletAccounts.haskey(account.address): newAccount = account break - - let balances = self.fetchBalances(@[newAccount.address]) - newAccount.tokens = self.buildTokens(newAccount, prices, balances{newAccount.address}) - self.accounts[newAccount.address] = newAccount + self.walletAccounts[newAccount.address] = newAccount + self.buildAllTokens() self.events.emit(SIGNAL_WALLET_ACCOUNT_SAVED, AccountSaved(account: newAccount)) proc generateNewAccount*(self: Service, password: string, accountName: string, color: string, emoji: string, path: string, derivedFrom: string): string = @@ -363,35 +247,35 @@ QtObject: proc deleteAccount*(self: Service, address: string) = discard status_go_accounts.deleteAccount(address) - let accountDeleted = self.accounts[address] - self.accounts.del(address) + let accountDeleted = self.walletAccounts[address] + self.walletAccounts.del(address) self.events.emit(SIGNAL_WALLET_ACCOUNT_DELETED, AccountDeleted(account: accountDeleted)) proc updateCurrency*(self: Service, newCurrency: string) = discard self.settingsService.saveCurrency(newCurrency) - self.refreshBalances() + self.buildAllTokens() self.events.emit(SIGNAL_WALLET_ACCOUNT_CURRENCY_UPDATED, CurrencyUpdated()) proc toggleTokenVisible*(self: Service, chainId: int, address: string) = self.tokenService.toggleVisible(chainId, address) - self.refreshBalances() + self.buildAllTokens() self.events.emit(SIGNAL_WALLET_ACCOUNT_TOKEN_VISIBILITY_UPDATED, TokenVisibilityToggled()) proc toggleNetworkEnabled*(self: Service, chainId: int) = self.networkService.toggleNetwork(chainId) self.tokenService.init() - self.refreshBalances() + self.buildAllTokens() self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, NetwordkEnabledToggled()) method toggleTestNetworksEnabled*(self: Service) = - discard self.settings_service.toggleTestNetworksEnabled() + discard self.settingsService.toggleTestNetworksEnabled() self.tokenService.init() - self.refreshBalances() + self.buildAllTokens() self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, NetwordkEnabledToggled()) proc updateWalletAccount*(self: Service, address: string, accountName: string, color: string, emoji: string) = - let account = self.accounts[address] + let account = self.walletAccounts[address] status_go_accounts.updateAccount( accountName, account.address, @@ -452,4 +336,67 @@ QtObject: error: error )) + proc onStartBuildingTokensTimer*(self: Service, response: string) {.slot.} = + if self.ignoreTimeInitiatedTokensBuild: + self.ignoreTimeInitiatedTokensBuild = false + return + self.buildAllTokens(true) + + proc startBuildingTokensTimer(self: Service) = + if(self.closingApp): + return + + let arg = TimerTaskArg( + tptr: cast[ByteAddress](timerTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onStartBuildingTokensTimer", + timeoutInMilliseconds: CheckBalanceIntervalInMilliseconds + ) + self.threadpool.start(arg) + + proc onAllTokensBuilt*(self: Service, response: string) {.slot.} = + let responseObj = response.parseJson + if (responseObj.kind != JObject): + info "prepared tokens are not a json object" + return + + var data = TokensPerAccountArgs() + let walletAddresses = toSeq(self.walletAccounts.keys) + for wAddress in walletAddresses: + var tokensArr: JsonNode + var tokens: seq[WalletTokenDto] + if(responseObj.getProp(wAddress, tokensArr)): + tokens = map(tokensArr.getElems(), proc(x: JsonNode): WalletTokenDto = x.toWalletTokenDto()) + self.walletAccounts[wAddress].tokens = tokens + data.accountsTokens[wAddress] = tokens + + self.events.emit(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT, data) + + # run timer again... + self.startBuildingTokensTimer() + + proc buildAllTokens(self: Service, calledFromTimerOrInit = false) = + if(self.closingApp or not singletonInstance.localAccountSensitiveSettings.getIsWalletEnabled()): + return + + # Since we don't have a way to re-run TimerTaskArg (to stop it and run again), we introduced some flags which will + # just ignore buildAllTokens in case that proc is called by some action in the time window between two successive calls + # initiated by TimerTaskArg. + if not calledFromTimerOrInit: + self.ignoreTimeInitiatedTokensBuild = true + + let walletAddresses = toSeq(self.walletAccounts.keys) + + let arg = BuildTokensTaskArg( + tptr: cast[ByteAddress](prepareTokensTask), + vptr: cast[ByteAddress](self.vptr), + slot: "onAllTokensBuilt", + walletAddresses: walletAddresses, + currency: self.settingsService.getCurrency(), + networks: self.networkService.getEnabledNetworks() + ) + self.threadpool.start(arg) + + proc onIsWalletEnabledChanged*(self: Service) {.slot.} = + self.buildAllTokens() \ No newline at end of file