diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index a4d66a131ba..edeabb95277 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -168,7 +168,9 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.tokenService = token_service.newService( statusFoundation.events, statusFoundation.threadpool, result.networkService ) - result.currencyService = currency_service.newService(result.tokenService, result.settingsService) + result.currencyService = currency_service.newService( + statusFoundation.events, statusFoundation.threadpool, result.tokenService, result.settingsService + ) result.collectibleService = collectible_service.newService(statusFoundation.events, statusFoundation.threadpool, result.networkService) result.walletAccountService = wallet_account_service.newService( statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService, 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 f1dc5e101a3..af84f69039e 100644 --- a/src/app/modules/main/browser_section/current_account/module.nim +++ b/src/app/modules/main/browser_section/current_account/module.nim @@ -27,6 +27,7 @@ type currentAccountIndex: int proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[WalletTokenDto]]) +proc onCurrencyFormatsUpdated(self: Module) proc newModule*( delegate: delegate_interface.AccessInterface, @@ -105,6 +106,9 @@ method load*(self: Module) = let arg = TokensPerAccountArgs(e) self.onTokensRebuilt(arg.accountsTokens) + self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args): + self.onCurrencyFormatsUpdated() + self.controller.init() self.view.load() self.switchAccount(0) @@ -125,5 +129,10 @@ proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[Wall return self.setAssets(accountsTokens[walletAccount.address]) +proc onCurrencyFormatsUpdated(self: Module) = + # Update assets + let walletAccount = self.controller.getWalletAccount(self.currentAccountIndex) + self.setAssets(walletAccount.tokens) + method findTokenSymbolByAddress*(self: Module, address: string): string = return self.controller.findTokenSymbolByAddress(address) diff --git a/src/app/modules/main/wallet_section/accounts/module.nim b/src/app/modules/main/wallet_section/accounts/module.nim index 7d41a2c4efb..16bbffa4a5b 100644 --- a/src/app/modules/main/wallet_section/accounts/module.nim +++ b/src/app/modules/main/wallet_section/accounts/module.nim @@ -141,6 +141,9 @@ method load*(self: Module) = self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT) do(e:Args): self.refreshWalletAccounts() + self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args): + self.refreshWalletAccounts() + self.events.on(SIGNAL_NEW_KEYCARD_SET) do(e: Args): let args = KeycardActivityArgs(e) if not args.success: 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 cf218620f5c..c16ffdbdfdc 100644 --- a/src/app/modules/main/wallet_section/current_account/module.nim +++ b/src/app/modules/main/wallet_section/current_account/module.nim @@ -29,6 +29,7 @@ type currentAccountIndex: int proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[WalletTokenDto]]) +proc onCurrencyFormatsUpdated(self: Module) proc newModule*( delegate: delegate_interface.AccessInterface, @@ -68,6 +69,9 @@ method load*(self: Module) = self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT) do(e:Args): let arg = TokensPerAccountArgs(e) self.onTokensRebuilt(arg.accountsTokens) + + self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args): + self.onCurrencyFormatsUpdated() self.controller.init() self.view.load() @@ -153,6 +157,11 @@ proc onTokensRebuilt(self: Module, accountsTokens: OrderedTable[string, seq[Wall return self.setAssetsAndBalance(accountsTokens[walletAccount.address]) +proc onCurrencyFormatsUpdated(self: Module) = + # Update assets + let walletAccount = self.controller.getWalletAccount(self.currentAccountIndex) + self.setAssetsAndBalance(walletAccount.tokens) + method findTokenSymbolByAddress*(self: Module, address: string): string = return self.controller.findTokenSymbolByAddress(address) diff --git a/src/app/modules/main/wallet_section/module.nim b/src/app/modules/main/wallet_section/module.nim index e267d2f51a5..6ee4dcef0d2 100644 --- a/src/app/modules/main/wallet_section/module.nim +++ b/src/app/modules/main/wallet_section/module.nim @@ -121,6 +121,8 @@ method load*(self: Module) = self.events.on(SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT) do(e:Args): self.setTotalCurrencyBalance() self.view.setTokensLoading(false) + self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args): + self.setTotalCurrencyBalance() self.controller.init() self.view.load() diff --git a/src/app/modules/main/wallet_section/transactions/controller.nim b/src/app/modules/main/wallet_section/transactions/controller.nim index 718816278b4..09a8663ddff 100644 --- a/src/app/modules/main/wallet_section/transactions/controller.nim +++ b/src/app/modules/main/wallet_section/transactions/controller.nim @@ -83,6 +83,10 @@ proc init*(self: Controller) = let args = TransactionsLoadedArgs(e) self.delegate.setHistoryFetchState(args.address, isFetching = false) + self.events.on(SIGNAL_CURRENCY_FORMATS_UPDATED) do(e:Args): + # TODO: Rebuild Transaction items + discard + proc watchPendingTransactions*(self: Controller): seq[TransactionDto] = return self.transactionService.watchPendingTransactions() diff --git a/src/app/modules/shared_models/currency_amount_utils.nim b/src/app/modules/shared_models/currency_amount_utils.nim index befa27e45dc..93345a9c8a0 100644 --- a/src/app/modules/shared_models/currency_amount_utils.nim +++ b/src/app/modules/shared_models/currency_amount_utils.nim @@ -5,6 +5,6 @@ proc currencyAmountToItem*(amount: float64, format: CurrencyFormatDto) : Currenc return newCurrencyAmount( amount, format.symbol, - format.displayDecimals, + int(format.displayDecimals), format.stripTrailingZeroes ) diff --git a/src/app_service/common/json_utils.nim b/src/app_service/common/json_utils.nim index 55b93c99ef3..b0b0922bcb7 100644 --- a/src/app_service/common/json_utils.nim +++ b/src/app_service/common/json_utils.nim @@ -17,6 +17,14 @@ template getProp(obj: JsonNode, prop: string, value: var typedesc[int64]): bool success +template getProp(obj: JsonNode, prop: string, value: var typedesc[uint]): bool = + var success = false + if (obj.kind == JObject and obj.contains(prop)): + value = uint(obj[prop].getInt) + success = true + + success + template getProp(obj: JsonNode, prop: string, value: var typedesc[uint64]): bool = var success = false if (obj.kind == JObject and obj.contains(prop)): diff --git a/src/app_service/service/currency/async_tasks.nim b/src/app_service/service/currency/async_tasks.nim new file mode 100644 index 00000000000..3a551ce0d4c --- /dev/null +++ b/src/app_service/service/currency/async_tasks.nim @@ -0,0 +1,16 @@ +type + FetchAllCurrencyFormatsTaskArg = ref object of QObjectTaskArg + discard + +const fetchAllCurrencyFormatsTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[FetchAllCurrencyFormatsTaskArg](argEncoded) + let output = %* { + "formats": "" + } + try: + let response = backend.fetchAllCurrencyFormats() + output["formats"] = response.result + except Exception as e: + let errDesription = e.msg + error "error fetchAllCurrencyFormatsTaskArg: ", errDesription + arg.finish(output) diff --git a/src/app_service/service/currency/dto.nim b/src/app_service/service/currency/dto.nim index e7daa0ebcaa..c7eaf765e1c 100644 --- a/src/app_service/service/currency/dto.nim +++ b/src/app_service/service/currency/dto.nim @@ -1,6 +1,32 @@ +import json +include ../../common/json_utils type CurrencyFormatDto* = ref object symbol*: string - displayDecimals*: int + displayDecimals*: uint stripTrailingZeroes*: bool + +proc newCurrencyFormatDto*( + symbol: string, + displayDecimals: uint, + stripTrailingZeroes: bool, +): CurrencyFormatDto = + return CurrencyFormatDto( + symbol: symbol, + displayDecimals: displayDecimals, + stripTrailingZeroes: stripTrailingZeroes + ) + +proc newCurrencyFormatDto*(symbol: string = ""): CurrencyFormatDto = + return CurrencyFormatDto( + symbol: symbol, + displayDecimals: 8, + stripTrailingZeroes: true + ) + +proc toCurrencyFormatDto*(jsonObj: JsonNode): CurrencyFormatDto = + result = CurrencyFormatDto() + discard jsonObj.getProp("symbol", result.symbol) + discard jsonObj.getProp("displayDecimals", result.displayDecimals) + discard jsonObj.getProp("stripTrailingZeroes", result.stripTrailingZeroes) \ No newline at end of file diff --git a/src/app_service/service/currency/service.nim b/src/app_service/service/currency/service.nim index 61eb5b1ad16..63718428f45 100644 --- a/src/app_service/service/currency/service.nim +++ b/src/app_service/service/currency/service.nim @@ -1,79 +1,102 @@ -import NimQml, strformat, strutils, tables +import NimQml, strformat, chronicles, strutils, tables, json + +import ../../../backend/backend as backend + +import ../../../app/core/eventemitter +import ../../../app/core/tasks/[qt, threadpool] +import ../../../app/core/signals/types + import ../settings/service as settings_service import ../token/service as token_service -import ./dto, ./utils -import ../../common/cache +import ./dto + +include ../../common/json_utils +include async_tasks export dto -const DECIMALS_CALCULATION_CURRENCY = "USD" +# Signals which may be emitted by this service: +const SIGNAL_CURRENCY_FORMATS_UPDATED* = "currencyFormatsUpdated" + +type + CurrencyFormatsUpdatedArgs* = ref object of Args + discard QtObject: type Service* = ref object of QObject + events: EventEmitter + threadpool: ThreadPool tokenService: token_service.Service settingsService: settings_service.Service - isCurrencyFiatCache: Table[string, bool] # Fiat info does not change, we can fetch/calculate once and - fiatCurrencyFormatCache: Table[string, CurrencyFormatDto] # keep the results forever. - tokenCurrencyFormatCache: TimedCache[CurrencyFormatDto] # Token format changes with price, so we use a timed cache. + currencyFormatCache: Table[string, CurrencyFormatDto] + + # Forward declarations + proc fetchAllCurrencyFormats(self: Service) + proc getCachedCurrencyFormats(self: Service): Table[string, CurrencyFormatDto] proc delete*(self: Service) = self.QObject.delete proc newService*( + events: EventEmitter, + threadpool: ThreadPool, tokenService: token_service.Service, settingsService: settings_service.Service, ): Service = new(result, delete) result.QObject.setup + result.events = events + result.threadpool = threadpool result.tokenService = tokenService result.settingsService = settingsService - result.tokenCurrencyFormatCache = newTimedCache[CurrencyFormatDto]() proc init*(self: Service) = - discard + self.events.on(SignalType.Wallet.event) do(e:Args): + var data = WalletSignal(e) + case data.eventType: + of "wallet-currency-tick-update-format": + self.fetchAllCurrencyFormats() + discard + # Load cache from DB + self.currencyFormatCache = self.getCachedCurrencyFormats() + # Trigger async fetch + self.fetchAllCurrencyFormats() + + proc jsonToFormatsTable(node: JsonNode) : Table[string, CurrencyFormatDto] = + result = initTable[string, CurrencyFormatDto]() + + for (symbol, formatObj) in node.pairs: + result[symbol] = formatObj.toCurrencyFormatDto() + + proc getCachedCurrencyFormats(self: Service): Table[string, CurrencyFormatDto] = + try: + let response = backend.getCachedCurrencyFormats() + result = jsonToFormatsTable(response.result) + except Exception as e: + let errDesription = e.msg + error "error getCachedCurrencyFormats: ", errDesription + + proc onAllCurrencyFormatsFetched(self: Service, response: string) {.slot.} = + try: + let responseObj = response.parseJson + if (responseObj.kind == JObject): + let formatsPerSymbol = jsonToFormatsTable(responseObj) + for symbol, format in formatsPerSymbol: + self.currencyFormatCache[symbol] = format + self.events.emit(SIGNAL_CURRENCY_FORMATS_UPDATED, CurrencyFormatsUpdatedArgs()) + except Exception as e: + let errDescription = e.msg + error "error onAllCurrencyFormatsFetched: ", errDescription - proc isCurrencyFiat(self: Service, symbol: string): bool = - if not self.isCurrencyFiatCache.hasKey(symbol): - self.isCurrencyFiatCache[symbol] = isCurrencyFiat(symbol) - return self.isCurrencyFiatCache[symbol] - - proc getFiatCurrencyFormat(self: Service, symbol: string): CurrencyFormatDto = - if not self.fiatCurrencyFormatCache.hasKey(symbol): - self.fiatCurrencyFormatCache[symbol] = CurrencyFormatDto( - symbol: toUpperAscii(symbol), - displayDecimals: getFiatDisplayDecimals(symbol), - stripTrailingZeroes: false - ) - return self.fiatCurrencyFormatCache[symbol] - - proc getTokenCurrencyFormat(self: Service, symbol: string): CurrencyFormatDto = - if self.tokenCurrencyFormatCache.isCached(symbol): - return self.tokenCurrencyFormatCache.get(symbol) - - var updateCache = true - let pegSymbol = self.tokenService.getTokenPegSymbol(symbol) - if pegSymbol != "": - let currencyFormat = self.getFiatCurrencyFormat(pegSymbol) - result = CurrencyFormatDto( - symbol: symbol, - displayDecimals: currencyFormat.displayDecimals, - stripTrailingZeroes: currencyFormat.stripTrailingZeroes - ) - updateCache = true - else: - let price = self.tokenService.getCachedTokenPrice(symbol, DECIMALS_CALCULATION_CURRENCY, true) - result = CurrencyFormatDto( - symbol: symbol, - displayDecimals: getTokenDisplayDecimals(price), - stripTrailingZeroes: true - ) - updateCache = self.tokenService.isCachedTokenPriceRecent(symbol, DECIMALS_CALCULATION_CURRENCY) - - if updateCache: - self.tokenCurrencyFormatCache.set(symbol, result) + proc fetchAllCurrencyFormats(self: Service) = + let arg = FetchAllCurrencyFormatsTaskArg( + tptr: cast[ByteAddress](fetchAllCurrencyFormatsTaskArg), + vptr: cast[ByteAddress](self.vptr), + slot: "onAllCurrencyFormatsFetched", + ) + self.threadpool.start(arg) proc getCurrencyFormat*(self: Service, symbol: string): CurrencyFormatDto = - if self.isCurrencyFiat(symbol): - return self.getFiatCurrencyFormat(symbol) - else: - return self.getTokenCurrencyFormat(symbol) + if not self.currencyFormatCache.hasKey(symbol): + return newCurrencyFormatDto(symbol) + return self.currencyFormatCache[symbol] \ No newline at end of file diff --git a/src/app_service/service/currency/utils.nim b/src/app_service/service/currency/utils.nim deleted file mode 100644 index 3a63396cae1..00000000000 --- a/src/app_service/service/currency/utils.nim +++ /dev/null @@ -1,33 +0,0 @@ -import math, chronicles, json, strutils -import ../../../backend/backend as backend - -logScope: - topics = "currency-utils" - -proc isCurrencyFiat*(symbol: string): bool = - let response = backend.isCurrencyFiat(symbol) - return response.result.getBool - -proc getFiatDisplayDecimals*(symbol: string): int = - result = 0 - try: - let response = backend.getFiatCurrencyMinorUnit(symbol) - result = response.result.getInt - except Exception as e: - let errDesription = e.msg - error "error getFiatDisplayDecimals: ", errDesription - -proc getTokenDisplayDecimals*(currencyPrice: float): int = - var decimals = 0.0 - if currencyPrice > 0: - const lowerCurrencyResolution = 0.1 - const higherCurrencyResolution = 0.01 - let lowerDecimalsBound = max(0.0, log10(currencyPrice) - log10(lowerCurrencyResolution)) - let upperDecimalsBound = max(0.0, log10(currencyPrice) - log10(higherCurrencyResolution)) - - # Use as few decimals as needed to ensure lower precision - decimals = ceil(lowerDecimalsBound) - if (decimals + 1 < upperDecimalsBound): - # If allowed by upper bound, ensure resolution changes as soon as currency hits multiple of 10 - decimals += 1 - return decimals.int diff --git a/src/app_service/service/settings/service.nim b/src/app_service/service/settings/service.nim index e1f0b169ecf..a288c331ea9 100644 --- a/src/app_service/service/settings/service.nim +++ b/src/app_service/service/settings/service.nim @@ -17,7 +17,7 @@ export settings_dto export stickers_dto # Default values: -const DEFAULT_CURRENCY* = "usd" +const DEFAULT_CURRENCY* = "USD" const DEFAULT_TELEMETRY_SERVER_URL* = "https://telemetry.status.im" const DEFAULT_FLEET* = $Fleet.StatusProd @@ -106,7 +106,7 @@ QtObject: if(self.settings.currency.len == 0): self.settings.currency = DEFAULT_CURRENCY - return self.settings.currency + return self.settings.currency.toUpperAscii() proc saveDappsAddress*(self: Service, value: string): bool = if(self.saveSetting(KEY_DAPPS_ADDRESS, value)): diff --git a/src/app_service/service/token/service.nim b/src/app_service/service/token/service.nim index 7b9129be107..7cbc1d508b8 100644 --- a/src/app_service/service/token/service.nim +++ b/src/app_service/service/token/service.nim @@ -50,7 +50,6 @@ QtObject: priceCache: TimedCache[float64] proc updateCachedTokenPrice(self: Service, crypto: string, fiat: string, price: float64) - proc initTokenPrices(self: Service, prices: Table[string, Table[string, float64]]) proc jsonToPricesMap(node: JsonNode): Table[string, Table[string, float64]] proc delete*(self: Service) = @@ -70,13 +69,6 @@ QtObject: result.priceCache = newTimedCache[float64]() proc init*(self: Service) = - try: - let response = backend.getCachedPrices() - let prices = jsonToPricesMap(response.result) - self.initTokenPrices(prices) - except Exception as e: - error "Cached prices init error", errDesription = e.msg - try: let networks = self.networkService.getNetworks() @@ -148,14 +140,6 @@ QtObject: for (currency, price) in pricePerCurrency.pairs: result[symbol][currency] = price.getFloat - proc initTokenPrices(self: Service, prices: Table[string, Table[string, float64]]) = - var cacheTable: Table[string, float64] - for token, pricesPerCurrency in prices: - for currency, price in pricesPerCurrency: - let cacheKey = getTokenPriceCacheKey(token, currency) - cacheTable[cacheKey] = price - self.priceCache.init(cacheTable) - proc updateTokenPrices*(self: Service, tokens: seq[WalletTokenDto]) = # Use data fetched by walletAccountService to update local price cache for token in tokens: diff --git a/src/backend/backend.nim b/src/backend/backend.nim index bfc78579e9a..fd54f4ee640 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -105,9 +105,6 @@ rpc(fetchPrices, "wallet"): symbols: seq[string] currencies: seq[string] -rpc(getCachedPrices, "wallet"): - discard - rpc(generateAccountWithDerivedPath, "accounts"): password: string name: string @@ -292,8 +289,8 @@ rpc(getBalanceHistory, "wallet"): address: string timeInterval: int -rpc(isCurrencyFiat, "wallet"): - code: string +rpc(getCachedCurrencyFormats, "wallet"): + discard -rpc(getFiatCurrencyMinorUnit, "wallet"): - code: string +rpc(fetchAllCurrencyFormats, "wallet"): + discard