diff --git a/src/app/modules/main/chat_section/chat_content/input_area/view.nim b/src/app/modules/main/chat_section/chat_content/input_area/view.nim index ec36e4dd869..62ed29fbc43 100644 --- a/src/app/modules/main/chat_section/chat_content/input_area/view.nim +++ b/src/app/modules/main/chat_section/chat_content/input_area/view.nim @@ -127,8 +127,8 @@ QtObject: proc removeLinkPreviewData*(self: View, index: int) {.slot.} = self.linkPreviewModel.removePreviewData(index) - proc addPaymentRequest*(self: View, receiver: string, amount: string, tokenKey: string, symbol: string) {.slot.} = - self.paymentRequestModel.addPaymentRequest(receiver, amount, tokenKey, symbol) + proc addPaymentRequest*(self: View, receiver: string, amount: string, tokenKey: string, symbol: string, logoUri: string) {.slot.} = + self.paymentRequestModel.addPaymentRequest(receiver, amount, tokenKey, symbol, logoUri) proc removePaymentRequestPreviewData*(self: View, index: int) {.slot.} = self.paymentRequestModel.removeItemWithIndex(index) 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 52b41112fdf..d21d1ea6328 100644 --- a/src/app/modules/main/wallet_section/all_tokens/controller.nim +++ b/src/app/modules/main/wallet_section/all_tokens/controller.nim @@ -73,6 +73,9 @@ proc getAllTokenLists*(self: Controller): var seq[TokenListItem] = proc buildGroupsForChain*(self: Controller, chainId: int): bool = return self.tokenService.buildGroupsForChain(chainId) +proc getTokenByKeyOrGroupKeyFromAllTokens*(self: Controller, key: string): TokenItem = + return self.tokenService.getTokenByKeyOrGroupKeyFromAllTokens(key) + proc getGroupsForChain*(self: Controller): var seq[TokenGroupItem] = return self.tokenService.getGroupsForChain() @@ -145,4 +148,7 @@ proc toggleAutoRefreshTokensLists*(self: Controller): bool = return self.settingsService.toggleAutoRefreshTokens() proc tokenAvailableForBridgingViaHop*(self: Controller, tokenChainId: int, tokenAddress: string): bool = - return self.tokenService.tokenAvailableForBridgingViaHop(tokenChainId, tokenAddress) \ No newline at end of file + return self.tokenService.tokenAvailableForBridgingViaHop(tokenChainId, tokenAddress) + +proc getMandatoryTokenGroupKeys*(self: Controller): seq[string] = + return self.tokenService.getMandatoryTokenGroupKeys() \ No newline at end of file diff --git a/src/app/modules/main/wallet_section/all_tokens/io_interface.nim b/src/app/modules/main/wallet_section/all_tokens/io_interface.nim index 21b88bc1420..45a88328cdc 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 @@ -81,6 +81,9 @@ method viewDidLoad*(self: AccessInterface) {.base.} = method buildGroupsForChain*(self: AccessInterface, chainId: int): bool {.base.} = raise newException(ValueError, "No implementation available") +method getTokenByKeyOrGroupKeyFromAllTokens*(self: AccessInterface, key: string): TokenItem {.base.} = + raise newException(ValueError, "No implementation available") + method filterChanged*(self: AccessInterface, addresses: seq[string]) {.base.} = raise newException(ValueError, "No implementation available") @@ -127,4 +130,7 @@ method showCommunityAssetWhenSendingTokensChanged*(self: AccessInterface) {.base raise newException(ValueError, "No implementation available") method tokenAvailableForBridgingViaHop*(self: AccessInterface, tokenChainId: int, tokenAddress: string): bool {.base.} = + raise newException(ValueError, "No implementation available") + +method getMandatoryTokenGroupKeys*(self: AccessInterface): seq[string] {.base.} = raise newException(ValueError, "No implementation available") \ No newline at end of file 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 33194b073c0..aee14447687 100644 --- a/src/app/modules/main/wallet_section/all_tokens/module.nim +++ b/src/app/modules/main/wallet_section/all_tokens/module.nim @@ -126,6 +126,9 @@ method getTokenMarketValuesDataSource*(self: Module): TokenMarketValuesDataSourc method buildGroupsForChain*(self: Module, chainId: int): bool = return self.controller.buildGroupsForChain(chainId) +method getTokenByKeyOrGroupKeyFromAllTokens*(self: Module, key: string): TokenItem = + return self.controller.getTokenByKeyOrGroupKeyFromAllTokens(key) + method filterChanged*(self: Module, addresses: seq[string]) = if addresses == self.addresses: return @@ -183,4 +186,7 @@ method showCommunityAssetWhenSendingTokensChanged*(self: Module) = self.view.showCommunityAssetWhenSendingTokensChanged() method tokenAvailableForBridgingViaHop*(self: Module, tokenChainId: int, tokenAddress: string): bool = - return self.controller.tokenAvailableForBridgingViaHop(tokenChainId, tokenAddress) \ No newline at end of file + return self.controller.tokenAvailableForBridgingViaHop(tokenChainId, tokenAddress) + +method getMandatoryTokenGroupKeys*(self: Module): seq[string] = + return self.controller.getMandatoryTokenGroupKeys() \ No newline at end of file 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 93b35b625b6..ef934544e58 100644 --- a/src/app/modules/main/wallet_section/all_tokens/view.nim +++ b/src/app/modules/main/wallet_section/all_tokens/view.nim @@ -1,4 +1,4 @@ -import nimqml, sequtils, strutils, chronicles +import nimqml, sequtils, strutils, json, chronicles import io_interface, token_lists_model, token_groups_model @@ -107,13 +107,24 @@ QtObject: read = getTokenGroupsModel notify = tokenGroupsModelChanged - proc buildGroupsForChain*(self: View, chainId: int, mandatoryKeysString: string) {.slot.} = - let mandatoryKeys = mandatoryKeysString.split("$$") + proc buildGroupsForChain*(self: View, chainId: int, mandatoryGroupKeysString: string) {.slot.} = if not self.delegate.buildGroupsForChain(chainId): return - self.tokenGroupsForChainModel.modelsUpdated(resetModelSize = true, mandatoryKeys) + var mandatoryGroupKeys: seq[string] = @[] + if mandatoryGroupKeysString.len > 0: + mandatoryGroupKeys = mandatoryGroupKeysString.split("$$") + else: + mandatoryGroupKeys = self.delegate.getMandatoryTokenGroupKeys() + + self.tokenGroupsForChainModel.modelsUpdated(resetModelSize = true, mandatoryGroupKeys) self.tokenGroupsForChainModelChanged() + proc getTokenByKeyOrGroupKeyFromAllTokens*(self: View, key: string): string {.slot.} = + let token = self.delegate.getTokenByKeyOrGroupKeyFromAllTokens(key) + if token.isNil: + return "" + return $(%* token) + proc modelsUpdated*(self: View) = self.tokenListsModel.modelsUpdated() self.tokenGroupsModel.modelsUpdated() diff --git a/src/app/modules/shared_models/payment_request_model.nim b/src/app/modules/shared_models/payment_request_model.nim index a83741b25fa..453449cd8b7 100644 --- a/src/app/modules/shared_models/payment_request_model.nim +++ b/src/app/modules/shared_models/payment_request_model.nim @@ -7,6 +7,7 @@ type Symbol Amount ReceiverAddress + LogoUri QtObject: type @@ -42,6 +43,7 @@ QtObject: ModelRole.Symbol.int: "symbol", ModelRole.Amount.int: "amount", ModelRole.ReceiverAddress.int: "receiver", + ModelRole.LogoUri.int:"logoUri", }.toTable method data(self: Model, index: QModelIndex, role: int): QVariant = @@ -63,6 +65,8 @@ QtObject: result = newQVariant(item.amount) of ModelRole.ReceiverAddress: result = newQVariant(item.receiver) + of ModelRole.LogoUri: + result = newQVariant(item.logoUri) else: result = newQVariant() @@ -85,8 +89,8 @@ QtObject: self.items.add(paymentRequest) self.endInsertRows() - proc addPaymentRequest*(self: Model, receiver: string, amount: string, tokenKey: string, symbol: string) {.slot.}= - let paymentRequest = newPaymentRequest(receiver, amount, tokenKey, symbol) + proc addPaymentRequest*(self: Model, receiver: string, amount: string, tokenKey: string, symbol: string, logoUri: string) {.slot.}= + let paymentRequest = newPaymentRequest(receiver, amount, tokenKey, symbol, logoUri) self.insertItem(paymentRequest) proc clearItems*(self: Model) = diff --git a/src/app_service/service/activity_center/service.nim b/src/app_service/service/activity_center/service.nim index a0aa936a28c..4e6ede10b50 100644 --- a/src/app_service/service/activity_center/service.nim +++ b/src/app_service/service/activity_center/service.nim @@ -334,7 +334,7 @@ QtObject: if response.result.kind != JNull: if response.result.contains("chats"): for jsonChat in response.result["chats"]: - let chat = toChatDto(jsonChat) + var chat = toChatDto(jsonChat) self.chatService.updateOrAddChat(chat) self.events.emit(SIGNAL_CHAT_UPDATE, ChatUpdateArgs(chats: @[chat])) diff --git a/src/app_service/service/message/dto/payment_request.nim b/src/app_service/service/message/dto/payment_request.nim index d4661453b89..af544ff162f 100644 --- a/src/app_service/service/message/dto/payment_request.nim +++ b/src/app_service/service/message/dto/payment_request.nim @@ -7,9 +7,12 @@ type PaymentRequest* = object amount*: string tokenKey*: string symbol*: string + logoUri*: string + chainId*: int # kept for backward compatibility with the old payment requests -proc newPaymentRequest*(receiver: string, amount: string, tokenKey: string, symbol: string): PaymentRequest = - result = PaymentRequest(receiver: receiver, amount: amount, tokenKey: tokenKey, symbol: symbol) + +proc newPaymentRequest*(receiver: string, amount: string, tokenKey: string, symbol: string, logoUri: string): PaymentRequest = + result = PaymentRequest(receiver: receiver, amount: amount, tokenKey: tokenKey, symbol: symbol, logoUri: logoUri) proc toPaymentRequest*(jsonObj: JsonNode): PaymentRequest = result = PaymentRequest() @@ -17,13 +20,16 @@ proc toPaymentRequest*(jsonObj: JsonNode): PaymentRequest = discard jsonObj.getProp("amount", result.amount) discard jsonObj.getProp("tokenKey", result.tokenKey) discard jsonObj.getProp("symbol", result.symbol) + discard jsonObj.getProp("logoUri", result.logoUri) + discard jsonObj.getProp("chainId", result.chainId) proc `%`*(self: PaymentRequest): JsonNode = return %*{ "receiver": self.receiver, "amount": self.amount, "tokenKey": self.tokenKey, - "symbol": self.symbol + "symbol": self.symbol, + "logoUri": self.logoUri } proc `$`*(self: PaymentRequest): string = @@ -31,5 +37,7 @@ proc `$`*(self: PaymentRequest): string = receiver: {self.receiver}, amount: {self.amount}, tokenKey: {self.tokenKey}, - symbol: {self.symbol} + symbol: {self.symbol}, + logoUri: {self.logoUri}, + chainId: {self.chainId} )""" \ No newline at end of file diff --git a/src/app_service/service/message/service.nim b/src/app_service/service/message/service.nim index acbcec3acc9..d8971dfc5f7 100644 --- a/src/app_service/service/message/service.nim +++ b/src/app_service/service/message/service.nim @@ -21,11 +21,10 @@ import ./dto/urls_unfurling_plan import ./dto/link_preview import ./message_cursor -import ../../common/activity_center -import ../../common/message as message_common -import ../../common/conversion as service_conversion - -from ../../common/account_constants import ZERO_ADDRESS +import app_service/common/activity_center +import app_service/common/message as message_common +import app_service/common/conversion as service_conversion +from app_service/common/account_constants import ZERO_ADDRESS import web3/conversions @@ -225,6 +224,27 @@ QtObject: return self.pinnedMsgCursor[chatId] + proc checkPaymentRequestsInMessage*(self: Service, message: var MessageDto) = + for paymentRequest in message.paymentRequests.mitems: + if paymentRequest.tokenKey.len == 0 or paymentRequest.logoUri.len == 0: + if paymentRequest.symbol.len > 0: + # due to backward compatibility, in case the tokenKey is empty, we should try to find it by symbol on the received chain. + let token = self.tokenService.getTokenBySymbolOnChain(paymentRequest.symbol, paymentRequest.chainId) + if token.isNil: + error "token is nil", tokenKey=paymentRequest.tokenKey, procName="checkPaymentRequestsInMessage" + continue + paymentRequest.tokenKey = token.key + paymentRequest.symbol = token.symbol + paymentRequest.logoUri = token.logoUri + + proc checkPaymentRequestsInMessages*(self: Service, messages: var seq[MessageDto]) = + for message in messages.mitems: + self.checkPaymentRequestsInMessage(message) + + proc checkPaymentRequestsInPinnedMessages*(self: Service, pinnedMessages: var seq[PinnedMessageDto]) = + for pinnedMessage in pinnedMessages.mitems: + self.checkPaymentRequestsInMessage(pinnedMessage.message) + proc asyncLoadMoreMessagesForChat*(self: Service, chatId: string, limit = MESSAGES_PER_PAGE): bool = if (chatId.len == 0): error "empty chat id", procName="asyncLoadMoreMessagesForChat" @@ -357,6 +377,7 @@ QtObject: return self.bulkReplacePubKeysWithDisplayNames(messages) + self.checkPaymentRequestsInMessages(messages) for i in 0 ..< chats.len: let chatId = chats[i].id @@ -405,7 +426,7 @@ QtObject: return self.numOfPinnedMessagesPerChat[chatId] return 0 - proc handlePinnedMessagesUpdate(self: Service, pinnedMessages: seq[PinnedMessageUpdateDto]) = + proc handlePinnedMessagesUpdate(self: Service, pinnedMessages: var seq[PinnedMessageUpdateDto]) = for pm in pinnedMessages: var chatId: string = "" if (self.numOfPinnedMessagesPerChat.contains(pm.localChatId)): @@ -480,6 +501,7 @@ QtObject: messages = map(args.messages.getElems(), proc(x: JsonNode): MessageDto = x.toMessageDto()) self.bulkReplacePubKeysWithDisplayNames(messages) + self.checkPaymentRequestsInMessages(messages) self.events.emit(SIGNAL_MESSAGES_LOADED, MessagesLoadedArgs( chatId: args.chatId, @@ -581,6 +603,8 @@ QtObject: result = x.toReactionDto() ) + self.checkPaymentRequestsInPinnedMessages(pinnedMessages) + let data = PinnedMessagesLoadedArgs(chatId: chatId, pinnedMessages: pinnedMessages, reactions: reactions) self.events.emit(SIGNAL_PINNED_MESSAGES_LOADED, data) @@ -619,6 +643,7 @@ QtObject: messages = map(messagesArr.getElems(), proc(x: JsonNode): MessageDto = x.toMessageDto()) self.bulkReplacePubKeysWithDisplayNames(messages) + self.checkPaymentRequestsInMessages(messages) # handling reactions var reactionsArr: JsonNode @@ -660,6 +685,7 @@ QtObject: var messages = map(rpcResponseObj{"messages"}.getElems(), proc(x: JsonNode): MessageDto = x.toMessageDto()) if messages.len > 0: self.bulkReplacePubKeysWithDisplayNames(messages) + self.checkPaymentRequestsInMessages(messages) let data = CommunityMemberMessagesArgs(communityId: communityId, messages: messages) self.events.emit(SIGNAL_COMMUNITY_MEMBER_ALL_MESSAGES, data) diff --git a/src/app_service/service/token/items/token.nim b/src/app_service/service/token/items/token.nim index d842e23fa7f..96b0fd9c661 100644 --- a/src/app_service/service/token/items/token.nim +++ b/src/app_service/service/token/items/token.nim @@ -1,4 +1,4 @@ -import strutils, tables +import strutils, tables, json import app_service/common/wallet_constants as common_wallet_constants import app_service/common/utils as common_utils @@ -113,4 +113,20 @@ proc createStatusTokenItem*(chainId: int): TokenItem = tokenDto.symbol = common_wallet_constants.STATUS_SYMBOL_TESTNET tokenDto.decimals = common_wallet_constants.STATUS_DECIMALS_TESTNET - return createTokenItem(tokenDto, common_types.TokenType.ERC20) \ No newline at end of file + return createTokenItem(tokenDto, common_types.TokenType.ERC20) + +proc `%`*(self: TokenItem): JsonNode = + return %*{ + "key": self.key, + "groupKey": self.groupKey, + "crossChainId": self.crossChainId, + "address": self.address, + "name": self.name, + "symbol": self.symbol, + "decimals": self.decimals, + "chainId": self.chainId, + "logoUri": self.logoUri, + "customToken": self.customToken, + "communityId": self.communityData.id, + "type": self.`type` + } \ No newline at end of file diff --git a/src/app_service/service/token/service.nim b/src/app_service/service/token/service.nim index db58f160ca4..8311a66904e 100644 --- a/src/app_service/service/token/service.nim +++ b/src/app_service/service/token/service.nim @@ -1,4 +1,4 @@ -import nimqml, tables, json, sequtils, chronicles, strutils, sugar, algorithm +import nimqml, tables, json, std/sequtils, chronicles, strutils, sugar, algorithm import web3/eth_api_types import backend/backend as backend diff --git a/src/app_service/service/token/service_main.nim b/src/app_service/service/token/service_main.nim index 1f47f9c3a4f..6ba775c4fd1 100644 --- a/src/app_service/service/token/service_main.nim +++ b/src/app_service/service/token/service_main.nim @@ -72,6 +72,14 @@ proc init*(self: Service) = self.refreshTokens() +proc getMandatoryTokenGroupKeys*(self: Service): seq[string] = + let tokenKeys = getMandatoryTokenKeys() + let tokens = getTokensByKeys(tokenKeys) + var groupKeysMap: Table[string, bool] = initTable[string, bool]() + for token in tokens: + groupKeysMap[token.groupKey] = true + return toSeq(groupKeysMap.keys) + proc getCurrency*(self: Service): string = return self.settingsService.getCurrency() @@ -105,6 +113,20 @@ proc getGroupsForChain*(self: Service): var seq[TokenGroupItem] = proc getAllTokenLists*(self: Service): var seq[TokenListItem] = return self.allTokenLists +################################################################################ +## This is a very special function that should not be used anywhere else, +## it covers the backward compatibility with the old payment requests. +## +## Itterates over all tokens for the given chain and returns the first token +## that matches the symbol or name (cause some tokens have different symbols for EVM/BSC chains), case insensitive. +proc getTokenBySymbolOnChain*(self: Service, symbol: string, chainId: int): TokenItem = + let tokens = getTokensByChain(chainId) + for token in tokens: + if cmpIgnoreCase(token.symbol, symbol) == 0 or cmpIgnoreCase(token.name, symbol) == 0: + return token + return nil +################################################################################ + proc getAllCommunityTokens*(self: Service): var seq[TokenItem] = const communityTokenListId = "community" for tl in self.allTokenLists: @@ -148,6 +170,20 @@ proc getTokensByGroupKey*(self: Service, groupKey: string): seq[TokenItem] = return @[token] return self.groupsOfInterestByKey[groupKey].tokens +## Note: use this function in a very rare case, when you're sure the token is not present in the models. +## Returns a token that matches the key, or the first token in the group that matches the key. +proc getTokenByKeyOrGroupKeyFromAllTokens*(self: Service, key: string): TokenItem = + if common_utils.isTokenKey(key): + return self.getTokenByKey(key) + var tokens = self.getTokensByGroupKey(key) + if tokens.len > 0: + return tokens[0] + tokens = getAllTokens() + let matchedTokens = tokens.filter(t => t.groupKey == key) + if matchedTokens.len > 0: + return matchedTokens[0] + return nil + proc getTokenByGroupKeyAndChainId*(self: Service, groupKey: string, chainId: int): TokenItem = let tokens = self.getTokensByGroupKey(groupKey) if tokens.len > 0: diff --git a/src/app_service/service/token/service_tokens.nim b/src/app_service/service/token/service_tokens.nim index 5f94c10ee6d..e6c68fa107b 100644 --- a/src/app_service/service/token/service_tokens.nim +++ b/src/app_service/service/token/service_tokens.nim @@ -1,3 +1,21 @@ +proc getMandatoryTokenKeys(): seq[string] = + try: + var response: JsonNode + var err = status_go_tokens.getMandatoryTokenKeys(response) + if err.len > 0: + raise newException(CatchableError, "failed" & err) + if response.isNil or response.kind != JsonNodeKind.JArray: + raise newException(CatchableError, "unexpected response") + + # Create a copy of the tokenResultStr to avoid exceptions in `decode` + # Workaround for https://github.com/status-im/status-desktop/issues/17398 + let responseStr = $response + let parsedResponse = Json.decode(responseStr, seq[string], allowUnknownFields = true) + result = parsedResponse + except Exception as e: + let errDesription = e.msg + error "error: ", errDesription + proc tokenAvailableForBridgingViaHop(tokenChainId: int, tokenAddress: string): bool = try: var response: JsonNode @@ -29,6 +47,23 @@ proc getAllTokenLists(): seq[TokenListItem] = let errDesription = e.msg error "error: ", errDesription +proc getAllTokens(): seq[TokenItem] = + try: + var response: JsonNode + var err = status_go_tokens.getAllTokens(response) + if err.len > 0: + raise newException(CatchableError, "failed" & err) + if response.isNil or response.kind != JsonNodeKind.JArray: + raise newException(CatchableError, "unexpected response") + + # Create a copy of the tokenResultStr to avoid exceptions in `decode` + # Workaround for https://github.com/status-im/status-desktop/issues/17398 + let responseStr = $response + let parsedResponse = Json.decode(responseStr, seq[TokenDto], allowUnknownFields = true) + result = parsedResponse.map(t => createTokenItem(t)) + except Exception as e: + let errDesription = e.msg + error "error: ", errDesription proc getTokensOfInterestForActiveNetworksMode(): seq[TokenItem] = try: @@ -77,7 +112,10 @@ proc getTokenByChainAddress(chainId: int, address: string): TokenItem = if response.isNil or response.kind != JsonNodeKind.JObject: raise newException(CatchableError, "unexpected response") - let parsedResponse = Json.decode($response, TokenDto, allowUnknownFields = true) + # Create a copy of the tokenResultStr to avoid exceptions in `decode` + # Workaround for https://github.com/status-im/status-desktop/issues/17398 + let responseStr = $response + let parsedResponse = Json.decode(responseStr, TokenDto, allowUnknownFields = true) result = createTokenItem(parsedResponse) except Exception as e: let errDesription = e.msg diff --git a/src/backend/tokens.nim b/src/backend/tokens.nim index 68316363444..07909595f56 100644 --- a/src/backend/tokens.nim +++ b/src/backend/tokens.nim @@ -6,9 +6,15 @@ include common export response_type +rpc(getMandatoryTokenKeys, "wallet"): + discard + rpc(getAllTokenLists, "wallet"): discard +rpc(getAllTokens, "wallet"): + discard + rpc(getTokensOfInterestForActiveNetworksMode, "wallet"): discard @@ -30,6 +36,18 @@ rpc(tokenAvailableForBridgingViaHop, "wallet"): tokenAddress: string +## Gets all mandatory token keys +## `resultOut` represents a json object that contains the mandatory token keys if the call was successful, or `nil` +## returns the error message if any, or an empty string +proc getMandatoryTokenKeys*(resultOut: var JsonNode): string = + try: + let response = getMandatoryTokenKeys() + return prepareResponse(resultOut, response) + except Exception as e: + warn "error getting all mandatory token keys", err = e.msg + return e.msg + + ## Checks if the token is available for bridging via Hop ## `resultOut` represents a json object that contains the bool if the call was successful, or `nil` ## `tokenChainId` is the chain id of the network @@ -68,6 +86,17 @@ proc getTokensOfInterestForActiveNetworksMode*(resultOut: var JsonNode): string return e.msg +## Gets all tokens +## `resultOut` represents a json object that contains the tokens if the call was successful, or `nil` +## returns the error message if any, or an empty string +proc getAllTokens*(resultOut: var JsonNode): string = + try: + let response = getAllTokens() + return prepareResponse(resultOut, response) + except Exception as e: + warn "error getting all tokens", err = e.msg + return e.msg + ## Gets all tokens for the active networks mode ## `resultOut` represents a json object that contains the tokens if the call was successful, or `nil` ## returns the error message if any, or an empty string diff --git a/storybook/pages/PaymentRequestAdaptorPage.qml b/storybook/pages/PaymentRequestAdaptorPage.qml index 45547379936..255a26e5ace 100644 --- a/storybook/pages/PaymentRequestAdaptorPage.qml +++ b/storybook/pages/PaymentRequestAdaptorPage.qml @@ -8,12 +8,13 @@ import AppLayouts.Wallet.adaptors import StatusQ.Core.Utils -import Storybook -import Models - import shared.stores import utils +import Storybook +import Models +import Mocks + Item { id: root @@ -22,15 +23,34 @@ Item { readonly property int selectedNetworkChainId: ctrlSelectedNetworkChainId.currentValue - readonly property var assetsModel: TokenGroupsModel {} readonly property var flatNetworks: NetworksModel.flatNetworks + + readonly property var tokensStore: TokensStoreMock { + tokenGroupsModel: TokenGroupsModel {} + tokenGroupsForChainModel: TokenGroupsModel { + skipInitialLoad: true + } + searchResultModel: TokenGroupsModel { + skipInitialLoad: true + tokenGroupsForChainModel: d.tokensStore.tokenGroupsForChainModel + } + } + + onSelectedNetworkChainIdChanged: { + d.tokensStore.buildGroupsForChain(d.selectedNetworkChainId) + } + } + + Component.onCompleted: { + Qt.callLater(() => d.tokensStore.buildGroupsForChain(d.selectedNetworkChainId)) } PaymentRequestAdaptor { id: adaptor selectedNetworkChainId: d.selectedNetworkChainId - tokenGroupsModel: d.assetsModel flatNetworksModel: d.flatNetworks + tokenGroupsForChainModel: d.tokensStore.tokenGroupsForChainModel + searchResultModel: d.tokensStore.searchResultModel } ColumnLayout { @@ -45,8 +65,8 @@ Item { text: "Chain:" } ComboBox { - Layout.fillWidth: true id: ctrlSelectedNetworkChainId + Layout.fillWidth: true model: d.flatNetworks textRole: "chainName" valueRole: "chainId" @@ -65,34 +85,49 @@ Item { ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true + Label { Layout.fillWidth: true horizontalAlignment: Qt.AlignHCenter font.bold: true text: "Input" } + GenericListView { - label: "Tokens model" + label: "Token groups model, total: " + d.tokensStore.tokenGroupsModel.count - model: d.assetsModel + model: d.tokensStore.tokenGroupsModel Layout.fillWidth: true Layout.fillHeight: true - roles: ["key", "name", "symbol"] + roles: ["index", "key", "name", "symbol", "decimals", "logoUri"] skipEmptyRoles: true insetComponent: Label { text: { if (!model) return "" - let chains = "Chains: \n" + JSON.stringify(ModelUtils.modelToFlatArray(model["addressPerChain"], "chainId")) + let chains = "Chains: \n" + JSON.stringify(ModelUtils.modelToFlatArray(model["tokens"], "chainId")) chains = chains.replace(/,/g, '\n') return chains } } } + + GenericListView { + label: "Token groups for chain model, total: " + d.tokensStore.tokenGroupsForChainModel.count + + model: d.tokensStore.tokenGroupsForChainModel + + Layout.fillWidth: true + Layout.fillHeight: true + + roles: ["index", "key", "name", "symbol", "decimals", "logoUri"] + skipEmptyRoles: true + } + GenericListView { - label: "Networks model" + label: "Networks model, total: " + d.flatNetworks.count model: d.flatNetworks @@ -121,7 +156,7 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - roles: ["tokensKey", "name", "symbol", "iconSource", "currencyBalanceAsString", "sectionName"] + roles: ["index", "key", "name", "symbol", "logoUri", "sectionName"] skipEmptyRoles: true } diff --git a/storybook/pages/PaymentRequestModalPage.qml b/storybook/pages/PaymentRequestModalPage.qml index 773dac23ef1..f14e933da16 100644 --- a/storybook/pages/PaymentRequestModalPage.qml +++ b/storybook/pages/PaymentRequestModalPage.qml @@ -9,13 +9,16 @@ import StatusQ.Core import StatusQ.Core.Utils import utils -import Storybook -import Models import AppLayouts.Wallet.adaptors +import AppLayouts.Wallet.stores import AppLayouts.Chat.popups import shared.stores as SharedStores +import Storybook +import Models +import Mocks + SplitView { id: root @@ -55,6 +58,17 @@ SplitView { Component.onCompleted: Qt.callLater(d.launchPopup) + readonly property var tokensStore: TokensStoreMock { + tokenGroupsModel: TokenGroupsModel {} + tokenGroupsForChainModel: TokenGroupsModel { + skipInitialLoad: true + } + searchResultModel: TokenGroupsModel { + skipInitialLoad: true + tokenGroupsForChainModel: popupBg.tokensStore.tokenGroupsForChainModel + } + } + Component { id: paymentRequestModalComponent PaymentRequestModal { @@ -63,18 +77,14 @@ SplitView { closePolicy: Popup.CloseOnEscape destroyOnClose: true + readonly property SharedStores.CurrenciesStore currenciesStore: SharedStores.CurrenciesStore {} + currentCurrency: currenciesStore.currentCurrency formatCurrencyAmount: currenciesStore.formatCurrencyAmount flatNetworksModel: d.flatNetworks accountsModel: d.accounts - assetsModel: paymentRequestAdaptor.outputModel - - readonly property SharedStores.CurrenciesStore currenciesStore: SharedStores.CurrenciesStore {} - readonly property var paymentRequestAdaptor: PaymentRequestAdaptor { - flatNetworksModel: d.flatNetworks - tokenGroupsModel: TokenGroupsModel {} - selectedNetworkChainId: paymentRequestModal.selectedNetworkChainId - } + tokenGroupsForChainModel: popupBg.tokensStore.tokenGroupsForChainModel + searchResultModel: popupBg.tokensStore.searchResultModel Connections { target: d @@ -85,11 +95,17 @@ SplitView { paymentRequestModal.selectedAccountAddress = d.selectedAccountAddress } } + Component.onCompleted: { if (d.selectedNetworkChainId > -1) paymentRequestModal.selectedNetworkChainId = d.selectedNetworkChainId if (!!d.selectedAccountAddress) paymentRequestModal.selectedAccountAddress = d.selectedAccountAddress + popupBg.tokensStore.buildGroupsForChain(paymentRequestModal.selectedNetworkChainId) + } + + onBuildGroupsForChain: { + popupBg.tokensStore.buildGroupsForChain(paymentRequestModal.selectedNetworkChainId) } } } @@ -131,8 +147,8 @@ SplitView { Layout.fillWidth: true Label { text: "Account:" } ComboBox { - Layout.fillWidth: true id: ctrlAccount + Layout.fillWidth: true textRole: "name" valueRole: "address" displayText: currentText || "----" diff --git a/storybook/qmlTests/tests/tst_AssetSelector.qml b/storybook/qmlTests/tests/tst_AssetSelector.qml index b8c2d3032bf..b94a7629ce1 100644 --- a/storybook/qmlTests/tests/tst_AssetSelector.qml +++ b/storybook/qmlTests/tests/tst_AssetSelector.qml @@ -27,7 +27,7 @@ Item { name: "Status Test Token", currencyBalanceAsString: "42,23 USD", symbol: "STT", - iconSource: Constants.tokenIcon("STT"), + logoUri: Constants.tokenIcon("STT"), balances: [ { @@ -44,7 +44,7 @@ Item { name: "Ether", currencyBalanceAsString: "4 276,86 USD", symbol: "ETH", - iconSource: Constants.tokenIcon("ETH"), + logoUri: Constants.tokenIcon("ETH"), balances: [ { @@ -61,7 +61,7 @@ Item { name: "Dai Stablecoin", currencyBalanceAsString: "45,92 USD", symbol: "DAI", - iconSource: Constants.tokenIcon("DAI"), + logoUri: Constants.tokenIcon("DAI"), balances: [], sectionName: "Popular assets" diff --git a/storybook/qmlTests/tests/tst_PaymentRequestAdaptor.qml b/storybook/qmlTests/tests/tst_PaymentRequestAdaptor.qml index 19fd1df158d..f02b1735f01 100644 --- a/storybook/qmlTests/tests/tst_PaymentRequestAdaptor.qml +++ b/storybook/qmlTests/tests/tst_PaymentRequestAdaptor.qml @@ -108,9 +108,9 @@ Item { }, ]) } - tokenGroupsModel: ListModel { + tokenGroupsForChainModel: ListModel { Component.onCompleted: append([{ - key: "ETH", + key: Constants.ethGroupKey, name: "Ether", symbol: "ETH", decimals: 18, @@ -119,23 +119,24 @@ Item { websiteUrl: "https://www.ethereum.org/", tokens: [ { chainId: 1, address: "0x0000000000000000000000000000000000000000"}, - { chainId: 5, address: "0x0000000000000000000000000000000000000000"}, + { chainId: 10, address: "0x0000000000000000000000000000000000000000"}, ] }, { - key: "STT", - name: "Status Test Token", - symbol: "STT", + key: Constants.sntGroupKey, + name: "Status", + symbol: "SNT", decimals: 18, communityId: "", description: "Status Network Token (SNT) is a utility token used within the Status.im platform, which is an open-source messaging and social media platform built on the Ethereum blockchain. SNT is designed to facilitate peer-to-peer communication and interactions within the decentralized Status network.", websiteUrl: "https://status.im/", tokens: [ - {chainId: 5, address: "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"}, + {chainId: 1, address: "0x744d70fdbe2ba4cf95131626614a1763df805b9e"}, + {chainId: 10, address: "0x650af3c15af43dcb218406d30784416d64cfb6b2"} ] }, { - key: "DAI", + key: Constants.daiGroupKey, name: "Dai Stablecoin", symbol: "DAI", decimals: 18, @@ -144,11 +145,11 @@ Item { websiteUrl: "https://makerdao.com/", tokens: [ { chainId: 1, address: "0x6b175474e89094c44da98b954eedeac495271d0f"}, - { chainId: 5, address: "0xf2edf1c091f683e3fb452497d9a98a49cba84666"}, + { chainId: 42161, address: "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1"}, ] }, { - key: "0x6b175474e89094c44da98b954eedeac495271d0f", + key: "1-0x0000000000000000000000000000000000000001", name: "Meth", symbol: "MET", decimals: 0, @@ -156,8 +157,7 @@ Item { description: "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. ", websiteUrl: "", tokens: [ - { chainId: 1, address: "0x6b175474e89094c44da98b954eedeac495271d0f"}, - { chainId: 5, address: "0x6b175474e89094c44da98b954eedeac495271d0f"} + { chainId: 1, address: "0x0000000000000000000000000000000000000001"} ] }, ]) @@ -173,18 +173,22 @@ Item { verify(adaptor) // Community and not selected network are filtered out - compare(adaptor.outputModel.ModelCount.count, 2) - compare(adaptor.outputModel.get(0).key, "DAI") - compare(adaptor.outputModel.get(1).key, "ETH") + compare(adaptor.outputModel.ModelCount.count, 3) + compare(adaptor.outputModel.get(0).key, Constants.daiGroupKey) + compare(adaptor.outputModel.get(1).key, Constants.ethGroupKey) + compare(adaptor.outputModel.get(2).key, Constants.sntGroupKey) adaptor.selectedNetworkChainId = 999 compare(adaptor.outputModel.ModelCount.count, 0) - adaptor.selectedNetworkChainId = 5 - compare(adaptor.outputModel.ModelCount.count, 3) - compare(adaptor.outputModel.get(0).key, "DAI") - compare(adaptor.outputModel.get(1).key, "ETH") - compare(adaptor.outputModel.get(2).key, "STT") + adaptor.selectedNetworkChainId = 42161 + compare(adaptor.outputModel.ModelCount.count, 1) + compare(adaptor.outputModel.get(0).key, Constants.daiGroupKey) + + adaptor.selectedNetworkChainId = 10 + compare(adaptor.outputModel.ModelCount.count, 2) + compare(adaptor.outputModel.get(0).key, Constants.ethGroupKey) + compare(adaptor.outputModel.get(1).key, Constants.sntGroupKey) } } } diff --git a/storybook/qmlTests/tests/tst_PaymentRequestModal.qml b/storybook/qmlTests/tests/tst_PaymentRequestModal.qml index 29aa4f3c51c..e7a3ea161e3 100644 --- a/storybook/qmlTests/tests/tst_PaymentRequestModal.qml +++ b/storybook/qmlTests/tests/tst_PaymentRequestModal.qml @@ -8,16 +8,16 @@ import StatusQ.Controls import QtQuick.Controls -import Models -import Storybook - import utils -import AppLayouts.Wallet.stores as WalletStores import AppLayouts.Wallet.adaptors import AppLayouts.Chat.popups import shared.stores as SharedStores +import Storybook +import Models +import Mocks + Item { id: root width: 800 @@ -28,6 +28,17 @@ Item { readonly property var accounts: WalletAccountsModel {} readonly property var flatNetworks: NetworksModel.flatNetworks + + readonly property var tokensStore: TokensStoreMock { + tokenGroupsModel: TokenGroupsModel {} + tokenGroupsForChainModel: TokenGroupsModel { + skipInitialLoad: true + } + searchResultModel: TokenGroupsModel { + skipInitialLoad: true + tokenGroupsForChainModel: d.tokensStore.tokenGroupsForChainModel + } + } } Component { @@ -42,89 +53,20 @@ Item { formatCurrencyAmount: currencyStore.formatCurrencyAmount flatNetworksModel: d.flatNetworks accountsModel: d.accounts - assetsModel: ListModel { - Component.onCompleted: populateModel() - - readonly property var data: [ - { - key: Constants.ethGroupKey, - name: "eth", - symbol: "ETH", - chainId: NetworksModel.ethNet, - address: "0xbbc200", - decimals: 18, - iconSource: ModelsData.assets.eth, - logoUri: ModelsData.assets.eth, - marketDetails: { - currencyPrice: { - amount: 1, - displayDecimals: true - } - } - }, - { - key: Constants.sntGroupKey, - name: "snt", - symbol: "SNT", - chainId: NetworksModel.ethNet, - address: "0xbbc2000000000000000000000000000000000123", - decimals: 18, - iconSource: ModelsData.assets.snt, - logoUri: ModelsData.assets.snt, - marketDetails: { - currencyPrice: { - amount: 1, - displayDecimals: true - } - } - }, - { - key: Constants.daiGroupKey, - name: "dai", - symbol: "DAI", - chainId: NetworksModel.ethNet, - address: "0xbbc2000000000000000000000000000000550567", - decimals: 2, - iconSource: ModelsData.assets.dai, - logoUri: ModelsData.assets.dai, - marketDetails: { - currencyPrice: { - amount: 1, - displayDecimals: true - } - } - }, - ] - readonly property var sepArbData: [ - { - key: Constants.sttGroupKey, - name: "stt", - symbol: "STT", - chainId: NetworksModel.sepArbChainId, - address: "0xbbc2000000000000000000000000000000550567", - decimals: 2, - iconSource: ModelsData.assets.snt, - logoUri: ModelsData.assets.snt, - marketDetails: { - currencyPrice: { - amount: 1, - displayDecimals: true - } - } - } - ] - - function populateModel() { - // Simulate model refresh when network is changed - clear() - append(data) - if (paymentRequestModal.selectedNetworkChainId === NetworksModel.sepArbChainId) { - append(sepArbData) - } - } + tokenGroupsForChainModel: d.tokensStore.tokenGroupsForChainModel + searchResultModel: d.tokensStore.searchResultModel + + Component.onCompleted: { + d.tokensStore.buildGroupsForChain(paymentRequestModal.selectedNetworkChainId) } - onSelectedNetworkChainIdChanged: assetsModel.populateModel() + onBuildGroupsForChain: { + d.tokensStore.buildGroupsForChain(paymentRequestModal.selectedNetworkChainId) + } + + onSelectedNetworkChainIdChanged: { + d.tokensStore.buildGroupsForChain(paymentRequestModal.selectedNetworkChainId) + } } } @@ -187,13 +129,21 @@ Item { function test_change_amount() { launchAndVerfyModal() + // Wait for the model to be populated and selection to be ready + const assetSelector = findChild(controlUnderTest, "assetSelector") + verify(!!assetSelector) + tryCompare(assetSelector.contentItem, "name", Constants.ethToken, 5000) + const amountInput = findChild(controlUnderTest, "amountInput") verify(!!amountInput) + tryVerify(() => amountInput.multiplierIndex > 0, 5000) + const amount = "1.24" amountInput.setValue(amount) + compare(amountInput.text, amount) - compare(controlUnderTest.amount, "1240000000000000000") // Raw amount is returned + compare(controlUnderTest.amount, "1240000000000000000") closeAndVerfyModal() } @@ -290,13 +240,16 @@ Item { controlUnderTest.open() tryVerify(() => controlUnderTest.opened) + // TODO: Fix the model population issue. We should be able to set the initial asset when building the control. controlUnderTest.selectedTokenGroupKey = assetGroupKey + wait(1000) // wait until change is conducted (because of callLater call in selectedHolding) compare(controlUnderTest.selectedNetworkChainId, Constants.chains.arbitrumSepoliaChainId) compare(controlUnderTest.selectedTokenGroupKey, assetGroupKey) const assetSelector = findChild(controlUnderTest, "assetSelector") verify(!!assetSelector) + compare(assetSelector.contentItem.name, "STT") controlUnderTest.selectedNetworkChainId = Constants.chains.mainnetChainId @@ -348,6 +301,7 @@ Item { controlUnderTest.selectedTokenGroupKey = assetGroupKey compare(controlUnderTest.selectedTokenGroupKey, assetGroupKey) + wait(1000) // wait until change is conducted (because of callLater call in selectedHolding) const assetSelector = findChild(controlUnderTest, "assetSelector") verify(!!assetSelector) verify(assetSelector.isSelected) diff --git a/storybook/src/Models/PaymentRequestModel.qml b/storybook/src/Models/PaymentRequestModel.qml index b889990327c..362bf9fe5ca 100644 --- a/storybook/src/Models/PaymentRequestModel.qml +++ b/storybook/src/Models/PaymentRequestModel.qml @@ -4,15 +4,17 @@ ListModel { id: root ListElement { - symbol: "WBTC" + tokenKey: "1-0xbbc2000000000000000000000000000000550567" + symbol: "DAI" amount: "0.00017" - address: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" - chainId: 1 // main + receiver: "0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240" + logoUri: "" } ListElement { + tokenKey: "10-0x0000000000000000000000000000000000000000" symbol: "ETH" amount: "12345.6789" - address: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881" - chainId: 10 // Opti + receiver: "0x7F47C2e98a4BBf5487E6fb082eC2D9Ab0E6d8881" + logoUri: "" } } diff --git a/ui/app/AppLayouts/Chat/popups/PaymentRequestModal.qml b/ui/app/AppLayouts/Chat/popups/PaymentRequestModal.qml index 17ca3e53f4c..09e237d1891 100644 --- a/ui/app/AppLayouts/Chat/popups/PaymentRequestModal.qml +++ b/ui/app/AppLayouts/Chat/popups/PaymentRequestModal.qml @@ -12,6 +12,7 @@ import StatusQ.Core.Utils as SQUtils import StatusQ.Popups.Dialog import AppLayouts.Wallet.controls +import AppLayouts.Wallet.adaptors import shared.controls import shared.popups.send.views @@ -38,7 +39,8 @@ StatusDialog { **/ required property var accountsModel /** Expected model structure: see SearchableAssetsPanel::model **/ - required property var assetsModel + required property var tokenGroupsForChainModel + required property var searchResultModel required property string currentCurrency property var formatCurrencyAmount: function() {} @@ -46,20 +48,21 @@ StatusDialog { property int selectedNetworkChainId: Constants.chains.mainnetChainId property string selectedAccountAddress property string selectedTokenGroupKey: defaultTokenGroupKey - // selected token key is automatically evaluated based on the selected group key and selected chain - readonly property string selectedTokenKey: SQUtils.ModelUtils.getByKey(d.selectedHolding.item.tokens, "chainId", root.selectedNetworkChainId, "key") - readonly property string symbol: d.selectedHolding.item.symbol - + readonly property string selectedTokenKey: d.selectedTokenKey + readonly property string selectedSymbol: d.selectedSymbol + readonly property string selectedTokenLogoUri: d.selectedTokenLogoUri readonly property string defaultTokenGroupKey: Utils.getNativeTokenGroupKey(selectedNetworkChainId) // output readonly property string amount: { - if (!d.isSelectedHoldingValidAsset || !d.selectedHolding.item.marketDetails || !d.selectedHolding.item.marketDetails.currencyPrice) { + if (!d.isSelectedHoldingValidAsset) { return "0" } return amountToSendInput.amount } + signal buildGroupsForChain() + objectName: "paymentRequestModal" width: 480 @@ -69,10 +72,11 @@ StatusDialog { title: qsTr("Payment request") + Component.onCompleted: { + root.buildGroupsForChain() + } + onAboutToShow: { - if (!!root.selectedTokenGroupKey && d.selectedHolding.available) { - holdingSelector.setSelection(d.selectedHolding.item.symbol, d.selectedHolding.item.iconSource, d.selectedHolding.item.key) - } if (!SQUtils.Utils.isMobile) amountToSendInput.forceActiveFocus() } @@ -84,25 +88,56 @@ StatusDialog { root.selectedTokenGroupKey = root.defaultTokenGroupKey } + property string selectedTokenKey: "" + property string selectedSymbol: "" + property string selectedTokenLogoUri: "" + + function updateSelectedTokenKey() { + const tokenGroup = SQUtils.ModelUtils.getByKey(holdingSelector.model, "key", root.selectedTokenGroupKey) + if (!tokenGroup) { + console.warn("cannot relove the token group for the group key", root.selectedTokenGroupKey) + } else { + const token = SQUtils.ModelUtils.getByKey(tokenGroup.tokens, "chainId", root.selectedNetworkChainId) + if (!token) { + console.warn("cannot find the token on chain", root.selectedTokenGroupKey, "for the group", root.selectedTokenGroupKey) + } else { + d.selectedTokenKey = token.key + d.selectedSymbol = token.symbol + d.selectedTokenLogoUri = token.image + } + } + + holdingSelector.setSelection(tokenGroup.symbol, tokenGroup.logoUri, tokenGroup.key) + } + readonly property ModelEntry selectedHolding: ModelEntry { sourceModel: holdingSelector.model key: "key" value: root.selectedTokenGroupKey onValueChanged: { - if (value !== undefined && !available) { + if (available) { + Qt.callLater(d.updateSelectedTokenKey) + } else if (root.selectedTokenGroupKey !== root.defaultTokenGroupKey) { Qt.callLater(d.resetSelectedToken) - } else { - holdingSelector.setSelection(d.selectedHolding.item.symbol, d.selectedHolding.item.iconSource, d.selectedHolding.item.key) } } onAvailableChanged: { - if (value !== undefined && !available) { + if (available) { + Qt.callLater(d.updateSelectedTokenKey) + } else if (root.selectedTokenGroupKey !== root.defaultTokenGroupKey) { Qt.callLater(d.resetSelectedToken) } } } readonly property bool isSelectedHoldingValidAsset: selectedHolding.available + + readonly property var adaptor: PaymentRequestAdaptor { + tokenGroupsForChainModel: root.tokenGroupsForChainModel + searchResultModel: root.searchResultModel + selectedNetworkChainId: root.selectedNetworkChainId + flatNetworksModel: root.flatNetworksModel + } } footer: StatusDialogFooter { @@ -161,8 +196,17 @@ StatusDialog { anchors.right: parent.right anchors.topMargin: (Theme.halfPadding / 2) - model: root.assetsModel - onSelected: { + model: d.adaptor.outputModel + hasMoreItems: d.adaptor.outputModel.hasMoreItems + isLoadingMore: d.adaptor.outputModel.isLoadingMore + + onSearch: function(keyword) { + d.adaptor.search(keyword) + } + + onLoadMoreRequested: d.adaptor.loadMoreItems() + + onSelected: (groupKey) => { root.selectedTokenGroupKey = groupKey } } @@ -263,6 +307,8 @@ StatusDialog { onClicked: { root.selectedNetworkChainId = model.chainId + root.buildGroupsForChain() + root.selectedTokenGroupKey = root.defaultTokenGroupKey networkSelector.popup.close() } } diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml index 38c1a32205c..acd4ed241cd 100644 --- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml +++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml @@ -224,21 +224,41 @@ Item { } // key can be either a group key or token key - function formatBalance(amount, key) { + function getSymbolAndDecimalsForTokenFomModel(model, key) { let decimals = 0 let symbol = "" - const tokenGroup = ModelUtils.getByKey(WalletStore.RootStore.tokensStore.tokenGroupsModel, "key", key) + const tokenGroup = ModelUtils.getByKey(model, "key", key) if (!!tokenGroup) { - decimals = tokenGroup.decimals - symbol = tokenGroup.symbol + return [tokenGroup.symbol, tokenGroup.decimals] } else { - for (let i = 0; i < WalletStore.RootStore.tokensStore.tokenGroupsModel.ModelCount.count; i++) { - let tG = ModelUtils.get(WalletStore.RootStore.tokensStore.tokenGroupsModel, i) + for (let i = 0; i < model.ModelCount.count; i++) { + let tG = ModelUtils.get(model, i) const token = ModelUtils.getByKey(tG.tokens, "key", key) if (!!token) { - decimals = token.decimals + return [token.symbol, token.decimals] + } + } + } + return ["", 0] + } + + // key can be either a group key or token key + function formatBalance(amount, key) { + // try to find it in token groups + let [symbol, decimals] = getSymbolAndDecimalsForTokenFomModel(WalletStore.RootStore.tokensStore.tokenGroupsModel, key); + if (!symbol) { + // fallback and try to find it in token groups for chain (in case it's swap, payment request...) + [symbol, decimals] = getSymbolAndDecimalsForTokenFomModel(WalletStore.RootStore.tokensStore.tokenGroupsForChainModel, key); + if (!symbol) { + // fallback and try to find it in search result model (in case of lazy loading the token is not displayed from the start + // but is displayed cause it matched the search criteria) + [symbol, decimals] = getSymbolAndDecimalsForTokenFomModel(WalletStore.RootStore.tokensStore.searchResultModel, key); + if (!symbol) { + // fallback and fetch details from the backend, this call fetch all tokens from statusgo and + // searchs for the token that matches the key (this is definitely the last resort) + const token = WalletStore.RootStore.tokensStore.getTokenByKeyOrGroupKeyFromAllTokens(key) symbol = token.symbol - break + decimals = token.decimals } } } diff --git a/ui/app/AppLayouts/Wallet/adaptors/GroupsModel.qml b/ui/app/AppLayouts/Wallet/adaptors/GroupsModel.qml new file mode 100644 index 00000000000..c2cf25ccea5 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/adaptors/GroupsModel.qml @@ -0,0 +1,96 @@ +import QtQuick + +import StatusQ +import StatusQ.Core.Utils + +import QtModelsToolkit +import SortFilterProxyModel + +import utils + +/** + Reusable component for filtering and preparing token groups. + Handles network filtering, proxy roles, sorting, and callbacks dynamically. +*/ +SortFilterProxyModel { + id: root + + required property var sourceTokenModel + required property var flatNetworksModel + required property int selectedNetworkChainId + required property string modelObjectName + required property string innerObjectName + required property var onFetchMoreCallback + + property var onSearchCallback: null + property var sourceModelConnectionTarget: null + + objectName: modelObjectName + + readonly property string networkName: ModelUtils.getByKey(flatNetworksModel, "chainId", selectedNetworkChainId, "chainName") + + function isPresentOnEnabledNetwork(tokens, selectedChainId) { + if(selectedChainId < 0) + return true + return !!ModelUtils.getFirstModelEntryIf( + tokens, + (t) => { + return selectedChainId === t.chainId + }) + } + + sourceModel: SortFilterProxyModel { + objectName: innerObjectName + sourceModel: root.sourceTokenModel + filters: [ + FastExpressionFilter { + expression: { + return root.isPresentOnEnabledNetwork(model.tokens, root.selectedNetworkChainId) + } + expectedRoles: ["tokens"] + } + ] + } + + proxyRoles: [ + ConstantRole { + name: "sectionName" + value: qsTr("Popular assets on %1").arg(root.networkName) + }, + FastExpressionRole { + function tokenIcon(symbol) { + return Constants.tokenIcon(symbol) + } + name: "iconSource" + expression: model.logoUri || tokenIcon(model.symbol) + expectedRoles: ["logoUri", "symbol"] + } + ] + + sorters: [ + RoleSorter { + roleName: "name" + } + ] + filters: [ + ValueFilter { + roleName: "communityId" + value: "" + } + ] + + property bool hasMoreItems: false + property bool isLoadingMore: false + + function search(keyword) { + if (onSearchCallback) { + onSearchCallback(keyword) + } + } + + function fetchMore() { + if (onFetchMoreCallback) { + onFetchMoreCallback() + } + } +} diff --git a/ui/app/AppLayouts/Wallet/adaptors/PaymentRequestAdaptor.qml b/ui/app/AppLayouts/Wallet/adaptors/PaymentRequestAdaptor.qml index 68791ba6408..95374ed51b8 100644 --- a/ui/app/AppLayouts/Wallet/adaptors/PaymentRequestAdaptor.qml +++ b/ui/app/AppLayouts/Wallet/adaptors/PaymentRequestAdaptor.qml @@ -11,68 +11,125 @@ import utils QObject { id: root + /** + Filters and prepares token groups for PaymentRequestModal needs. Filters tokens by selected network chain ID + and excludes community tokens. Supports lazy loading and search functionality. + + Expected tokenGroupsForChainModel structure: + - key: string -> token group key (e.g. "eth-native" or cross-chain ID) + - name: string -> token group name (e.g. "Ether") + - symbol: string -> token symbol (e.g. "ETH") + - decimals: int -> number of decimal places + - logoUri: string -> token group logo/image URL + - tokens: model/array -> contains tokens that belong to this group, each token has: + - chainId: int -> chain ID where this token exists + - address: string -> token contract address (or "0x0000..." for native tokens) + - key: string -> unique token key (e.g. "1-0x0000000000000000000000000000000000000000") + - symbol: string -> token symbol + - name: string -> token name + - decimals: int -> token decimals + - image: string -> token image URL + - communityId: string -> optional; ID of the community this token belongs to (empty string for non-community tokens) + - marketDetails: object -> optional; market data containing properties like `currencyPrice` + + Expected searchResultModel structure: + Same as tokenGroupsForChainModel, but contains only token groups that match the search keyword. + + Expected flatNetworksModel structure: + - chainId: int -> unique chain identifier + - chainName: string -> network name (e.g. "Ethereum Mainnet") + - iconUrl: string -> network icon URL + + Computed values in outputModel: + - iconSource: string -> computed from logoUri or Constants.tokenIcon(symbol) - should be removed, cause all tokens should have non empty logoUri role + - sectionName: string -> e.g. "Popular assets on Ethereum Mainnet" + */ + // Input API - /** Plain tokens model without balances **/ - required property var tokenGroupsModel + /** Token groups for chain, loaded on demand, without balances **/ + required property var tokenGroupsForChainModel + /** token groups that match the search keyword **/ + property var searchResultModel /** All networks model **/ required property var flatNetworksModel /** Selected network chain id **/ required property int selectedNetworkChainId - // output model - readonly property SortFilterProxyModel outputModel: SortFilterProxyModel { - objectName: "TokenSelectorViewAdaptor_outputModel" - - readonly property string networkName: ModelUtils.getByKey(root.flatNetworksModel, "chainId", root.selectedNetworkChainId, "chainName") - - sourceModel: SortFilterProxyModel { - objectName: "PaymentRequestAdaptor_allTokensPlain" - sourceModel: root.tokenGroupsModel - filters: [ - FastExpressionFilter { - function isPresentOnEnabledNetwork(tokens, selectedChainId) { - if(selectedChainId < 0) - return true - return !!ModelUtils.getFirstModelEntryIf( - tokens, - (t) => { - return selectedChainId === t.chainId - }) - } - expression: { - return isPresentOnEnabledNetwork(model.tokens, root.selectedNetworkChainId) - } - expectedRoles: ["tokens"] - } - ] + + function loadMoreItems() { + root.outputModel.fetchMore() + } + + function search(keyword) { + let kw = keyword.trim() + if (kw === "") { + root.outputModel.search(kw) + d.searchKeyword = kw + } else { + d.searchKeyword = kw + root.outputModel.search(kw) } + } + + // output model - lazy loaded subset for display + readonly property var outputModel: !!d.searchKeyword ? d.searchModel : d.fullOutputModel - proxyRoles: [ - ConstantRole { - name: "sectionName" - value: qsTr("Popular assets on %1").arg(outputModel.networkName) - }, - FastExpressionRole { - function tokenIcon(symbol) { - return Constants.tokenIcon(symbol) - } - name: "iconSource" - expression: model.logoUri || tokenIcon(model.symbol) - expectedRoles: ["logoUri", "symbol"] + QtObject { + id: d + + property string searchKeyword: "" + + // output model - lazy loaded full model + readonly property GroupsModel fullOutputModel: GroupsModel { + modelObjectName: "TokenSelectorViewAdaptor_allTokensModel" + innerObjectName: "PaymentRequestAdaptor_allTokensPlain" + sourceTokenModel: root.tokenGroupsForChainModel + flatNetworksModel: root.flatNetworksModel + selectedNetworkChainId: root.selectedNetworkChainId + onFetchMoreCallback: function() { + root.tokenGroupsForChainModel.fetchMore() } - ] + sourceModelConnectionTarget: root.tokenGroupsForChainModel + } - sorters: [ - // FIXME popular first and then by name - RoleSorter { - roleName: "name" + // output model - search results model + readonly property GroupsModel searchModel: GroupsModel { + modelObjectName: "TokenSelectorViewAdaptor_outputSearchResultModel" + innerObjectName: "PaymentRequestAdaptor_searchResultTokensPlain" + sourceTokenModel: root.searchResultModel + flatNetworksModel: root.flatNetworksModel + selectedNetworkChainId: root.selectedNetworkChainId + onFetchMoreCallback: function() { + root.searchResultModel.fetchMore() } - ] - filters: [ - ValueFilter { - roleName: "communityId" - value: "" + onSearchCallback: function(keyword) { + root.searchResultModel.search(keyword) } - ] + sourceModelConnectionTarget: root.searchResultModel + } + } + + Connections { + target: root.tokenGroupsForChainModel + + function onHasMoreItemsChanged() { + d.fullOutputModel.hasMoreItems = root.tokenGroupsForChainModel.hasMoreItems + } + + function onIsLoadingMoreChanged() { + d.fullOutputModel.isLoadingMore = root.tokenGroupsForChainModel.isLoadingMore + } + } + + Connections { + target: root.searchResultModel + + function onHasMoreItemsChanged() { + d.searchModel.hasMoreItems = root.searchResultModel.hasMoreItems + } + + function onIsLoadingMoreChanged() { + d.searchModel.isLoadingMore = root.searchResultModel.isLoadingMore + } } } diff --git a/ui/app/AppLayouts/Wallet/controls/AssetSelector.qml b/ui/app/AppLayouts/Wallet/controls/AssetSelector.qml index 8919a81c5a4..457fa71a189 100644 --- a/ui/app/AppLayouts/Wallet/controls/AssetSelector.qml +++ b/ui/app/AppLayouts/Wallet/controls/AssetSelector.qml @@ -25,8 +25,8 @@ Control { signal selected(string groupKey) signal loadMoreRequested() - function setSelection(name: string, icon: url, tokenGroupKey: string) { - button.name = name + function setSelection(symbol, icon, tokenGroupKey) { + button.name = symbol button.icon = icon button.selected = true @@ -83,10 +83,8 @@ Control { onLoadMoreRequested: root.loadMoreRequested() - function setCurrentAndClose(name, icon) { - button.name = name - button.icon = icon - button.selected = true + function setCurrentAndClose(symbol, icon, tokenGroupKey) { + root.setSelection(symbol, icon, tokenGroupKey) dropdown.close() } @@ -96,10 +94,9 @@ Control { console.error("asset couldn't be resolved for the key", key) return } - highlightedKey = key - setCurrentAndClose(entry.symbol, entry.iconSource) - root.selected(key) + setCurrentAndClose(entry.symbol, entry.logoUri, entry.key) + root.selected(entry.key) } onSearch: function(keyword) { diff --git a/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml b/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml index d1979aea30f..99bca2856fd 100644 --- a/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml +++ b/ui/app/AppLayouts/Wallet/controls/TokenSelectorButton.qml @@ -94,7 +94,7 @@ Control { font.pixelSize: root.size === TokenSelectorButton.Size.Normal ? 28 : 22 lineHeightMode: Text.FixedHeight lineHeight: root.size === TokenSelectorButton.Size.Normal ? 38 : 30 - color: root.hovered ? StatusColors.getColor("blue") : StatusColors.getColor("darkBlue") + color: root.hovered ? StatusColors.getColor("blue", 1) : StatusColors.getColor("darkBlue", 1) elide: Text.ElideRight text: root.name @@ -112,6 +112,8 @@ Control { cursorShape: root.enabled ? Qt.PointingHandCursor : undefined anchors.fill: parent - onClicked: root.clicked() + onClicked: { + root.clicked() + } } } diff --git a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml index 8ed0211b612..4d70dae3436 100644 --- a/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml +++ b/ui/app/AppLayouts/Wallet/popups/simpleSend/SimpleSendModal.qml @@ -189,6 +189,9 @@ StatusDialog { !amountToSend.markAsInvalid && amountToSend.valid + /** Returns token that matches provided key (key can be token or group key). **/ + property var getTokenByKeyOrGroupKeyFromAllTokens: function(key){ return {}} + /** Input function to resolve Ens Name **/ required property var fnResolveENS @@ -207,6 +210,19 @@ StatusDialog { QtObject { id: d + readonly property bool selectedTokenExistsInAssetsModel: { + const tokenGroup = SQUtils.ModelUtils.getByKey(root.assetsModel, "key", root.selectedGroupKey) + return !!tokenGroup + } + + readonly property var resolvedSelectedToken: { + if (d.selectedTokenExistsInAssetsModel) { + return {} + } + + return root.getTokenByKeyOrGroupKeyFromAllTokens(root.selectedGroupKey) + } + readonly property real scrollViewContentY: scrollView.flickable.contentY onScrollViewContentYChanged: { const buffer = sendModalHeader.height + scrollViewLayout.spacing * 2 @@ -226,13 +242,26 @@ StatusDialog { cacheOnRemoval: true onItemChanged: d.setAssetInTokenSelector() onAvailableChanged: d.setAssetInTokenSelector() + onValueChanged: { + // if it's non-interactive mode and the assets model doesn't contain selectedGroupKey set the UI properly + if (!root.interactive && !!root.selectedGroupKey) { + if (d.selectedTokenExistsInAssetsModel) { + return + } + if (!d.resolvedSelectedToken) { + console.error("cannot init the send flow for", root.selectedGroupKey) + return + } + + d.setTokenOnBothHeaders(d.resolvedSelectedToken.symbol, d.resolvedSelectedToken.logoUri, d.resolvedSelectedToken.key) + } + } } // Holds if the asset entry is valid - readonly property bool selectedAssetEntryValid: (selectedAssetEntry.itemRemovedFromModel || - selectedAssetEntry.available) && - isSelectedAssetAvailableInSelectedNetwork && - !!selectedAssetEntry.item + readonly property bool selectedAssetEntryValid: isSelectedAssetAvailableInSelectedNetwork && + ((selectedAssetEntry.itemRemovedFromModel || selectedAssetEntry.available) && + !!selectedAssetEntry.item || !root.interactive) // Used to set selected asset in token selector function setAssetInTokenSelector() { @@ -244,6 +273,9 @@ StatusDialog { } readonly property bool isSelectedAssetAvailableInSelectedNetwork: { + if (!root.interactive) { + return true + } const chainId = root.selectedChainId const tokensKey = root.selectedGroupKey if (!tokensKey || !chainId || !root.groupedAccountAssetsModel) @@ -311,6 +343,10 @@ StatusDialog { } readonly property var debounceResetTokenSelector: Backpressure.debounce(root, 200, function() { + if (!root.interactive) { + // non interactive flow should not be resetted, it should display what was requested + return + } if(!selectedAssetEntryValid && !selectedCollectibleEntryValid) { // reset token selector in case selected tokens doesnt exist in either models d.setTokenOnBothHeaders("", "", "") @@ -326,12 +362,21 @@ StatusDialog { root.selectedRawAmount = amountToSend.amount }) - readonly property string selectedCryptoTokenSymbol: selectedAssetEntryValid ? - selectedAssetEntry.item.symbol: - selectedCollectibleEntryValid ? - selectedCollectibleEntry.item.symbol: "" + readonly property string selectedCryptoTokenSymbol: { + if (!d.selectedTokenExistsInAssetsModel) { + return d.resolvedSelectedToken.symbol + } + + return selectedAssetEntryValid ? + selectedAssetEntry.item.symbol: + selectedCollectibleEntryValid ? + selectedCollectibleEntry.item.symbol: "" + } readonly property double maxSafeCryptoValue: { + if (!d.selectedTokenExistsInAssetsModel) { + return 0 + } !!d.selectedAssetEntry.item.balances ? d.selectedAssetEntry.item.balances.ModelCount.count : null if (selectedCollectibleEntryValid) { let collectibleBalance = SQUtils.ModelUtils.getByKey(selectedCollectibleEntry.item.ownership, "accountAddress", root.selectedAccountAddress, "balance") @@ -349,6 +394,9 @@ StatusDialog { } readonly property bool allowTryingToSendEnteredAmount: { + if (!d.selectedTokenExistsInAssetsModel) { + false + } if (!selectedCollectibleEntryValid && !d.selectedAssetEntryValid) { return true // if no asset selected } @@ -641,11 +689,20 @@ StatusDialog { selectedSymbol: amountToSend.fiatMode ? root.currentCurrency: d.selectedCryptoTokenSymbol - cryptoPrice: root.marketDataNotAvailable ? 0 : - d.selectedAssetEntry.item.marketDetails.currencyPrice.amount - multiplierIndex: !!d.selectedAssetEntryValid && + cryptoPrice: !root.interactive || root.marketDataNotAvailable ? 0 + : !!d.selectedAssetEntry.item && + !!d.selectedAssetEntry.item.marketDetails && + d.selectedAssetEntry.item.marketDetails.currencyPrice.amount + multiplierIndex: { + if (!d.selectedTokenExistsInAssetsModel) { + return d.resolvedSelectedToken.decimals + } + + return !!d.selectedAssetEntryValid && + !!d.selectedAssetEntry.item && !!d.selectedAssetEntry.item.decimals ? d.selectedAssetEntry.item.decimals : 0 + } /** this is needed because in some cases the multiplier index is set after rawAmount and this leads to incorrect parsing of amount **/ onMultiplierIndexChanged: d.setRawValue() diff --git a/ui/app/AppLayouts/Wallet/stores/TokensStore.qml b/ui/app/AppLayouts/Wallet/stores/TokensStore.qml index 8b2d67afba1..dda45b39a77 100644 --- a/ui/app/AppLayouts/Wallet/stores/TokensStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/TokensStore.qml @@ -114,6 +114,35 @@ QtObject { root._allTokensModule.buildGroupsForChain(chainId, mandatoryKeys) } + // Due to performance reasons, use this function as the last option, when you're sure the token is not present in the models. + function getTokenByKeyOrGroupKeyFromAllTokens(key) { + + const defaultValue = { + key: "", + groupKey: "", + crossChainId: "", + address: "", + name: "", + symbol: "", + decimals: 0, + hainId: 0, + logoUri: "", + customToken:false, + communityId: "", + type: "" + } + + const jsonToken = root._allTokensModule.getTokenByKeyOrGroupKeyFromAllTokens(key) + + try { + return JSON.parse(jsonToken) + } + catch (e) { + console.warn("error parsing token for the key: ", key) + return defaultValue + } + } + function getHistoricalDataForToken(tokenKey, currency) { root._allTokensModule.getHistoricalDataForToken(tokenKey, currency) } diff --git a/ui/app/mainui/Handlers/SendModalHandler.qml b/ui/app/mainui/Handlers/SendModalHandler.qml index d7ebb4d0bf6..96f76e276e2 100644 --- a/ui/app/mainui/Handlers/SendModalHandler.qml +++ b/ui/app/mainui/Handlers/SendModalHandler.qml @@ -410,7 +410,17 @@ QtObject { } if (!groupKey) { - console.error("cannot resolve group key from the provided token key", tokenKey) + // fallback and fetch details from the backend, this call fetch all tokens from statusgo and + // searchs for the token that matches the key (this is definitely the last resort) + const token = WalletStores.RootStore.tokensStore.getTokenByKeyOrGroupKeyFromAllTokens(tokenKey) + groupKey = token.groupKey + chainId = token.chainId + + if (!groupKey) { + console.error("cannot resolve group key from the provided token key", tokenKey) + Global.openInfoPopup(qsTr("Info"), qsTr("Token that you're trying to send is not supported.")) + return + } } @@ -471,6 +481,8 @@ QtObject { fnResolveENS: root.fnResolveENS marketDataNotAvailable: handler.marketDataNotAvailable + getTokenByKeyOrGroupKeyFromAllTokens: WalletStores.RootStore.tokensStore.getTokenByKeyOrGroupKeyFromAllTokens + onOpened: { if(isValidParameter(root.simpleSendParams.interactive)) { interactive = root.simpleSendParams.interactive diff --git a/ui/app/mainui/Popups.qml b/ui/app/mainui/Popups.qml index 7bcef3ea448..944d9e9b929 100644 --- a/ui/app/mainui/Popups.qml +++ b/ui/app/mainui/Popups.qml @@ -1,5 +1,6 @@ import QtCore import QtQuick +import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import QtQml.Models @@ -21,7 +22,6 @@ import AppLayouts.Communities.popups import AppLayouts.Communities.helpers import AppLayouts.Wallet.popups.buy import AppLayouts.Wallet.popups -import AppLayouts.Wallet.adaptors import AppLayouts.Communities.stores import AppLayouts.Profile.helpers @@ -123,6 +123,7 @@ QtObject { Global.termsOfUseRequested.connect(() => openPopup(termsOfUsePopupComponent)) Global.openNewsMessagePopupRequested.connect(openNewsMessagePopup) Global.quitAppRequested.connect(() => openPopup(quitConfirmPopupComponent)) + Global.openInfoPopup.connect(openInfoPopup) } property var currentPopup @@ -437,6 +438,10 @@ QtObject { openPopup(newsMessageComponent, {notification: notification, notificationId: notificationId}) } + function openInfoPopup(title, message) { + openPopup(infoComponent, {title: title, message, message}) + } + readonly property list _components: [ Component { id: removeContactConfirmationDialog @@ -1375,24 +1380,26 @@ QtObject { id: paymentRequestModalComponent PaymentRequestModal { id: paymentRequestModal - readonly property var paymentRequestAdaptor: PaymentRequestAdaptor { - tokenGroupsModel: WalletStores.RootStore.tokensStore.tokenGroupsModel - selectedNetworkChainId: paymentRequestModal.selectedNetworkChainId - flatNetworksModel: root.networksStore.allNetworks - } + property var callback: null currentCurrency: root.currencyStore.currentCurrency formatCurrencyAmount: root.currencyStore.formatCurrencyAmount flatNetworksModel: root.networksStore.activeNetworks accountsModel: WalletStores.RootStore.nonWatchAccounts - assetsModel: paymentRequestAdaptor.outputModel + + tokenGroupsForChainModel: WalletStores.RootStore.tokensStore.tokenGroupsForChainModel + searchResultModel: WalletStores.RootStore.tokensStore.searchResultModel + + onBuildGroupsForChain: { + WalletStores.RootStore.tokensStore.buildGroupsForChain(selectedNetworkChainId, "") + } onAccepted: { if (!callback) { console.error("No callback set for Payment Request") return } - callback(selectedAccountAddress, amount, selectedTokenKey, symbol) + callback(selectedAccountAddress, amount, selectedTokenKey, selectedSymbol, selectedTokenLogoUri) } destroyOnClose: true } @@ -1416,6 +1423,25 @@ QtObject { confirmButtonLabel: qsTr("Sign out & Quit") onConfirmButtonClicked: Qt.exit(0) } + }, + + Component { + id: infoComponent + + StatusDialog { + id: infoPopup + + property string message + + StatusBaseText { + anchors.fill: parent + font.pixelSize: Constants.keycard.general.fontSize2 + color: Theme.palette.directColor1 + text: infoPopup.message + } + + standardButtons: Dialog.Ok + } } ] } diff --git a/ui/i18n/qml_base_en.ts b/ui/i18n/qml_base_en.ts index b5124cca84e..1822357522b 100644 --- a/ui/i18n/qml_base_en.ts +++ b/ui/i18n/qml_base_en.ts @@ -8151,6 +8151,13 @@ L2 fee: %2 + + GroupsModel + + Popular assets on %1 + + + HelpUsImproveStatusPage @@ -12907,13 +12914,6 @@ to load - - PaymentRequestAdaptor - - Popular assets on %1 - - - PaymentRequestCardDelegate @@ -15177,6 +15177,17 @@ to load + + SendModalHandler + + Info + + + + Token that you're trying to send is not supported. + + + SendModalHeader diff --git a/ui/i18n/qml_base_lokalise_en.ts b/ui/i18n/qml_base_lokalise_en.ts index a7165318228..188d8d29dc5 100644 --- a/ui/i18n/qml_base_lokalise_en.ts +++ b/ui/i18n/qml_base_lokalise_en.ts @@ -9955,6 +9955,14 @@ Back up now + + GroupsModel + + Popular assets on %1 + GroupsModel + Popular assets on %1 + + HelpUsImproveStatusPage @@ -15717,14 +15725,6 @@ Symbols - - PaymentRequestAdaptor - - Popular assets on %1 - PaymentRequestAdaptor - Popular assets on %1 - - PaymentRequestCardDelegate @@ -18480,6 +18480,19 @@ Review Send + + SendModalHandler + + Info + SendModalHandler + Info + + + Token that you're trying to send is not supported. + SendModalHandler + Token that you're trying to send is not supported. + + SendModalHeader diff --git a/ui/i18n/qml_cs.ts b/ui/i18n/qml_cs.ts index 504db3a2a1d..26114309344 100644 --- a/ui/i18n/qml_cs.ts +++ b/ui/i18n/qml_cs.ts @@ -8193,6 +8193,13 @@ L2 poplatek: %2 Zálohovat nyní + + GroupsModel + + Popular assets on %1 + Populární aktiva na %1 + + HelpUsImproveStatusPage @@ -13009,13 +13016,6 @@ selhalo Symboly - - PaymentRequestAdaptor - - Popular assets on %1 - Populární aktiva na %1 - - PaymentRequestCardDelegate @@ -15285,6 +15285,17 @@ selhalo Zkontrolovat odeslání + + SendModalHandler + + Info + + + + Token that you're trying to send is not supported. + + + SendModalHeader diff --git a/ui/i18n/qml_es.ts b/ui/i18n/qml_es.ts index 4ec9d16d3fd..5c6d9d4a8bb 100644 --- a/ui/i18n/qml_es.ts +++ b/ui/i18n/qml_es.ts @@ -8166,6 +8166,13 @@ Tarifa L2: %2 Hacer copia de seguridad ahora + + GroupsModel + + Popular assets on %1 + Activos populares en %1 + + HelpUsImproveStatusPage @@ -12930,13 +12937,6 @@ al cargar Símbolos - - PaymentRequestAdaptor - - Popular assets on %1 - Activos populares en %1 - - PaymentRequestCardDelegate @@ -15200,6 +15200,17 @@ al cargar Revisar envío + + SendModalHandler + + Info + + + + Token that you're trying to send is not supported. + + + SendModalHeader @@ -15764,31 +15775,31 @@ al cargar SimpleSendModal To - Para + Para Fees - + Insufficient funds for send transaction - Fondos insuficientes para la transacción de envío + Add ETH - Agregar ETH + Add BNB - Agregar BNB + Add assets - Agregar activos + Agregar activos Add %1 - Agregar %1 + Agregar %1 diff --git a/ui/i18n/qml_ko.ts b/ui/i18n/qml_ko.ts index 51771bcf355..daaa56496cf 100644 --- a/ui/i18n/qml_ko.ts +++ b/ui/i18n/qml_ko.ts @@ -8138,6 +8138,13 @@ L2 수수료: %2 지금 백업 + + GroupsModel + + Popular assets on %1 + %1에서 인기 있는 자산 + + HelpUsImproveStatusPage @@ -12892,13 +12899,6 @@ to load 기호 - - PaymentRequestAdaptor - - Popular assets on %1 - %1에서 인기 있는 자산 - - PaymentRequestCardDelegate @@ -15156,6 +15156,17 @@ to load 보내기 검토 + + SendModalHandler + + Info + + + + Token that you're trying to send is not supported. + + + SendModalHeader @@ -15716,31 +15727,31 @@ to load SimpleSendModal To - 수신 + 수신 Fees - 수수료 + 수수료 Insufficient funds for send transaction - 전송 트랜잭션을 수행하기에 잔액이 부족합니다 + Add ETH - ETH 추가 + Add BNB - BNB 추가 + Add assets - 자산 추가 + 자산 추가 Add %1 - %1 추가 + %1 추가 diff --git a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml index 290bbe369a3..9da16111f52 100644 --- a/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml +++ b/ui/imports/shared/controls/chat/ChatInputLinksPreviewArea.qml @@ -118,7 +118,11 @@ Control { return root.formatBalance(model.amount, model.tokenKey) } symbol: model.symbol - onClose: root.removePaymentRequestPreview(model.index) + logoUri: model.logoUri + + onClose: { + root.removePaymentRequestPreview(model.index) + } } } Repeater { diff --git a/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml b/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml index 921e28dccdc..202c2df83ca 100644 --- a/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml +++ b/ui/imports/shared/controls/chat/PaymentRequestMiniCardDelegate.qml @@ -13,6 +13,7 @@ CalloutCard { required property string amount required property string symbol + required property string logoUri readonly property bool containsMouse: mouseArea.hovered || closeButton.hovered @@ -56,7 +57,7 @@ CalloutCard { asset.bgHeight: 20 asset.bgWidth: 20 asset.isImage: true - asset.name: Constants.tokenIcon(root.symbol) + asset.name: root.logoUri || Constants.tokenIcon(root.symbol) } } diff --git a/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml b/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml index e2d0d546326..26431f4ea3d 100644 --- a/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml +++ b/ui/imports/shared/controls/delegates/PaymentRequestCardDelegate.qml @@ -19,6 +19,7 @@ CalloutCard { required property string amount required property string symbol required property string address + required property string logoUri required property bool areTestNetworksEnabled @@ -73,7 +74,7 @@ CalloutCard { StatusRoundedImage { id: symbolImage anchors.verticalCenter: parent.verticalCenter - image.source: Constants.tokenIcon(root.symbol) + image.source: root.logoUri || Constants.tokenIcon(root.symbol) width: 44 height: width image.layer.enabled: true diff --git a/ui/imports/shared/views/chat/LinksMessageView.qml b/ui/imports/shared/views/chat/LinksMessageView.qml index 441d66eccfe..6e11bb9abe3 100644 --- a/ui/imports/shared/views/chat/LinksMessageView.qml +++ b/ui/imports/shared/views/chat/LinksMessageView.qml @@ -82,10 +82,14 @@ Flow { } symbol: model.symbol address: model.receiver + logoUri: model.logoUri senderName: root.senderName senderThumbnailImage: root.senderThumbnailImage senderColorId: root.senderColorId - onClicked: root.paymentRequestClicked(model.index) + + onClicked: { + root.paymentRequestClicked(model.index) + } } } diff --git a/ui/imports/utils/Global.qml b/ui/imports/utils/Global.qml index c2ac62d650c..f4dcaa2c560 100644 --- a/ui/imports/utils/Global.qml +++ b/ui/imports/utils/Global.qml @@ -90,6 +90,8 @@ QtObject { signal openNewsMessagePopupRequested(var notification, string notificationId) + signal openInfoPopup(string title, string message) + // BuyCrypto signal openBuyCryptoModalRequested(var formDataParams)