From 98e3788bdbb54fa12979b31202a80ce17a1288a9 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Fri, 20 Oct 2023 10:19:48 +0200 Subject: [PATCH] chore(@desktop/wallet): Wallet: explore streamlining "all tokens model" spread across different sections closes #12424 --- src/app/modules/main/communities/module.nim | 2 +- src/app/modules/main/module.nim | 4 +- .../ens_usernames/controller.nim | 2 +- src/app/modules/main/stickers/controller.nim | 2 +- .../wallet_section/activity/controller.nim | 3 +- .../all_tokens/address_per_chain_model.nim | 55 ++++++ .../wallet_section/all_tokens/controller.nim | 11 +- .../all_tokens/flat_tokens_model.nim | 110 +++++++++++ .../all_tokens/io_interface.nim | 23 +++ .../main/wallet_section/all_tokens/module.nim | 33 +++- .../all_tokens/sources_of_tokens_model.nim | 76 ++++++++ .../all_tokens/token_by_symbol_model.nim | 110 +++++++++++ .../main/wallet_section/all_tokens/view.nim | 52 +++++- .../wallet_section/networks/controller.nim | 6 +- .../main/wallet_section/networks/module.nim | 8 +- .../main/wallet_section/networks/view.nim | 23 ++- src/app_service/common/types.nim | 9 +- src/app_service/service/ens/service.nim | 4 +- src/app_service/service/message/service.nim | 4 +- src/app_service/service/network/service.nim | 12 ++ src/app_service/service/stickers/service.nim | 4 +- src/app_service/service/token/async_tasks.nim | 27 ++- src/app_service/service/token/dto.nim | 81 ++++---- src/app_service/service/token/service.nim | 174 +++++++++++++++--- .../service/token/service_items.nim | 93 ++++++++++ .../service/transaction/service.nim | 2 +- .../wallet_account/service_account.nim | 2 - src/backend/backend.nim | 3 + .../pages/SupportedTokenListsPanelPage.qml | 4 +- storybook/pages/TokenListPopupPage.qml | 9 +- .../panels/SupportedTokenListsPanel.qml | 5 +- .../Profile/popups/TokenListPopup.qml | 6 +- .../AppLayouts/Wallet/stores/TokensStore.qml | 26 ++- ui/imports/utils/Constants.qml | 4 +- vendor/status-go | 2 +- 35 files changed, 862 insertions(+), 129 deletions(-) create mode 100644 src/app/modules/main/wallet_section/all_tokens/address_per_chain_model.nim create mode 100644 src/app/modules/main/wallet_section/all_tokens/flat_tokens_model.nim create mode 100644 src/app/modules/main/wallet_section/all_tokens/sources_of_tokens_model.nim create mode 100644 src/app/modules/main/wallet_section/all_tokens/token_by_symbol_model.nim create mode 100644 src/app_service/service/token/service_items.nim diff --git a/src/app/modules/main/communities/module.nim b/src/app/modules/main/communities/module.nim index 0798f69c6fb..b6f9a4d22d1 100644 --- a/src/app/modules/main/communities/module.nim +++ b/src/app/modules/main/communities/module.nim @@ -480,7 +480,7 @@ proc buildTokensAndCollectiblesFromWallet(self: Module) = key = token.symbol, name = token.name, symbol = token.symbol, - color = token.color, + color = "", communityId = token.communityId, image = "", category = ord(TokenListItemCategory.General), diff --git a/src/app/modules/main/module.nim b/src/app/modules/main/module.nim index b84674ad1e6..0f96894a8c0 100644 --- a/src/app/modules/main/module.nim +++ b/src/app/modules/main/module.nim @@ -245,7 +245,7 @@ method delete*[T](self: Module[T]) = self.view.delete self.viewVariant.delete -proc createTokenItem[T](self: Module[T], tokenDto: CommunityTokenDto) : TokenItem = +proc createTokenItem[T](self: Module[T], tokenDto: CommunityTokenDto) : token_item.TokenItem = let network = self.controller.getNetwork(tokenDto.chainId) let tokenOwners = self.controller.getCommunityTokenOwners(tokenDto.communityId, tokenDto.chainId, tokenDto.address) let ownerAddressName = if len(tokenDto.deployer) > 0: self.controller.getCommunityTokenOwnerName(tokenDto.deployer) else: "" @@ -255,7 +255,7 @@ proc createTokenItem[T](self: Module[T], tokenDto: CommunityTokenDto) : TokenIte let destructedAmount = self.controller.getRemoteDestructedAmount(tokenDto.chainId, tokenDto.address) result = initTokenItem(tokenDto, network, tokenOwners, ownerAddressName, burnState, remoteDestructedAddresses, remainingSupply, destructedAmount) -proc createTokenItemImproved[T](self: Module[T], tokenDto: CommunityTokenDto, communityTokenJsonItems: JsonNode) : TokenItem = +proc createTokenItemImproved[T](self: Module[T], tokenDto: CommunityTokenDto, communityTokenJsonItems: JsonNode) : token_item.TokenItem = # These 3 values come from local caches so they can be done sync let network = self.controller.getNetwork(tokenDto.chainId) let tokenOwners = self.controller.getCommunityTokenOwners(tokenDto.communityId, tokenDto.chainId, tokenDto.address) diff --git a/src/app/modules/main/profile_section/ens_usernames/controller.nim b/src/app/modules/main/profile_section/ens_usernames/controller.nim index a0f087ad235..f0446b46a45 100644 --- a/src/app/modules/main/profile_section/ens_usernames/controller.nim +++ b/src/app/modules/main/profile_section/ens_usernames/controller.nim @@ -158,7 +158,7 @@ proc getStatusToken*(self: Controller): string = let jsonObj = %* { "name": token.name, "symbol": token.symbol, - "address": token.addressAsString() + "address": token.address } return $jsonObj diff --git a/src/app/modules/main/stickers/controller.nim b/src/app/modules/main/stickers/controller.nim index 114fa459e1f..8eb6aa4941c 100644 --- a/src/app/modules/main/stickers/controller.nim +++ b/src/app/modules/main/stickers/controller.nim @@ -168,7 +168,7 @@ proc getStatusToken*(self: Controller): string = let jsonObj = %* { "name": token.name, "symbol": token.symbol, - "address": token.addressAsString() + "address": token.address } return $jsonObj diff --git a/src/app/modules/main/wallet_section/activity/controller.nim b/src/app/modules/main/wallet_section/activity/controller.nim index 4445d0a3478..133aa142654 100644 --- a/src/app/modules/main/wallet_section/activity/controller.nim +++ b/src/app/modules/main/wallet_section/activity/controller.nim @@ -18,6 +18,7 @@ import app/core/signals/types import backend/activity as backend_activity import backend/backend as backend +import app_service/common/conversion import app_service/service/currency/service as currency_service import app_service/service/transaction/service as transaction_service import app_service/service/token/service as token_service @@ -361,7 +362,7 @@ QtObject: assets.add(backend_activity.Token( tokenType: tokenType, chainId: backend_activity.ChainId(token.chainId), - address: some(token.address) + address: some(parseAddress(token.address)) )) self.currentActivityFilter.assets = assets diff --git a/src/app/modules/main/wallet_section/all_tokens/address_per_chain_model.nim b/src/app/modules/main/wallet_section/all_tokens/address_per_chain_model.nim new file mode 100644 index 00000000000..4507fd0763a --- /dev/null +++ b/src/app/modules/main/wallet_section/all_tokens/address_per_chain_model.nim @@ -0,0 +1,55 @@ +import NimQml, Tables, strutils + +import ./io_interface + +type + ModelRole {.pure.} = enum + ChainId = UserRole + 1 + Address + +QtObject: + type AddressPerChainModel* = ref object of QAbstractListModel + delegate: io_interface.TokenBySymbolModelDataSource + index: int + + proc setup(self: AddressPerChainModel) = + self.QAbstractListModel.setup + self.index = 0 + + proc delete(self: AddressPerChainModel) = + self.QAbstractListModel.delete + + proc newAddressPerChainModel*(delegate: io_interface.TokenBySymbolModelDataSource, index: int): AddressPerChainModel = + new(result, delete) + result.setup + result.delegate = delegate + result.index = index + + method rowCount(self: AddressPerChainModel, index: QModelIndex = nil): int = + return self.delegate.getTokenBySymbolList()[self.index].addressPerChainId.len + + proc countChanged(self: AddressPerChainModel) {.signal.} + proc getCount(self: AddressPerChainModel): int {.slot.} = + return self.rowCount() + QtProperty[int] count: + read = getCount + notify = countChanged + + method roleNames(self: AddressPerChainModel): Table[int, string] = + { + ModelRole.ChainId.int:"chainId", + ModelRole.Address.int:"address", + }.toTable + + method data(self: AddressPerChainModel, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.rowCount(): + return + let item = self.delegate.getTokenBySymbolList()[self.index].addressPerChainId[index.row] + let enumRole = role.ModelRole + case enumRole: + of ModelRole.ChainId: + result = newQVariant(item.chainId) + of ModelRole.Address: + result = newQVariant(item.address) diff --git a/src/app/modules/main/wallet_section/all_tokens/controller.nim b/src/app/modules/main/wallet_section/all_tokens/controller.nim index 614771c1a90..8115a9de562 100644 --- a/src/app/modules/main/wallet_section/all_tokens/controller.nim +++ b/src/app/modules/main/wallet_section/all_tokens/controller.nim @@ -44,4 +44,13 @@ method getHistoricalDataForToken*(self: Controller, symbol: string, currency: st self.tokenService.getHistoricalDataForToken(symbol, currency, range) method fetchHistoricalBalanceForTokenAsJson*(self: Controller, address: string, tokenSymbol: string, currencySymbol: string, timeIntervalEnum: int) = - self.tokenService.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, BalanceHistoryTimeInterval(timeIntervalEnum)) \ No newline at end of file + self.tokenService.fetchHistoricalBalanceForTokenAsJson(address, tokenSymbol, currencySymbol, BalanceHistoryTimeInterval(timeIntervalEnum)) + +proc getSourcesOfTokensList*(self: Controller): var seq[SupportedSourcesItem] = + return self.tokenService.getSourcesOfTokensList() + +proc getFlatTokensList*(self: Controller): var seq[TokenItem] = + return self.tokenService.getFlatTokensList() + +proc getTokenBySymbolList*(self: Controller): var seq[TokenBySymbolItem] = + return self.tokenService.getTokenBySymbolList() diff --git a/src/app/modules/main/wallet_section/all_tokens/flat_tokens_model.nim b/src/app/modules/main/wallet_section/all_tokens/flat_tokens_model.nim new file mode 100644 index 00000000000..a0bd3818e05 --- /dev/null +++ b/src/app/modules/main/wallet_section/all_tokens/flat_tokens_model.nim @@ -0,0 +1,110 @@ +import NimQml, Tables, strutils + +import ./io_interface + +type + ModelRole {.pure.} = enum + Key = UserRole + 1 + Name + Symbol + # uniswap/status/custom seq[string] + Sources + ChainId + Address + Decimals + Image + # Native, Erc20, Erc721 + Type + # only be valid if source is custom + CommunityId + # everything below should be lazy loaded + Description + # properties below this are optional and may not exist in case of community minted assets + # built from chainId and address using networks service + WebsiteUrl + MarketValues + +QtObject: + type FlatTokensModel* = ref object of QAbstractListModel + delegate: io_interface.FlatTokenModelDataSource + + proc setup(self: FlatTokensModel) = + self.QAbstractListModel.setup + + proc delete(self: FlatTokensModel) = + self.QAbstractListModel.delete + + proc newFlatTokensModel*(delegate: io_interface.FlatTokenModelDataSource): FlatTokensModel = + new(result, delete) + result.setup + result.delegate = delegate + + method rowCount(self: FlatTokensModel, index: QModelIndex = nil): int = + return self.delegate.getFlatTokensList().len + + proc countChanged(self: FlatTokensModel) {.signal.} + proc getCount(self: FlatTokensModel): int {.slot.} = + return self.rowCount() + QtProperty[int] count: + read = getCount + notify = countChanged + + method roleNames(self: FlatTokensModel): Table[int, string] = + { + ModelRole.Key.int:"key", + ModelRole.Name.int:"name", + ModelRole.Symbol.int:"symbol", + ModelRole.Sources.int:"sources", + ModelRole.ChainId.int:"chainId", + ModelRole.Address.int:"address", + ModelRole.Decimals.int:"decimals", + ModelRole.Image.int:"image", + ModelRole.Type.int:"type", + ModelRole.CommunityId.int:"communityId", + ModelRole.Description.int:"description", + ModelRole.WebsiteUrl.int:"websiteUrl", + ModelRole.MarketValues.int:"marketValues", + }.toTable + + method data(self: FlatTokensModel, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.rowCount(): + return + # the only way to read items from service is by this single method getFlatTokensList + let item = self.delegate.getFlatTokensList()[index.row] + let enumRole = role.ModelRole + case enumRole: + of ModelRole.Key: + result = newQVariant(item.key) + of ModelRole.Name: + result = newQVariant(item.name) + of ModelRole.Symbol: + result = newQVariant(item.symbol) + of ModelRole.Sources: + result = newQVariant(item.sources.join(";")) + of ModelRole.ChainId: + result = newQVariant(item.chainId) + of ModelRole.Address: + result = newQVariant(item.address) + of ModelRole.Decimals: + result = newQVariant(item.decimals) + of ModelRole.Image: + result = newQVariant(item.image) + of ModelRole.Type: + result = newQVariant(ord(item.tokenType)) + of ModelRole.CommunityId: + result = newQVariant(item.communityId) + # ToDo fetching of market values not done yet + of ModelRole.Description: + result = newQVariant("") + of ModelRole.WebsiteUrl: + result = newQVariant("") + of ModelRole.MarketValues: + result = newQVariant("") + + proc modelAboutToUpdate*(self: FlatTokensModel) = + self.beginResetModel() + + proc modelUpdated*(self: FlatTokensModel) = + self.endResetModel() diff --git a/src/app/modules/main/wallet_section/all_tokens/io_interface.nim b/src/app/modules/main/wallet_section/all_tokens/io_interface.nim index 88689706bc0..32e12eb4542 100644 --- a/src/app/modules/main/wallet_section/all_tokens/io_interface.nim +++ b/src/app/modules/main/wallet_section/all_tokens/io_interface.nim @@ -1,3 +1,17 @@ +import app_service/service/token/service_items + +type + SourcesOfTokensModelDataSource* = tuple[ + getSourcesOfTokensList: proc(): var seq[SupportedSourcesItem] + ] +type + FlatTokenModelDataSource* = tuple[ + getFlatTokensList: proc(): var seq[TokenItem] + ] +type + TokenBySymbolModelDataSource* = tuple[ + getTokenBySymbolList: proc(): var seq[TokenBySymbolItem] + ] type AccessInterface* {.pure inheritable.} = ref object of RootObj ## Abstract class for any input/interaction with this module. @@ -26,6 +40,15 @@ method fetchHistoricalBalanceForTokenAsJson*(self: AccessInterface, address: str method tokenBalanceHistoryDataResolved*(self: AccessInterface, balanceHistoryJson: string) {.base.} = raise newException(ValueError, "No implementation available") +method getSourcesOfTokensModelDataSource*(self: AccessInterface): SourcesOfTokensModelDataSource {.base.} = + raise newException(ValueError, "No implementation available") + +method getFlatTokenModelDataSource*(self: AccessInterface): FlatTokenModelDataSource {.base.} = + raise newException(ValueError, "No implementation available") + +method getTokenBySymbolModelDataSource*(self: AccessInterface): TokenBySymbolModelDataSource {.base.} = + raise newException(ValueError, "No implementation available") + # View Delegate Interface # Delegate for the view must be declared here due to use of QtObject and multi # inheritance, which is not well supported in Nim. diff --git a/src/app/modules/main/wallet_section/all_tokens/module.nim b/src/app/modules/main/wallet_section/all_tokens/module.nim index 000f02f4c38..74a23e9efb6 100644 --- a/src/app/modules/main/wallet_section/all_tokens/module.nim +++ b/src/app/modules/main/wallet_section/all_tokens/module.nim @@ -39,6 +39,12 @@ method delete*(self: Module) = method load*(self: Module) = singletonInstance.engine.setRootContextProperty("walletSectionAllTokens", newQVariant(self.view)) + # Passing on the events for cahnegs in model to abstract model + self.events.on(SIGNAL_TOKEN_LIST_ABOUT_TO_BE_UPDATED) do(e: Args): + self.view.modelAboutToUpdate() + self.events.on(SIGNAL_TOKEN_LIST_UPDATED) do(e: Args): + self.view.modelUpdated() + self.controller.init() self.view.load() @@ -62,9 +68,34 @@ method getHistoricalDataForToken*(self: Module, symbol: string, currency: string method tokenHistoricalDataResolved*(self: Module, tokenDetails: string) = self.view.setTokenHistoricalDataReady(tokenDetails) - 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) + +method getFlatTokensList*(self: Module): var seq[TokenItem] = + return self.controller.getFlatTokensList() + +method getTokenBySymbolList*(self: Module): var seq[TokenBySymbolItem] = + return self.controller.getTokenBySymbolList() + +method getSourcesOfTokensList*(self: Module): var seq[SupportedSourcesItem] = + return self.controller.getSourcesOfTokensList() + +# Interfaces for getting lists from the service files into the abstract models + +method getSourcesOfTokensModelDataSource*(self: Module): SourcesOfTokensModelDataSource = + return ( + getSourcesOfTokensList: proc(): var seq[SupportedSourcesItem] = self.getSourcesOfTokensList() + ) + +method getFlatTokenModelDataSource*(self: Module): FlatTokenModelDataSource = + return ( + getFlatTokensList: proc(): var seq[TokenItem] = self.getFlatTokensList() + ) + +method getTokenBySymbolModelDataSource*(self: Module): TokenBySymbolModelDataSource = + return ( + getTokenBySymbolList: proc(): var seq[TokenBySymbolItem] = self.getTokenBySymbolList() + ) diff --git a/src/app/modules/main/wallet_section/all_tokens/sources_of_tokens_model.nim b/src/app/modules/main/wallet_section/all_tokens/sources_of_tokens_model.nim new file mode 100644 index 00000000000..dc5b3c52327 --- /dev/null +++ b/src/app/modules/main/wallet_section/all_tokens/sources_of_tokens_model.nim @@ -0,0 +1,76 @@ +import NimQml, Tables + + +import ./io_interface + +type + ModelRole {.pure.} = enum + # key = name + Key = UserRole + 1 + Name + UpdatedAt + Source + Version + TokensCount + +QtObject: + type SourcesOfTokensModel* = ref object of QAbstractListModel + delegate: io_interface.SourcesOfTokensModelDataSource + + proc setup(self: SourcesOfTokensModel) = + self.QAbstractListModel.setup + + proc delete(self: SourcesOfTokensModel) = + self.QAbstractListModel.delete + + proc newSourcesOfTokensModel*(delegate: io_interface.SourcesOfTokensModelDataSource): SourcesOfTokensModel = + new(result, delete) + result.setup + result.delegate = delegate + + method rowCount(self: SourcesOfTokensModel, index: QModelIndex = nil): int = + return self.delegate.getSourcesOfTokensList().len + + proc countChanged(self: SourcesOfTokensModel) {.signal.} + proc getCount(self: SourcesOfTokensModel): int {.slot.} = + return self.rowCount() + QtProperty[int] count: + read = getCount + notify = countChanged + + method roleNames(self: SourcesOfTokensModel): Table[int, string] = + { + ModelRole.Key.int:"key", + ModelRole.Name.int:"name", + ModelRole.UpdatedAt.int:"updatedAt", + ModelRole.Source.int:"source", + ModelRole.Version.int:"version", + ModelRole.TokensCount.int:"tokensCount", + }.toTable + + method data(self: SourcesOfTokensModel, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.rowCount(): + return + let item = self.delegate.getSourcesOfTokensList()[index.row] + let enumRole = role.ModelRole + case enumRole: + of ModelRole.Key: + result = newQVariant(item.name) + of ModelRole.Name: + result = newQVariant(item.name) + of ModelRole.UpdatedAt: + result = newQVariant(item.updatedAt) + of ModelRole.Source: + result = newQVariant(item.source) + of ModelRole.Version: + result = newQVariant(item.version) + of ModelRole.TokensCount: + result = newQVariant(item.tokensCount) + + proc modelAboutToUpdate*(self: SourcesOfTokensModel) = + self.beginResetModel() + + proc modelUpdated*(self: SourcesOfTokensModel) = + self.endResetModel() diff --git a/src/app/modules/main/wallet_section/all_tokens/token_by_symbol_model.nim b/src/app/modules/main/wallet_section/all_tokens/token_by_symbol_model.nim new file mode 100644 index 00000000000..1944711dcc3 --- /dev/null +++ b/src/app/modules/main/wallet_section/all_tokens/token_by_symbol_model.nim @@ -0,0 +1,110 @@ +import NimQml, Tables, strutils + +import ./io_interface, ./address_per_chain_model + +type + ModelRole {.pure.} = enum + Key = UserRole + 1 + Name + Symbol + # uniswap/status/custom seq[string] + Sources + AddressPerChain + Decimals + Image + # Native, Erc20, Erc721 + Type + # only be valid if source is custom + CommunityId + # everything below should be lazy loaded + Description + # properties below this are optional and may not exist in case of community minted assets + # built from chainId and address using networks service + WebsiteUrl + MarketValues + +QtObject: + type TokensBySymbolModel* = ref object of QAbstractListModel + delegate: io_interface.TokenBySymbolModelDataSource + addressPerChainModel: seq[AddressPerChainModel] + + proc setup(self: TokensBySymbolModel) = + self.QAbstractListModel.setup + self.addressPerChainModel = @[] + + proc delete(self: TokensBySymbolModel) = + self.QAbstractListModel.delete + + proc newTokensBySymbolModel*(delegate: io_interface.TokenBySymbolModelDataSource): TokensBySymbolModel = + new(result, delete) + result.setup + result.delegate = delegate + + method rowCount(self: TokensBySymbolModel, index: QModelIndex = nil): int = + return self.delegate.getTokenBySymbolList().len + + proc countChanged(self: TokensBySymbolModel) {.signal.} + proc getCount(self: TokensBySymbolModel): int {.slot.} = + return self.rowCount() + QtProperty[int] count: + read = getCount + notify = countChanged + + method roleNames(self: TokensBySymbolModel): Table[int, string] = + { + ModelRole.Key.int:"key", + ModelRole.Name.int:"name", + ModelRole.Symbol.int:"symbol", + ModelRole.Sources.int:"sources", + ModelRole.AddressPerChain.int:"addressPerChain", + ModelRole.Decimals.int:"decimals", + ModelRole.Image.int:"image", + ModelRole.Type.int:"type", + ModelRole.CommunityId.int:"communityId", + ModelRole.Description.int:"description", + ModelRole.WebsiteUrl.int:"websiteUrl", + ModelRole.MarketValues.int:"marketValues", + }.toTable + + method data(self: TokensBySymbolModel, index: QModelIndex, role: int): QVariant = + if not index.isValid: + return + if index.row < 0 or index.row >= self.rowCount(): + return + let item = self.delegate.getTokenBySymbolList()[index.row] + let enumRole = role.ModelRole + case enumRole: + of ModelRole.Key: + result = newQVariant(item.key) + of ModelRole.Name: + result = newQVariant(item.name) + of ModelRole.Symbol: + result = newQVariant(item.symbol) + of ModelRole.Sources: + result = newQVariant(item.sources.join(";")) + of ModelRole.AddressPerChain: + result = newQVariant(self.addressPerChainModel[index.row]) + of ModelRole.Decimals: + result = newQVariant(item.decimals) + of ModelRole.Image: + result = newQVariant(item.image) + of ModelRole.Type: + result = newQVariant(ord(item.tokenType)) + of ModelRole.CommunityId: + result = newQVariant(item.communityId) + # ToDo fetching of market values not done yet + of ModelRole.Description: + result = newQVariant("") + of ModelRole.WebsiteUrl: + result = newQVariant("") + of ModelRole.MarketValues: + result = newQVariant("") + + proc modelAboutToUpdate*(self: TokensBySymbolModel) = + self.beginResetModel() + + proc modelUpdated*(self: TokensBySymbolModel) = + self.addressPerChainModel = @[] + for index in countup(0, self.delegate.getTokenBySymbolList().len): + self.addressPerChainModel.add(newAddressPerChainModel(self.delegate,index)) + self.endResetModel() diff --git a/src/app/modules/main/wallet_section/all_tokens/view.nim b/src/app/modules/main/wallet_section/all_tokens/view.nim index 1e035a11451..01cadce4bc0 100644 --- a/src/app/modules/main/wallet_section/all_tokens/view.nim +++ b/src/app/modules/main/wallet_section/all_tokens/view.nim @@ -1,6 +1,6 @@ import NimQml, sequtils, sugar, strutils -import ./io_interface +import ./io_interface, ./sources_of_tokens_model, ./flat_tokens_model, ./token_by_symbol_model QtObject: type @@ -8,6 +8,16 @@ QtObject: delegate: io_interface.AccessInterface marketHistoryIsLoading: bool balanceHistoryIsLoading: bool + # This contains the different sources for the tokens list + # ex. uniswap list, status tokens list + sourcesOfTokensModel: SourcesOfTokensModel + # this list contains the complete list of tokens with separate + # entry per token which has a unique address + network pair */ + flatTokensModel: FlatTokensModel + # this list contains list of tokens grouped by symbol + # EXCEPTION: We may have different entries for the same symbol in case + # of symbol clash when minting community tokens + tokensBySymbolModel: TokensBySymbolModel proc delete*(self: View) = self.QObject.delete @@ -18,36 +28,33 @@ QtObject: result.delegate = delegate result.marketHistoryIsLoading = false result.balanceHistoryIsLoading = false + result.sourcesOfTokensModel = newSourcesOfTokensModel(delegate.getSourcesOfTokensModelDataSource()) + result.flatTokensModel = newFlatTokensModel(delegate.getFlatTokenModelDataSource()) + result.tokensBySymbolModel = newTokensBySymbolModel(delegate.getTokenBySymbolModelDataSource()) proc load*(self: View) = self.delegate.viewDidLoad() proc marketHistoryIsLoadingChanged*(self: View) {.signal.} - proc getMarketHistoryIsLoading(self: View): QVariant {.slot.} = return newQVariant(self.marketHistoryIsLoading) - proc setMarketHistoryIsLoading(self: View, isLoading: bool) = if self.marketHistoryIsLoading == isLoading: return self.marketHistoryIsLoading = isLoading self.marketHistoryIsLoadingChanged() - QtProperty[QVariant] marketHistoryIsLoading: read = getMarketHistoryIsLoading notify = marketHistoryIsLoadingChanged proc balanceHistoryIsLoadingChanged*(self: View) {.signal.} - proc getBalanceHistoryIsLoading(self: View): QVariant {.slot.} = return newQVariant(self.balanceHistoryIsLoading) - proc setBalanceHistoryIsLoading(self: View, isLoading: bool) = if self.balanceHistoryIsLoading == isLoading: return self.balanceHistoryIsLoading = isLoading self.balanceHistoryIsLoadingChanged() - QtProperty[QVariant] balanceHistoryIsLoading: read = getBalanceHistoryIsLoading notify = balanceHistoryIsLoadingChanged @@ -74,3 +81,34 @@ QtObject: proc setTokenBalanceHistoryDataReady*(self: View, balanceHistoryJson: string) = self.setBalanceHistoryIsLoading(false) self.tokenBalanceHistoryDataReady(balanceHistoryJson) + + proc sourcesOfTokensModelChanged*(self: View) {.signal.} + proc getSourcesOfTokensModel(self: View): QVariant {.slot.} = + return newQVariant(self.sourcesOfTokensModel) + QtProperty[QVariant] sourcesOfTokensModel: + read = getSourcesOfTokensModel + notify = sourcesOfTokensModelChanged + + proc flatTokensModelChanged*(self: View) {.signal.} + proc getFlatTokensModel(self: View): QVariant {.slot.} = + return newQVariant(self.flatTokensModel) + QtProperty[QVariant] flatTokensModel: + read = getFlatTokensModel + notify = flatTokensModelChanged + + proc tokensBySymbolModelChanged*(self: View) {.signal.} + proc getTokensBySymbolModel(self: View): QVariant {.slot.} = + return newQVariant(self.tokensBySymbolModel) + QtProperty[QVariant] tokensBySymbolModel: + read = getTokensBySymbolModel + notify = tokensBySymbolModelChanged + + proc modelAboutToUpdate*(self: View) = + self.sourcesOfTokensModel.modelAboutToUpdate() + self.flatTokensModel.modelAboutToUpdate() + self.tokensBySymbolModel.modelAboutToUpdate() + + proc modelUpdated*(self: View) = + self.sourcesOfTokensModel.modelUpdated() + self.flatTokensModel.modelUpdated() + self.tokensBySymbolModel.modelUpdated() diff --git a/src/app/modules/main/wallet_section/networks/controller.nim b/src/app/modules/main/wallet_section/networks/controller.nim index 8f0fe1ab664..86d5699d1f1 100644 --- a/src/app/modules/main/wallet_section/networks/controller.nim +++ b/src/app/modules/main/wallet_section/networks/controller.nim @@ -34,11 +34,11 @@ proc init*(self: Controller) = self.events.on(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED) do(e: Args): self.delegate.refreshNetworks() -proc getNetworks*(self: Controller): seq[NetworkDto] = - return self.networkService.getNetworks() +proc getFlatNetworks*(self: Controller): seq[NetworkDto] = + return self.networkService.getFlatNetworks() proc setNetworksState*(self: Controller, chainIds: seq[int], enabled: bool) = self.walletAccountService.setNetworksState(chainIds, enabled) proc areTestNetworksEnabled*(self: Controller): bool = - return self.settingsService.areTestNetworksEnabled() \ No newline at end of file + return self.settingsService.areTestNetworksEnabled() diff --git a/src/app/modules/main/wallet_section/networks/module.nim b/src/app/modules/main/wallet_section/networks/module.nim index 5aa738bf272..c8efe06ddb0 100644 --- a/src/app/modules/main/wallet_section/networks/module.nim +++ b/src/app/modules/main/wallet_section/networks/module.nim @@ -40,12 +40,12 @@ method delete*(self: Module) = method refreshNetworks*(self: Module) = self.view.setAreTestNetworksEnabled(self.controller.areTestNetworksEnabled()) - self.view.setItems(self.controller.getNetworks()) + self.view.setItems(self.controller.getFlatNetworks()) method load*(self: Module) = self.controller.init() - self.view.setAreTestNetworksEnabled(self.controller.areTestNetworksEnabled()) - self.view.load(self.controller.getNetworks()) + self.view.load() + self.refreshNetworks() method isLoaded*(self: Module): bool = return self.moduleLoaded @@ -64,4 +64,4 @@ method setNetworksState*(self: Module, chainIds: seq[int], enabled: bool) = self.controller.setNetworksState(chainIds, enabled) method getNetworkLayer*(self: Module, chainId: int): string = - return self.view.getNetworkLayer(chainId) \ No newline at end of file + return self.view.getNetworkLayer(chainId) diff --git a/src/app/modules/main/wallet_section/networks/view.nim b/src/app/modules/main/wallet_section/networks/view.nim index b0c1f1136e8..48d30d7373b 100644 --- a/src/app/modules/main/wallet_section/networks/view.nim +++ b/src/app/modules/main/wallet_section/networks/view.nim @@ -17,6 +17,7 @@ QtObject: enabled: Model layer1: Model layer2: Model + flatNetworks: Model areTestNetworksEnabled: bool proc setup(self: View) = @@ -32,6 +33,7 @@ QtObject: result.layer1 = newModel() result.layer2 = newModel() result.enabled = newModel() + result.flatNetworks = newModel() result.setup() proc areTestNetworksEnabledChanged*(self: View) {.signal.} @@ -74,6 +76,14 @@ QtObject: read = getLayer2 notify = layer2Changed + + proc flatNetworksChanged*(self: View) {.signal.} + proc getFlatNetworks(self: View): QVariant {.slot.} = + return newQVariant(self.flatNetworks) + QtProperty[QVariant] flatNetworks: + read = getFlatNetworks + notify = flatNetworksChanged + proc enabledChanged*(self: View) {.signal.} proc getEnabled(self: View): QVariant {.slot.} = @@ -105,18 +115,19 @@ QtObject: networkEnabledToUxEnabledState(n.enabled, allEnabled) )) - self.all.setItems(items) - self.layer1.setItems(items.filter(i => i.getLayer() == 1)) - self.layer2.setItems(items.filter(i => i.getLayer() == 2)) - self.enabled.setItems(items.filter(i => i.getIsEnabled())) + let filteredItems = items.filter(i => i.getIsTest() == self.areTestNetworksEnabled) + self.flatNetworks.setItems(items) + self.all.setItems(filteredItems) + self.layer1.setItems(filteredItems.filter(i => i.getLayer() == 1)) + self.layer2.setItems(filteredItems.filter(i => i.getLayer() == 2)) + self.enabled.setItems(filteredItems.filter(i => i.getIsEnabled())) self.allChanged() self.layer1Changed() self.layer2Changed() self.enabledChanged() - proc load*(self: View, networks: seq[NetworkDto]) = - self.setItems(networks) + proc load*(self: View) = self.delegate.viewDidLoad() proc toggleNetwork*(self: View, chainId: int) {.slot.} = diff --git a/src/app_service/common/types.nim b/src/app_service/common/types.nim index 4f50a512118..2e27a13329d 100644 --- a/src/app_service/common/types.nim +++ b/src/app_service/common/types.nim @@ -74,4 +74,11 @@ type ContractTransactionStatus* {.pure.} = enum Failed, InProgress, - Completed \ No newline at end of file + Completed + +# ToDo: Will be streamlines to single TokenType under https://github.com/status-im/status-desktop/pull/12654/files +type NewTokenType* {.pure.} = enum + Native = 0 + ERC20 = 1, + ERC721 = 2, + ERC1155 diff --git a/src/app_service/service/ens/service.nim b/src/app_service/service/ens/service.nim index a3f851a53fb..abd07da1a8f 100644 --- a/src/app_service/service/ens/service.nim +++ b/src/app_service/service/ens/service.nim @@ -414,8 +414,8 @@ QtObject: proc getSNTBalance*(self: Service): string = let token = self.getStatusToken() let account = self.walletAccountService.getWalletAccount(0).address - let balances = status_go_backend.getTokensBalancesForChainIDs(@[self.getChainId()], @[account], @[token.addressAsString()]).result - return ens_utils.hex2Token(balances{account}{token.addressAsString()}.getStr, token.decimals) + let balances = status_go_backend.getTokensBalancesForChainIDs(@[self.getChainId()], @[account], @[token.address]).result + return ens_utils.hex2Token(balances{account}{token.address}.getStr, token.decimals) proc resourceUrl*(self: Service, username: string): (string, string, string) = try: diff --git a/src/app_service/service/message/service.nim b/src/app_service/service/message/service.nim index 954d92765a4..95c1af4f427 100644 --- a/src/app_service/service/message/service.nim +++ b/src/app_service/service/message/service.nim @@ -414,11 +414,11 @@ QtObject: proc getTransactionDetails*(self: Service, message: MessageDto): (string, string) = let networksDto = self.networkService.getNetworks() - var token = newTokenDto(networksDto[0].nativeCurrencyName, networksDto[0].chainId, parseAddress(ZERO_ADDRESS), networksDto[0].nativeCurrencySymbol, networksDto[0].nativeCurrencyDecimals, true) + var token = self.tokenService.findTokenByAddress(networksDto[0].chainId, ZERO_ADDRESS) if message.transactionParameters.contract != "": for networkDto in networksDto: - let tokenFound = self.tokenService.findTokenByAddress(networkDto, parseAddress(message.transactionParameters.contract)) + let tokenFound = self.tokenService.findTokenByAddress(networkDto.chainId, message.transactionParameters.contract) if tokenFound == nil: continue diff --git a/src/app_service/service/network/service.nim b/src/app_service/service/network/service.nim index a55f7e53c8c..8583f8b93e2 100644 --- a/src/app_service/service/network/service.nim +++ b/src/app_service/service/network/service.nim @@ -58,6 +58,13 @@ proc resetNetworks*(self: Service) = proc getCombinedNetworks*(self: Service): seq[CombinedNetworkDto] = return self.fetchNetworks() +# TODO:: update the networks service to unify the model exposed from this service +# We currently have 3 types: combined, test/mainet and flat and probably can be optimized +proc getFlatNetworks*(self: Service): seq[NetworkDto] = + for network in self.fetchNetworks(): + result.add(network.test) + result.add(network.prod) + proc getNetworks*(self: Service): seq[NetworkDto] = let testNetworksEnabled = self.settingsService.areTestNetworksEnabled() @@ -67,6 +74,11 @@ proc getNetworks*(self: Service): seq[NetworkDto] = else: result.add(network.prod) +proc getAllNetworkChainIds*(self: Service): seq[int] = + for network in self.fetchNetworks(): + result.add(network.test.chainId) + result.add(network.prod.chainId) + proc upsertNetwork*(self: Service, network: NetworkDto): bool = let response = backend.addEthereumChain(backend.Network( chainId: network.chainId, diff --git a/src/app_service/service/stickers/service.nim b/src/app_service/service/stickers/service.nim index ebe97306f52..a8dc1ccf337 100644 --- a/src/app_service/service/stickers/service.nim +++ b/src/app_service/service/stickers/service.nim @@ -453,5 +453,5 @@ QtObject: let account = self.walletAccountService.getWalletAccount(0).address let network = self.networkService.getNetworkForStickers() - let balances = status_go_backend.getTokensBalancesForChainIDs(@[network.chainId], @[account], @[token.addressAsString()]).result - return ens_utils.hex2Token(balances{account}{token.addressAsString()}.getStr, token.decimals) + let balances = status_go_backend.getTokensBalancesForChainIDs(@[network.chainId], @[account], @[token.address]).result + return ens_utils.hex2Token(balances{account}{token.address}.getStr, token.decimals) diff --git a/src/app_service/service/token/async_tasks.nim b/src/app_service/service/token/async_tasks.nim index 6e04a90e2f1..ebd2d1298ae 100644 --- a/src/app_service/service/token/async_tasks.nim +++ b/src/app_service/service/token/async_tasks.nim @@ -1,8 +1,7 @@ import times -include ../../common/json_utils +import backend/backend as backend -import ../../../backend/backend as backend -import ./dto +include app_service/common/json_utils ################################################# # Async load transactions ################################################# @@ -10,6 +9,26 @@ import ./dto const DAYS_IN_WEEK = 7 const HOURS_IN_DAY = 24 +type + GetTokenListTaskArg = ref object of QObjectTaskArg + +const getSupportedTokenList*: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[GetTokenListTaskArg](argEncoded) + var response: RpcResponse[JsonNode] + try: + response = backend.getTokenList() + let output = %* { + "supportedTokensJson": response, + "error": "" + } + arg.finish(output) + except Exception as e: + let output = %* { + "supportedTokensJson": response, + "error": e.msg + } + arg.finish(output) + type GetTokenHistoricalDataTaskArg = ref object of QObjectTaskArg symbol: string @@ -98,4 +117,4 @@ const getTokenBalanceHistoryDataTask*: Task = proc(argEncoded: string) {.gcsafe, "timeInterval": int(arg.timeInterval), "error": e.msg, } - arg.finish(output) \ No newline at end of file + arg.finish(output) diff --git a/src/app_service/service/token/dto.nim b/src/app_service/service/token/dto.nim index bf2d6685bbc..f6f46096cbc 100644 --- a/src/app_service/service/token/dto.nim +++ b/src/app_service/service/token/dto.nim @@ -1,64 +1,67 @@ -import json +import json, strformat -include ../../common/json_utils +include app_service/common/json_utils -import - web3/ethtypes, json_serialization -from web3/conversions import `$` +import json_serialization +# TODO: remove once this is moved to wallet_accounts service const WEEKLY_TIME_RANGE* = 0 const MONTHLY_TIME_RANGE* = 1 const HALF_YEARLY_TIME_RANGE* = 2 const YEARLY_TIME_RANGE* = 3 const ALL_TIME_RANGE* = 4 +# Only contains DTO used for deserialisation of data from go lib + type TokenDto* = ref object of RootObj - name*: string - chainId*: int - address*: Address - symbol*: string - decimals*: int - hasIcon* {.dontSerialize.}: bool - color*: string - isCustom* {.dontSerialize.}: bool - isVisible* {.dontSerialize.}: bool - communityId*: string + address* {.serializedFieldName("address").}: string + name* {.serializedFieldName("name").}: string + symbol* {.serializedFieldName("symbol").}: string + decimals* {.serializedFieldName("decimals").}: int + chainID* {.serializedFieldName("chainId").}: int + communityID* {.serializedFieldName("communityId").}: string + +proc `$`*(self: TokenDto): string = + result = fmt"""TokenDto[ + address: {self.address}, + name: {self.name}, + symbol: {self.symbol}, + decimals: {self.decimals}, + chainID: {self.chainID}, + communityID: {self.communityID} + ]""" +# TODO: Remove after https://github.com/status-im/status-desktop/issues/12513 proc newTokenDto*( + address: string, name: string, - chainId: int, - address: Address, symbol: string, decimals: int, - hasIcon: bool, - isCustom: bool = false, + chainId: int, communityId: string = "" ): TokenDto = return TokenDto( - name: name, - chainId: chainId, address: address, + name: name, symbol: symbol, decimals: decimals, - hasIcon: hasIcon, - communityId: communityId, - isCustom: isCustom + chainId: chainId, + communityId: communityId ) -proc toTokenDto*(jsonObj: JsonNode, isVisible: bool, hasIcon: bool = false, isCustom: bool = true): TokenDto = - result = TokenDto() - result.isCustom = isCustom - result.hasIcon = hasIcon - - discard jsonObj.getProp("name", result.name) - discard jsonObj.getProp("chainId", result.chainId) - discard jsonObj.getProp("address", result.address) - discard jsonObj.getProp("symbol", result.symbol) - discard jsonObj.getProp("decimals", result.decimals) - discard jsonObj.getProp("color", result.color) - discard jsonObj.getProp("communityId", result.communityId) - result.isVisible = isVisible +type TokenSourceDto* = ref object of RootObj + name* {.serializedFieldName("name").}: string + tokens* {.serializedFieldName("tokens").}: seq[TokenDto] + updatedAt* {.serializedFieldName("updatedAt").}: int64 + source* {.serializedFieldName("source").}: string + version* {.serializedFieldName("version").}: string -proc addressAsString*(self: TokenDto): string = - return $self.address +proc `$`*(self: TokenSourceDto): string = + result = fmt"""TokenSourceDto[ + name: {self.name}, + tokens: {self.tokens}, + updatedAt: {self.updatedAt}, + source: {self.source}, + version: {self.version} + ]""" diff --git a/src/app_service/service/token/service.nim b/src/app_service/service/token/service.nim index 40902174ccb..8250e092f22 100644 --- a/src/app_service/service/token/service.nim +++ b/src/app_service/service/token/service.nim @@ -1,20 +1,19 @@ -import NimQml, Tables, json, sequtils, chronicles, strutils +import NimQml, Tables, json, sequtils, chronicles, strutils, strformat, algorithm import web3/ethtypes from web3/conversions import `$` -import ../../../backend/backend as backend +import backend/backend as backend -import ../network/service as network_service -import ../wallet_account/dto/account_dto as wallet_account_dto -import ../../../app/global/global_singleton +import app_service/service/network/service as network_service +import app_service/service/wallet_account/dto/account_dto -import ../../../app/core/eventemitter -import ../../../app/core/tasks/[qt, threadpool] -import ../../common/cache +import app/core/eventemitter +import app/core/tasks/[qt, threadpool] +import app_service/common/cache import ../../../constants as main_constants -import ./dto +import ./dto, ./service_items -export dto +export dto, service_items logScope: topics = "token-service" @@ -32,6 +31,8 @@ const CRYPTO_SUB_UNITS_TO_FACTOR = { # Signals which may be emitted by this service: const SIGNAL_TOKEN_HISTORICAL_DATA_LOADED* = "tokenHistoricalDataLoaded" const SIGNAL_BALANCE_HISTORY_DATA_READY* = "tokenBalanceHistoryDataReady" +const SIGNAL_TOKEN_LIST_UPDATED* = "tokensListUpdated" +const SIGNAL_TOKEN_LIST_ABOUT_TO_BE_UPDATED* = "tokensListAboutToBeUpdated" type TokenHistoricalDataArgs* = ref object of Args @@ -51,11 +52,21 @@ QtObject: events: EventEmitter threadpool: ThreadPool networkService: network_service.Service + + # TODO: remove these once community usage of this service is removed etc... tokens: Table[int, seq[TokenDto]] tokenList: seq[TokenDto] tokensToAddressesMap: Table[string, TokenData] + priceCache: TimedCache[float64] + sourcesOfTokensList: seq[SupportedSourcesItem] + flatTokenList: seq[TokenItem] + tokenBySymbolList: seq[TokenBySymbolItem] + # TODO: Table[symbol, TokenDetails] fetched from cryptocompare + tokenDetailsList: Table[string, TokenDetails] + + proc updateCachedTokenPrice(self: Service, crypto: string, fiat: string, price: float64) proc jsonToPricesMap(node: JsonNode): Table[string, Table[string, float64]] @@ -77,10 +88,106 @@ QtObject: result.tokenList = @[] result.tokensToAddressesMap = initTable[string, TokenData]() - proc loadData*(self: Service) = - if(not main_constants.WALLET_ENABLED): - return + result.sourcesOfTokensList = @[] + result.flatTokenList = @[] + result.tokenBySymbolList = @[] + result.tokenDetailsList = initTable[string, TokenDetails]() + + # Callback to process the response of getSupportedTokensList call + proc supportedTokensListRetrieved(self: Service, response: string) {.slot.} = + # this is emited so that the models can know that the seq it depends on has been updated + defer: self.events.emit(SIGNAL_TOKEN_LIST_UPDATED, Args()) + + let parsedJson = response.parseJson + var errorString: string + var supportedTokensJson, tokensResult: JsonNode + discard parsedJson.getProp("supportedTokensJson", supportedTokensJson) + discard parsedJson.getProp("supportedTokensJson", errorString) + discard supportedTokensJson.getProp("result", tokensResult) + + if not errorString.isEmptyOrWhitespace: + raise newException(Exception, "Error getting networks: " & errorString) + let sourcesList = if tokensResult.isNil or tokensResult.kind == JNull: @[] + else: Json.decode($tokensResult, seq[TokenSourceDto], allowUnknownFields = true) + + let supportedNetworkChains = self.networkService.getAllNetworkChainIds() + var flatTokensList: Table[string, TokenItem] = initTable[string, TokenItem]() + var tokenBySymbolList: Table[string, TokenBySymbolItem] = initTable[string, TokenBySymbolItem]() + + for s in sourcesList: + let newSource = SupportedSourcesItem(name: s.name, updatedAt: s.updatedAt, source: s.source, version: s.version, tokensCount: s.tokens.len) + self.sourcesOfTokensList.add(newSource) + + for token in s.tokens: + # Remove tokens that are not on list of supported status networks + if supportedNetworkChains.contains(token.chainID): + # logic for building flat tokens list + let unique_key = $token.chainID & token.address + if flatTokensList.hasKey(unique_key): + flatTokensList[unique_key].sources.add(s.name) + else: + let tokenType = if s.name == "native" : NewTokenType.Native + else: NewTokenType.ERC20 + flatTokensList[unique_key] = TokenItem( + key: unique_key, + name: token.name, + symbol: token.symbol, + sources: @[s.name], + chainID: token.chainID, + address: token.address, + decimals: token.decimals, + image: "", + tokenType: tokenType, + communityId: token.communityID) + + # logic for building tokens by symbol list + # In case the token is not a community token the unique key is symbol + # In case this is a community token the only param reliably unique is its address + # as there is always a rare case that a user can create two or more community token + # with same symbol and cannot be avoided + let token_by_symbol_key = if token.communityID.isEmptyOrWhitespace: token.symbol + else: token.address + if tokenBySymbolList.hasKey(token_by_symbol_key): + if not tokenBySymbolList[token_by_symbol_key].sources.contains(s.name): + tokenBySymbolList[token_by_symbol_key].sources.add(s.name) + # this logic is to check if an entry for same chainId as been made already, + # in that case we simply add it to address per chain + var addedChains: seq[int] = @[] + for addressPerChain in tokenBySymbolList[token_by_symbol_key].addressPerChainId: + addedChains.add(addressPerChain.chainId) + if not addedChains.contains(token.chainID): + tokenBySymbolList[token_by_symbol_key].addressPerChainId.add(AddressPerChain(chainId: token.chainID, address: token.address)) + else: + let tokenType = if s.name == "native": NewTokenType.Native + else: NewTokenType.ERC20 + tokenBySymbolList[token_by_symbol_key] = TokenBySymbolItem( + key: token_by_symbol_key, + name: token.name, + symbol: token.symbol, + sources: @[s.name], + addressPerChainId: @[AddressPerChain(chainId: token.chainID, address: token.address)], + decimals: token.decimals, + image: "", + tokenType: tokenType, + communityId: token.communityID) + + self.flatTokenList = toSeq(flatTokensList.values) + self.flatTokenList.sort(cmpTokenItem) + self.tokenBySymbolList = toSeq(tokenBySymbolList.values) + self.tokenBySymbolList.sort(cmpTokenBySymbolItem) + + proc getSupportedTokensList(self: Service) = + # this is emited so that the models can know that an update is about to happen + self.events.emit(SIGNAL_TOKEN_LIST_ABOUT_TO_BE_UPDATED, Args()) + let arg = GetTokenListTaskArg( + tptr: cast[ByteAddress](getSupportedTokenList), + vptr: cast[ByteAddress](self.vptr), + slot: "supportedTokensListRetrieved", + ) + self.threadpool.start(arg) + # TODO: Remove after https://github.com/status-im/status-desktop/issues/12513 + proc loadData*(self: Service) = try: let networks = self.networkService.getNetworks() @@ -95,21 +202,18 @@ QtObject: if found: continue let responseTokens = backend.getTokens(network.chainId) - let default_tokens = map( - responseTokens.result.getElems(), - proc(x: JsonNode): TokenDto = x.toTokenDto(network.enabled, hasIcon=true, isCustom=false) - ) + let default_tokens = Json.decode($responseTokens.result, seq[TokenDto], allowUnknownFields = true) self.tokens[network.chainId] = default_tokens.filter( proc(x: TokenDto): bool = x.chainId == network.chainId ) let nativeToken = newTokenDto( + address = "0x0000000000000000000000000000000000000000", name = network.nativeCurrencyName, - chainId = network.chainId, - address = Address.fromHex("0x0000000000000000000000000000000000000000"), symbol = network.nativeCurrencySymbol, decimals = network.nativeCurrencyDecimals, - hasIcon = false + chainId = network.chainId, + communityID = "" ) if not self.tokensToAddressesMap.hasKey(network.nativeCurrencySymbol): @@ -137,8 +241,23 @@ QtObject: error "Tokens init error", errDesription = e.msg proc init*(self: Service) = + if(not main_constants.WALLET_ENABLED): + return self.loadData() + self.getSupportedTokensList() + # ToDo: on self.events.on(SignalType.Message.event) do(e: Args): + # update and polulate internal list and then emit signal + + proc getSourcesOfTokensList*(self: Service): var seq[SupportedSourcesItem] = + return self.sourcesOfTokensList + + proc getFlatTokensList*(self: Service): var seq[TokenItem] = + return self.flatTokenList + + proc getTokenBySymbolList*(self: Service): var seq[TokenBySymbolItem] = + return self.tokenBySymbolList + # TODO: Remove after https://github.com/status-im/status-desktop/issues/12513 proc getTokenList*(self: Service): seq[TokenDto] = return self.tokenList @@ -160,10 +279,11 @@ QtObject: if token.symbol == symbol: return token - proc findTokenByAddress*(self: Service, network: NetworkDto, address: Address): TokenDto = - if not self.tokens.hasKey(network.chainId): + # TODO: Shouldnt be needed after accounts assets are restructured + proc findTokenByAddress*(self: Service, networkChainId: int, address: string): TokenDto = + if not self.tokens.hasKey(networkChainId): return - for token in self.tokens[network.chainId]: + for token in self.tokens[networkChainId]: if token.address == address: return token @@ -179,7 +299,7 @@ QtObject: for _, tokens in self.tokens: for token in tokens: - if token.address == hexAddressValue: + if token.address == $hexAddressValue: return token.symbol return "" @@ -264,7 +384,9 @@ QtObject: ) self.threadpool.start(arg) - # Callback to process the response of fetchHistoricalBalanceForTokenAsJson call + # TODO: The below two APIS are not linked with generic tokens list but with assets per account and should perhaps be moved to + # wallet_account->token_service.nim and clean up rest of the code too. Callback to process the response of + # fetchHistoricalBalanceForTokenAsJson call proc tokenBalanceHistoryDataResolved*(self: Service, response: string) {.slot.} = let responseObj = response.parseJson if (responseObj.kind != JObject): @@ -291,7 +413,7 @@ QtObject: chainIds.add(network.chainId) if chainIds.len == 0: - error "faild to find a network with the symbol", tokenSymbol + error "failed to find a network with the symbol", tokenSymbol return let arg = GetTokenBalanceHistoryDataTaskArg( diff --git a/src/app_service/service/token/service_items.nim b/src/app_service/service/token/service_items.nim new file mode 100644 index 00000000000..9a0c86e5f6b --- /dev/null +++ b/src/app_service/service/token/service_items.nim @@ -0,0 +1,93 @@ +import strformat, Tables + +import app_service/common/types as common_types +#TODO: remove dependency +import ../wallet_account/dto/token_dto + +# This file hold the data types used by models internally + +type SupportedSourcesItem* = ref object of RootObj + name*: string + updatedAt* : int64 + source*: string + version*: string + # Needed to show upfront on ui count of tokens on each list + tokensCount*: int + +proc `$`*(self: SupportedSourcesItem): string = + result = fmt"""SupportedSourcesItem[ + name: {self.name}, + updatedAt: {self.updatedAt}, + source: {self.source}, + version: {self.version}, + tokensCount: {self.tokensCount} + ]""" + +type + TokenItem* = ref object of RootObj + # key is created using chainId and Address + key*: string + name*: string + symbol*: string + # uniswap/status/custom seq[string] + sources*: seq[string] + chainID*: int + address*: string + decimals*: int + # will remain empty until backend provides us this data + image*: string + tokenType*: common_types.NewTokenType + communityId*: string + +proc `$`*(self: TokenItem): string = + result = fmt"""TokenItem[ + key: {self.key}, + name: {self.name}, + symbol: {self.symbol}, + sources: {self.sources}, + chainID: {self.chainID}, + address: {self.address}, + decimals: {self.decimals}, + image: {self.image}, + tokenType: {self.tokenType}, + communityId: {self.communityId} + ]""" + +type AddressPerChain* = ref object of RootObj + chainId*: int + address*: string + +proc `$`*(self: AddressPerChain): string = + result = fmt"""AddressPerChain[ + chainId: {self.chainId}, + address: {self.address} + ]""" + +type + TokenBySymbolItem* = ref object of TokenItem + addressPerChainId*: seq[AddressPerChain] + +proc `$`*(self: TokenBySymbolItem): string = + result = fmt"""TokenBySymbolItem[ + key: {self.key}, + name: {self.name}, + symbol: {self.symbol}, + sources: {self.sources}, + addressPerChainId: {self.addressPerChainId}, + decimals: {self.decimals}, + image: {self.image}, + tokenType: {self.tokenType}, + communityId: {self.communityId} + ]""" + +# In case of community tokens only the description will be available +type TokenDetails* = ref object of RootObj + description*: string + websiteUrl*: string + marketValues*: TokenMarketValuesDto + +proc cmpTokenItem*(x, y: TokenItem): int = + cmp(x.name, y.name) + +proc cmpTokenBySymbolItem*(x, y: TokenBySymbolItem): int = + cmp(x.name, y.name) diff --git a/src/app_service/service/transaction/service.nim b/src/app_service/service/transaction/service.nim index 6f74ee2bef7..a0df2b17a13 100644 --- a/src/app_service/service/transaction/service.nim +++ b/src/app_service/service/transaction/service.nim @@ -341,7 +341,7 @@ QtObject: else: let network = self.networkService.getNetwork(chainID) let token = self.tokenService.findTokenBySymbol(network.chainId, tokenSym) - toAddress = token.address + toAddress = parseAddress(token.address) let transfer = Transfer( to: parseAddress(to_addr), diff --git a/src/app_service/service/wallet_account/service_account.nim b/src/app_service/service/wallet_account/service_account.nim index 8326475ac21..361ba3ed953 100644 --- a/src/app_service/service/wallet_account/service_account.nim +++ b/src/app_service/service/wallet_account/service_account.nim @@ -526,7 +526,6 @@ proc toggleTestNetworksEnabled*(self: Service) = discard self.settingsService.toggleTestNetworksEnabled() let addresses = self.getWalletAddresses() self.buildAllTokens(addresses, store = true) - self.tokenService.loadData() self.checkRecentHistory(addresses) self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, Args()) @@ -535,7 +534,6 @@ proc toggleIsSepoliaEnabled*(self: Service) = self.networkService.resetNetworks() let addresses = self.getWalletAddresses() self.buildAllTokens(addresses, store = true) - self.tokenService.loadData() self.checkRecentHistory(addresses) self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, Args()) diff --git a/src/backend/backend.nim b/src/backend/backend.nim index 034905ad592..eac0bdc6c75 100644 --- a/src/backend/backend.nim +++ b/src/backend/backend.nim @@ -95,6 +95,9 @@ rpc(checkConnected, "wallet"): rpc(getTokens, "wallet"): chainId: int +rpc(getTokenList, "wallet"): + discard + rpc(getTokensBalancesForChainIDs, "wallet"): chainIds: seq[int] accounts: seq[string] diff --git a/storybook/pages/SupportedTokenListsPanelPage.qml b/storybook/pages/SupportedTokenListsPanelPage.qml index 7e6fc3dce05..9a70bc9bc36 100644 --- a/storybook/pages/SupportedTokenListsPanelPage.qml +++ b/storybook/pages/SupportedTokenListsPanelPage.qml @@ -33,8 +33,8 @@ SplitView { expression: { return "https://status.im/" } }, ExpressionRole { - name: "jsArraySources" - expression: model.sources.split(";") + name: "networkChainName" + expression: model.chainName + (model.isTest? " " + qsTr("(Test)") : "") } ] } diff --git a/storybook/pages/TokenListPopupPage.qml b/storybook/pages/TokenListPopupPage.qml index 19770efc020..94b38f819e8 100644 --- a/storybook/pages/TokenListPopupPage.qml +++ b/storybook/pages/TokenListPopupPage.qml @@ -31,8 +31,8 @@ SplitView { expression: { return "https://status.im/" } }, ExpressionRole { - name: "jsArraySources" - expression: model.sources.split(";") + name: "networkChainName" + expression: model.chainName + (model.isTest? " " + qsTr("(Test)") : "") } ] } @@ -89,8 +89,9 @@ SplitView { sourceModel: root.tokensProxyModel // Filter by source - filters: ExpressionFilter { - expression: model.jsArraySources.includes(keyFilter.value) + filters: RegExpFilter { + roleName: "sources" + pattern: ".*" + keyFilter.value + ".*" } } diff --git a/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml b/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml index fbe7046bf63..cc7d843cad8 100644 --- a/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml +++ b/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml @@ -94,8 +94,9 @@ StatusListView { sourceModel: root.tokensListModel // Filter by source - filters: ExpressionFilter { - expression: (model.jsArraySources).includes(keyFilter.value) + filters: RegExpFilter { + roleName: "sources" + pattern: ".*" + keyFilter.value + ".*" } } diff --git a/ui/app/AppLayouts/Profile/popups/TokenListPopup.qml b/ui/app/AppLayouts/Profile/popups/TokenListPopup.qml index 1efe06fe9df..702f6d7810e 100644 --- a/ui/app/AppLayouts/Profile/popups/TokenListPopup.qml +++ b/ui/app/AppLayouts/Profile/popups/TokenListPopup.qml @@ -45,7 +45,6 @@ StatusDialog { bottomMargin: Style.current.padding implicitHeight: contentHeight - model: root.tokensListModel header: ColumnLayout { spacing: 20 width: list.width @@ -60,6 +59,7 @@ StatusDialog { CustomHeaderDelegate {} } delegate: CustomDelegate {} + Component.onCompleted: model = Qt.binding(() => root.tokensListModel) } header: StatusDialogHeader { @@ -118,7 +118,7 @@ StatusDialog { textColor: Theme.palette.baseColor1 textHoverColor: Theme.palette.directColor1 icon.name: "external-link" - onClicked: root.linkClicked(root.sourceUrl) + onClicked: root.linkClicked(link) } component CustomSourceInfoComponent: ColumnLayout { @@ -236,7 +236,7 @@ StatusDialog { StatusBaseText { Layout.fillWidth: true - text: model.chainName + text: model.networkChainName elide: Text.ElideMiddle color: Theme.palette.baseColor1 } diff --git a/ui/app/AppLayouts/Wallet/stores/TokensStore.qml b/ui/app/AppLayouts/Wallet/stores/TokensStore.qml index fc3a966c402..aca2b4a32a8 100644 --- a/ui/app/AppLayouts/Wallet/stores/TokensStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/TokensStore.qml @@ -9,13 +9,13 @@ QtObject { id: root /* PRIVATE: Modules used to get data from backend */ - readonly property var _walletModule: walletSectionNewModule + readonly property var _allTokensModule: walletSectionAllTokens readonly property var _networksModule: networksModule /* This contains the different sources for the tokens list ex. uniswap list, status tokens list */ readonly property var sourcesOfTokensModel: SortFilterProxyModel { - sourceModel: root._walletModule.sourcesOfTokensModel + sourceModel: root._allTokensModule.sourcesOfTokensModel proxyRoles: ExpressionRole { function sourceImage(sourceKey) { return Constants.getSupportedTokenSourceImage(sourceKey) @@ -23,16 +23,25 @@ QtObject { name: "image" expression: sourceImage(model.key) } + filters: ExpressionFilter { + function filterByKey(sourceKey) { + return (sourceKey === Constants.supportedTokenSources.uniswap) || + (sourceKey === Constants.supportedTokenSources.status) + } + expression: { + return filterByKey(model.key) + } + } } /* This list contains the complete list of tokens with separate entry per token which has a unique [address + network] pair */ - readonly property var flatTokensModel: root._walletModule.flatTokensModel + readonly property var flatTokensModel: root._allTokensModule.flatTokensModel /* PRIVATE: This model just combines tokens and network information in one */ readonly property LeftJoinModel _joinFlatTokensModel : LeftJoinModel { leftModel: root.flatTokensModel - rightModel: root._networksModule.all + rightModel: root._networksModule.flatNetworks joinRole: "chainId" } @@ -46,7 +55,8 @@ QtObject { proxyRoles: [ ExpressionRole { name: "explorerUrl" - expression: { return model.blockExplorerURL + "/" + model.address } // TO REVIEW the correct composition!! + expression: { + return model.blockExplorerURL + "/token/" + model.address } }, ExpressionRole { function tokenIcon(symbol) { @@ -56,8 +66,8 @@ QtObject { expression: tokenIcon(model.symbol) }, ExpressionRole { - name: "jsArraySources" - expression: model.sources.split(";") + name: "networkChainName" + expression: model.chainName + (model.isTest? " " + qsTr("(Test)") : "") } ] } @@ -68,7 +78,7 @@ QtObject { there will be one entry per address + network pair */ // TODO in #12513 readonly property var tokensBySymbolModel: SortFilterProxyModel { - sourceModel: root._walletModule.tokensBySymbolModel + sourceModel: root._allTokensModule.tokensBySymbolModel proxyRoles: [ ExpressionRole { function tokenIcon(symbol) { diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 21b8ae224e0..415131d8b93 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -788,8 +788,8 @@ QtObject { } readonly property QtObject supportedTokenSources: QtObject { - readonly property string uniswap: "uniswap" - readonly property string status: "status" + readonly property string uniswap: "Uniswap Labs Default Token List" + readonly property string status: "Status Token List" readonly property string custom: "custom" } diff --git a/vendor/status-go b/vendor/status-go index 9f69c325939..a3ac3f6e7df 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 9f69c3259397821483bad722ab92eb52470185de +Subproject commit a3ac3f6e7df39cdf9e3cd553ebf725528c402eb4