From 27abf30fc88896acc5755c1368b86c6b2dfc6e5e Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 8 Sep 2020 11:52:09 -0400 Subject: [PATCH] feat: list, toggle and remove custom tokens --- src/app/wallet/view.nim | 29 ++- src/app/wallet/views/token_list.nim | 21 ++- src/status/libstatus/default_tokens.nim | 12 -- src/status/libstatus/tokens.nim | 83 ++++++--- src/status/wallet.nim | 9 + .../AppLayouts/Wallet/AddCustomTokenModal.qml | 2 + .../AppLayouts/Wallet/TokenSettingsModal.qml | 5 +- ui/app/AppLayouts/Wallet/WalletHeader.qml | 1 + .../components/TokenSettingsModalContent.qml | 167 +++++++++++++----- 9 files changed, 231 insertions(+), 98 deletions(-) diff --git a/src/app/wallet/view.nim b/src/app/wallet/view.nim index e13453b942..a868b9339c 100644 --- a/src/app/wallet/view.nim +++ b/src/app/wallet/view.nim @@ -17,6 +17,7 @@ QtObject: focusedAccount: AccountItemView currentTransactions: TransactionList defaultTokenList: TokenList + customTokenList: TokenList status: Status totalFiatBalance: string etherscanLink: string @@ -35,6 +36,7 @@ QtObject: self.focusedAccount.delete self.currentTransactions.delete self.defaultTokenList.delete + self.customTokenList.delete self.QAbstractListModel.delete proc setup(self: WalletView) = @@ -50,6 +52,7 @@ QtObject: result.currentTransactions = newTransactionList() result.currentCollectiblesLists = newCollectiblesList() result.defaultTokenList = newTokenList() + result.customTokenList = newTokenList() result.totalFiatBalance = "" result.etherscanLink = "" result.safeLowGasPrice = "0" @@ -295,6 +298,20 @@ QtObject: self.accountListChanged() self.currentAccountChanged() + proc removeCustomToken*(self: WalletView, tokenAddress: string) {.slot.} = + let t = getTokenByAddress(getCustomTokens(), tokenAddress) + if t.kind == JNull: return + self.status.wallet.hideAsset(t["symbol"].getStr) + removeCustomToken(tokenAddress) + self.customTokenList.loadCustomTokens() + for account in self.status.wallet.accounts: + if account.address == self.currentAccount.address: + self.currentAccount.setAccountItem(account) + else: + self.accounts.updateAssetsInList(account.address, account.assetList) + self.accountListChanged() + self.currentAccountChanged() + proc updateView*(self: WalletView) = self.totalFiatBalanceChanged() self.currentAccountChanged() @@ -456,11 +473,21 @@ QtObject: result = $status_wallet.getWalletAccounts()[0].address proc getDefaultTokenList(self: WalletView): QVariant {.slot.} = - self.defaultTokenList.setupTokens() + self.defaultTokenList.loadDefaultTokens() result = newQVariant(self.defaultTokenList) QtProperty[QVariant] defaultTokenList: read = getDefaultTokenList + + proc loadCustomTokens(self: WalletView) {.slot.} = + self.customTokenList.loadCustomTokens() + + proc getCustomTokenList(self: WalletView): QVariant {.slot.} = + result = newQVariant(self.customTokenList) + + QtProperty[QVariant] customTokenList: + read = getCustomTokenList + proc historyWasFetched*(self: WalletView) {.signal.} proc setHistoryFetchState*(self: WalletView, accounts: seq[string], isFetching: bool) = diff --git a/src/app/wallet/views/token_list.nim b/src/app/wallet/views/token_list.nim index f9e2fb0f5a..a456f67bd0 100644 --- a/src/app/wallet/views/token_list.nim +++ b/src/app/wallet/views/token_list.nim @@ -1,11 +1,13 @@ import NimQml, tables, json import ../../../status/libstatus/default_tokens +import ../../../status/libstatus/tokens type TokenRoles {.pure.} = enum Name = UserRole + 1, Symbol = UserRole + 2, - HasIcon = UserRole + 3 + HasIcon = UserRole + 3, + Address = UserRole + 4 QtObject: type TokenList* = ref object of QAbstractListModel @@ -18,10 +20,16 @@ QtObject: self.tokens = @[] self.QAbstractListModel.delete - proc setupTokens*(self:TokenList) = + proc loadDefaultTokens*(self:TokenList) = if self.tokens.len == 0: self.tokens = getDefaultTokens().getElems() + proc loadCustomTokens*(self: TokenList) = + self.beginResetModel() + self.tokens = getCustomTokens().getElems() + echo $self.tokens + self.endResetModel() + proc newTokenList*(): TokenList = new(result, delete) result.tokens = @[] @@ -40,13 +48,12 @@ QtObject: case tokenRole: of TokenRoles.Name: result = newQVariant(token["name"].getStr) of TokenRoles.Symbol: result = newQVariant(token["symbol"].getStr) - of TokenRoles.HasIcon: result = newQVariant(token["hasIcon"].getBool) + of TokenRoles.HasIcon: result = newQVariant(token{"hasIcon"}.getBool) + of TokenRoles.Address: result = newQVariant(token["address"].getStr) method roleNames(self: TokenList): Table[int, string] = {TokenRoles.Name.int:"name", TokenRoles.Symbol.int:"symbol", - TokenRoles.HasIcon.int:"hasIcon"}.toTable + TokenRoles.HasIcon.int:"hasIcon", + TokenRoles.Address.int:"address"}.toTable - proc forceUpdate*(self: TokenList) = - self.beginResetModel() - self.endResetModel() diff --git a/src/status/libstatus/default_tokens.nim b/src/status/libstatus/default_tokens.nim index ee2cef8d54..87ce4d6556 100644 --- a/src/status/libstatus/default_tokens.nim +++ b/src/status/libstatus/default_tokens.nim @@ -1152,15 +1152,3 @@ proc getDefaultTokens*(): JsonNode = "hasIcon": true }) -proc getTokenBySymbol*(symbol: string): JsonNode = - for defToken in getDefaultTokens().getElems(): - if defToken["symbol"].getStr == symbol: - return defToken - return newJNull() - -proc getTokenByAddress*(address: string): JsonNode = - for defToken in getDefaultTokens().getElems(): - if defToken["address"].getStr == address: - return defToken - - return newJNull() \ No newline at end of file diff --git a/src/status/libstatus/tokens.nim b/src/status/libstatus/tokens.nim index 3bc658f5c8..e8d0303436 100644 --- a/src/status/libstatus/tokens.nim +++ b/src/status/libstatus/tokens.nim @@ -7,22 +7,45 @@ import settings from types import Setting, Network import default_tokens import strutils +import locks logScope: topics = "wallet" -proc getCustomTokens*(): JsonNode = - let payload = %* [] - let response = callPrivateRPC("wallet_getCustomTokens", payload).parseJson - if response["result"].kind == JNull: - return %* [] - return response["result"] +var customTokensLock: Lock +initLock(customTokensLock) + +var customTokens {.guard: customTokensLock.} = %*{} +var dirty {.guard: customTokensLock.} = true + +proc getCustomTokens*(useCached: bool = true): JsonNode = + {.gcsafe.}: + withLock customTokensLock: + if useCached and not dirty: + result = customTokens + else: + let payload = %* [] + result = callPrivateRPC("wallet_getCustomTokens", payload).parseJSON()["result"] + if result.kind == JNull: result = %* [] + dirty = false + customTokens = result + +proc getTokenBySymbol*(tokenList: JsonNode, symbol: string): JsonNode = + for defToken in tokenList.getElems(): + if defToken["symbol"].getStr == symbol: + return defToken + return newJNull() + +proc getTokenByAddress*(tokenList: JsonNode, address: string): JsonNode = + for defToken in tokenList.getElems(): + if defToken["address"].getStr == address: + return defToken + return newJNull() proc visibleTokensSNTDefault(): JsonNode = let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork) let SNT = if getCurrentNetwork() == Network.Testnet: "STT" else: "SNT" let response = getSetting[string](Setting.VisibleTokens, "{\"" & currentNetwork & "\": [\"" & SNT & "\"]}") - echo response result = response.parseJson proc toggleAsset*(symbol: string) = @@ -38,6 +61,17 @@ proc toggleAsset*(symbol: string) = visibleTokens[currentNetwork] = %* visibleTokenList discard saveSetting(Setting.VisibleTokens, $visibleTokens) +proc hideAsset*(symbol: string) = + let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork) + let visibleTokens = visibleTokensSNTDefault() + var visibleTokenList = visibleTokens[currentNetwork].to(seq[string]) + var symbolIdx = visibleTokenList.find(symbol) + if symbolIdx > -1: + visibleTokenList.del(symbolIdx) + visibleTokens[currentNetwork] = newJArray() + visibleTokens[currentNetwork] = %* visibleTokenList + discard saveSetting(Setting.VisibleTokens, $visibleTokens) + proc getVisibleTokens*(): JsonNode = let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork) let visibleTokens = visibleTokensSNTDefault() @@ -45,24 +79,23 @@ proc getVisibleTokens*(): JsonNode = let customTokens = getCustomTokens() result = newJArray() - for v in visibleTokenList: - let t = getTokenBySymbol(v) + let t = getTokenBySymbol(getDefaultTokens(), v) if t.kind != JNull: result.elems.add(t) - - for custToken in customTokens.getElems(): - for v in visibleTokenList: - if custToken["symbol"].getStr == v: - result.elems.add(custToken) - break - + let ct = getTokenBySymbol(getCustomTokens(), v) + if ct.kind != JNull: result.elems.add(ct) + proc addCustomToken*(address: string, name: string, symbol: string, decimals: int, color: string) = let payload = %* [{"address": address, "name": name, "symbol": symbol, "decimals": decimals, "color": color}] discard callPrivateRPC("wallet_addCustomToken", payload) + withLock customTokensLock: + dirty = true proc removeCustomToken*(address: string) = let payload = %* [address] - discard callPrivateRPC("wallet_deleteCustomToken", payload) + echo callPrivateRPC("wallet_deleteCustomToken", payload) + withLock customTokensLock: + dirty = true proc getTokensBalances*(accounts: openArray[string], tokens: openArray[string]): JsonNode = let payload = %* [accounts, tokens] @@ -80,9 +113,14 @@ proc getTokenBalance*(tokenAddress: string, account: string): string = let response = callPrivateRPC("eth_call", payload) let balance = response.parseJson["result"].getStr - let t = getTokenByAddress(tokenAddress) var decimals = 18 - if t.kind != JNull: decimals = t["decimals"].getInt + let t = getTokenByAddress(getDefaultTokens(), tokenAddress) + let ct = getTokenByAddress(getCustomTokens(), tokenAddress) + if t.kind != JNull: + decimals = t["decimals"].getInt + elif ct.kind != JNull: + decimals = ct["decimals"].getInt + result = $hex2Token(balance, decimals) proc getSNTAddress*(): string = @@ -92,10 +130,3 @@ proc getSNTAddress*(): string = proc getSNTBalance*(account: string): string = let snt = contracts.getContract("snt") result = getTokenBalance("0x" & $snt.address, account) - -proc addOrRemoveToken*(enable: bool, address: string, name: string, symbol: string, decimals: int, color: string): JsonNode = - if enable: - addCustomToken(address, name, symbol, decimals, color) - else: - removeCustomToken(address) - getCustomTokens() diff --git a/src/status/wallet.nim b/src/status/wallet.nim index cbad8efd82..022872e9ef 100644 --- a/src/status/wallet.nim +++ b/src/status/wallet.nim @@ -198,6 +198,7 @@ proc addWatchOnlyAccount*(self: WalletModel, address: string, accountName: strin return self.addNewGeneratedAccount(account, "", accountName, color, constants.WATCH, false) proc hasAsset*(self: WalletModel, account: string, symbol: string): bool = + self.tokens = status_tokens.getVisibleTokens() self.tokens.anyIt(it["symbol"].getStr == symbol) proc changeAccountSettings*(self: WalletModel, address: string, accountName: string, color: string): string = @@ -224,6 +225,14 @@ proc toggleAsset*(self: WalletModel, symbol: string) = updateBalance(account, self.getDefaultCurrency()) self.events.emit("assetChanged", Args()) +proc hideAsset*(self: WalletModel, symbol: string) = + status_tokens.hideAsset(symbol) + self.tokens = status_tokens.getVisibleTokens() + for account in self.accounts: + account.assetList = self.generateAccountConfiguredAssets(account.address) + updateBalance(account, self.getDefaultCurrency()) + self.events.emit("assetChanged", Args()) + proc addCustomToken*(self: WalletModel, symbol: string, enable: bool, address: string, name: string, decimals: int, color: string) = addCustomToken(address, name, symbol, decimals, color) diff --git a/ui/app/AppLayouts/Wallet/AddCustomTokenModal.qml b/ui/app/AppLayouts/Wallet/AddCustomTokenModal.qml index 9b7f49169c..0f28d836a0 100644 --- a/ui/app/AppLayouts/Wallet/AddCustomTokenModal.qml +++ b/ui/app/AppLayouts/Wallet/AddCustomTokenModal.qml @@ -75,6 +75,8 @@ ModalPopup { changeError.open() return } + + walletModel.loadCustomTokens() popup.close(); } } diff --git a/ui/app/AppLayouts/Wallet/TokenSettingsModal.qml b/ui/app/AppLayouts/Wallet/TokenSettingsModal.qml index a34c4c8213..0a6b51c361 100644 --- a/ui/app/AppLayouts/Wallet/TokenSettingsModal.qml +++ b/ui/app/AppLayouts/Wallet/TokenSettingsModal.qml @@ -20,10 +20,7 @@ ModalPopup { //% "Add custom token" label: qsTrId("add-custom-token") anchors.top: parent.top - onClicked: { - popup.close() - addCustomTokenModal.open() - } + onClicked: addCustomTokenModal.open() } } diff --git a/ui/app/AppLayouts/Wallet/WalletHeader.qml b/ui/app/AppLayouts/Wallet/WalletHeader.qml index 67367f84e6..64a6351f0d 100644 --- a/ui/app/AppLayouts/Wallet/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/WalletHeader.qml @@ -158,6 +158,7 @@ Item { icon.source: "../../img/add_remove_token.svg" onTriggered: { tokenSettingsModal.open() + walletModel.loadCustomTokens() } } Action { diff --git a/ui/app/AppLayouts/Wallet/components/TokenSettingsModalContent.qml b/ui/app/AppLayouts/Wallet/components/TokenSettingsModalContent.qml index 583b050ddf..4a99f835fc 100644 --- a/ui/app/AppLayouts/Wallet/components/TokenSettingsModalContent.qml +++ b/ui/app/AppLayouts/Wallet/components/TokenSettingsModalContent.qml @@ -1,5 +1,7 @@ import QtQuick 2.13 import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.13 + import "../../../../imports" import "../../../../shared" import "../../Chat/ContactsColumn" @@ -16,70 +18,139 @@ Item { anchors.top: modalBody.top } - ListView { - id: tokenListView - spacing: 0 + Component { + id: tokenComponent + Item { + id: tokenContainer + anchors.left: parent.left + anchors.leftMargin: Style.current.smallPadding + width: 300 + property bool isVisible: symbol && (searchBox.text == "" || name.toLowerCase().includes(searchBox.text.toLowerCase()) || symbol.toLowerCase().includes(searchBox.text.toLowerCase())) + + visible: isVisible + height: isVisible ? 40 + Style.current.smallPadding : 0 + + Image { + id: assetInfoImage + width: 36 + height: tokenContainer.isVisible !== "" ? 36 : 0 + anchors.top: parent.top + anchors.topMargin: 0 + source: hasIcon ? "../../../img/tokens/" + symbol + ".png" : "../../../img/tokens/0-native.png" + anchors.left: parent.left + anchors.leftMargin: 0 + } + StyledText { + id: assetSymbol + text: symbol + anchors.left: assetInfoImage.right + anchors.leftMargin: Style.current.smallPadding + anchors.top: assetInfoImage.top + anchors.topMargin: 0 + font.pixelSize: 15 + } + StyledText { + id: assetFullTokenName + text: name || "" + anchors.bottom: assetInfoImage.bottom + anchors.bottomMargin: 0 + anchors.left: assetInfoImage.right + anchors.leftMargin: Style.current.smallPadding + color: Style.current.darkGrey + font.pixelSize: 15 + width: 330 + } + CheckBox { + id: assetCheck + checked: walletModel.hasAsset("0x123", symbol) + anchors.left: assetFullTokenName.right + anchors.leftMargin: Style.current.smallPadding + onClicked: walletModel.toggleAsset(symbol) + } + + MouseArea { + acceptedButtons: Qt.RightButton + anchors.fill: parent + onClicked: contextMenu.popup(assetSymbol.x - 100, assetSymbol.y + 25) + PopupMenu { + id: contextMenu + Action { + icon.source: "../../../img/make-admin.svg" + text: qsTr("Token details") + onTriggered: { + console.log("TODO") + } + } + Action { + icon.source: "../../../img/remove-from-group.svg" + icon.color: Style.current.red + text: qsTr("Remove token") + onTriggered: walletModel.removeCustomToken(address) + } + } + } + } + } + + ScrollView { + id: sview clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + contentHeight: tokenList.height + anchors.top: searchBox.bottom anchors.topMargin: Style.current.smallPadding anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - model: walletModel.defaultTokenList - ScrollBar.vertical: ScrollBar { active: true } - delegate: Component { - id: component - Item { - id: tokenContainer - anchors.right: parent.right - anchors.left: parent.left - anchors.leftMargin: Style.current.smallPadding - width: parent.width - property bool isVisible: symbol && (searchBox.text == "" || name.toLowerCase().includes(searchBox.text.toLowerCase()) || symbol.toLowerCase().includes(searchBox.text.toLowerCase())) - visible: isVisible - height: isVisible ? 40 + Style.current.smallPadding : 0 + Item { + id: tokenList + height: childrenRect.height + + Column { + id: customTokens - Image { - id: assetInfoImage - width: 36 - height: tokenContainer.isVisible !== "" ? 36 : 0 - anchors.top: parent.top - anchors.topMargin: 0 - source: hasIcon ? "../../../img/tokens/" + symbol + ".png" : "../../../img/tokens/0-native.png" - anchors.left: parent.left - anchors.leftMargin: 0 - } StyledText { - id: assetSymbol - text: symbol - anchors.left: assetInfoImage.right - anchors.leftMargin: Style.current.smallPadding - anchors.top: assetInfoImage.top - anchors.topMargin: 0 - font.pixelSize: 15 + id: customLbl + text: qsTr("Custom") + font.pixelSize: 13 + color: Style.current.secondaryText + height: 20 } + + Repeater { + model: walletModel.customTokenList + delegate: tokenComponent + anchors.top: customLbl.bottom + anchors.topMargin: Style.current.smallPadding + } + } + + Column { + anchors.top: customTokens.bottom + anchors.topMargin: Style.current.smallPadding + id: defaultTokens + StyledText { - id: assetFullTokenName - text: name || "" - anchors.bottom: assetInfoImage.bottom - anchors.bottomMargin: 0 - anchors.left: assetInfoImage.right - anchors.leftMargin: Style.current.smallPadding - color: Style.current.darkGrey - font.pixelSize: 15 + id: defaultLbl + text: qsTr("Default") + font.pixelSize: 13 + color: Style.current.secondaryText + height: 20 } - CheckBox { - id: assetCheck - checked: walletModel.hasAsset("0x123", symbol) - anchors.right: parent.right - anchors.rightMargin: Style.current.smallPadding - onClicked: walletModel.toggleAsset(symbol) + + Repeater { + model: walletModel.defaultTokenList + delegate: tokenComponent + anchors.top: defaultLbl.bottom + anchors.topMargin: Style.current.smallPadding } } } - highlightFollowsCurrentItem: true } }