feat: list, toggle and remove custom tokens

This commit is contained in:
Richard Ramos 2020-09-08 11:52:09 -04:00 committed by Iuri Matias
parent f3f27a5e59
commit 27abf30fc8
9 changed files with 231 additions and 98 deletions

View File

@ -17,6 +17,7 @@ QtObject:
focusedAccount: AccountItemView focusedAccount: AccountItemView
currentTransactions: TransactionList currentTransactions: TransactionList
defaultTokenList: TokenList defaultTokenList: TokenList
customTokenList: TokenList
status: Status status: Status
totalFiatBalance: string totalFiatBalance: string
etherscanLink: string etherscanLink: string
@ -35,6 +36,7 @@ QtObject:
self.focusedAccount.delete self.focusedAccount.delete
self.currentTransactions.delete self.currentTransactions.delete
self.defaultTokenList.delete self.defaultTokenList.delete
self.customTokenList.delete
self.QAbstractListModel.delete self.QAbstractListModel.delete
proc setup(self: WalletView) = proc setup(self: WalletView) =
@ -50,6 +52,7 @@ QtObject:
result.currentTransactions = newTransactionList() result.currentTransactions = newTransactionList()
result.currentCollectiblesLists = newCollectiblesList() result.currentCollectiblesLists = newCollectiblesList()
result.defaultTokenList = newTokenList() result.defaultTokenList = newTokenList()
result.customTokenList = newTokenList()
result.totalFiatBalance = "" result.totalFiatBalance = ""
result.etherscanLink = "" result.etherscanLink = ""
result.safeLowGasPrice = "0" result.safeLowGasPrice = "0"
@ -295,6 +298,20 @@ QtObject:
self.accountListChanged() self.accountListChanged()
self.currentAccountChanged() 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) = proc updateView*(self: WalletView) =
self.totalFiatBalanceChanged() self.totalFiatBalanceChanged()
self.currentAccountChanged() self.currentAccountChanged()
@ -456,11 +473,21 @@ QtObject:
result = $status_wallet.getWalletAccounts()[0].address result = $status_wallet.getWalletAccounts()[0].address
proc getDefaultTokenList(self: WalletView): QVariant {.slot.} = proc getDefaultTokenList(self: WalletView): QVariant {.slot.} =
self.defaultTokenList.setupTokens() self.defaultTokenList.loadDefaultTokens()
result = newQVariant(self.defaultTokenList) result = newQVariant(self.defaultTokenList)
QtProperty[QVariant] defaultTokenList: QtProperty[QVariant] defaultTokenList:
read = getDefaultTokenList 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 historyWasFetched*(self: WalletView) {.signal.}
proc setHistoryFetchState*(self: WalletView, accounts: seq[string], isFetching: bool) = proc setHistoryFetchState*(self: WalletView, accounts: seq[string], isFetching: bool) =

View File

@ -1,11 +1,13 @@
import NimQml, tables, json import NimQml, tables, json
import ../../../status/libstatus/default_tokens import ../../../status/libstatus/default_tokens
import ../../../status/libstatus/tokens
type type
TokenRoles {.pure.} = enum TokenRoles {.pure.} = enum
Name = UserRole + 1, Name = UserRole + 1,
Symbol = UserRole + 2, Symbol = UserRole + 2,
HasIcon = UserRole + 3 HasIcon = UserRole + 3,
Address = UserRole + 4
QtObject: QtObject:
type TokenList* = ref object of QAbstractListModel type TokenList* = ref object of QAbstractListModel
@ -18,10 +20,16 @@ QtObject:
self.tokens = @[] self.tokens = @[]
self.QAbstractListModel.delete self.QAbstractListModel.delete
proc setupTokens*(self:TokenList) = proc loadDefaultTokens*(self:TokenList) =
if self.tokens.len == 0: if self.tokens.len == 0:
self.tokens = getDefaultTokens().getElems() self.tokens = getDefaultTokens().getElems()
proc loadCustomTokens*(self: TokenList) =
self.beginResetModel()
self.tokens = getCustomTokens().getElems()
echo $self.tokens
self.endResetModel()
proc newTokenList*(): TokenList = proc newTokenList*(): TokenList =
new(result, delete) new(result, delete)
result.tokens = @[] result.tokens = @[]
@ -40,13 +48,12 @@ QtObject:
case tokenRole: case tokenRole:
of TokenRoles.Name: result = newQVariant(token["name"].getStr) of TokenRoles.Name: result = newQVariant(token["name"].getStr)
of TokenRoles.Symbol: result = newQVariant(token["symbol"].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] = method roleNames(self: TokenList): Table[int, string] =
{TokenRoles.Name.int:"name", {TokenRoles.Name.int:"name",
TokenRoles.Symbol.int:"symbol", 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()

View File

@ -1152,15 +1152,3 @@ proc getDefaultTokens*(): JsonNode =
"hasIcon": true "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()

View File

@ -7,22 +7,45 @@ import settings
from types import Setting, Network from types import Setting, Network
import default_tokens import default_tokens
import strutils import strutils
import locks
logScope: logScope:
topics = "wallet" topics = "wallet"
proc getCustomTokens*(): JsonNode = var customTokensLock: Lock
let payload = %* [] initLock(customTokensLock)
let response = callPrivateRPC("wallet_getCustomTokens", payload).parseJson
if response["result"].kind == JNull: var customTokens {.guard: customTokensLock.} = %*{}
return %* [] var dirty {.guard: customTokensLock.} = true
return response["result"]
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 = proc visibleTokensSNTDefault(): JsonNode =
let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork) let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork)
let SNT = if getCurrentNetwork() == Network.Testnet: "STT" else: "SNT" let SNT = if getCurrentNetwork() == Network.Testnet: "STT" else: "SNT"
let response = getSetting[string](Setting.VisibleTokens, "{\"" & currentNetwork & "\": [\"" & SNT & "\"]}") let response = getSetting[string](Setting.VisibleTokens, "{\"" & currentNetwork & "\": [\"" & SNT & "\"]}")
echo response
result = response.parseJson result = response.parseJson
proc toggleAsset*(symbol: string) = proc toggleAsset*(symbol: string) =
@ -38,6 +61,17 @@ proc toggleAsset*(symbol: string) =
visibleTokens[currentNetwork] = %* visibleTokenList visibleTokens[currentNetwork] = %* visibleTokenList
discard saveSetting(Setting.VisibleTokens, $visibleTokens) 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 = proc getVisibleTokens*(): JsonNode =
let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork) let currentNetwork = getSetting[string](Setting.Networks_CurrentNetwork)
let visibleTokens = visibleTokensSNTDefault() let visibleTokens = visibleTokensSNTDefault()
@ -45,24 +79,23 @@ proc getVisibleTokens*(): JsonNode =
let customTokens = getCustomTokens() let customTokens = getCustomTokens()
result = newJArray() result = newJArray()
for v in visibleTokenList: for v in visibleTokenList:
let t = getTokenBySymbol(v) let t = getTokenBySymbol(getDefaultTokens(), v)
if t.kind != JNull: result.elems.add(t) if t.kind != JNull: result.elems.add(t)
let ct = getTokenBySymbol(getCustomTokens(), v)
for custToken in customTokens.getElems(): if ct.kind != JNull: result.elems.add(ct)
for v in visibleTokenList:
if custToken["symbol"].getStr == v:
result.elems.add(custToken)
break
proc addCustomToken*(address: string, name: string, symbol: string, decimals: int, color: string) = proc addCustomToken*(address: string, name: string, symbol: string, decimals: int, color: string) =
let payload = %* [{"address": address, "name": name, "symbol": symbol, "decimals": decimals, "color": color}] let payload = %* [{"address": address, "name": name, "symbol": symbol, "decimals": decimals, "color": color}]
discard callPrivateRPC("wallet_addCustomToken", payload) discard callPrivateRPC("wallet_addCustomToken", payload)
withLock customTokensLock:
dirty = true
proc removeCustomToken*(address: string) = proc removeCustomToken*(address: string) =
let payload = %* [address] 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 = proc getTokensBalances*(accounts: openArray[string], tokens: openArray[string]): JsonNode =
let payload = %* [accounts, tokens] let payload = %* [accounts, tokens]
@ -80,9 +113,14 @@ proc getTokenBalance*(tokenAddress: string, account: string): string =
let response = callPrivateRPC("eth_call", payload) let response = callPrivateRPC("eth_call", payload)
let balance = response.parseJson["result"].getStr let balance = response.parseJson["result"].getStr
let t = getTokenByAddress(tokenAddress)
var decimals = 18 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) result = $hex2Token(balance, decimals)
proc getSNTAddress*(): string = proc getSNTAddress*(): string =
@ -92,10 +130,3 @@ proc getSNTAddress*(): string =
proc getSNTBalance*(account: string): string = proc getSNTBalance*(account: string): string =
let snt = contracts.getContract("snt") let snt = contracts.getContract("snt")
result = getTokenBalance("0x" & $snt.address, account) 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()

View File

@ -198,6 +198,7 @@ proc addWatchOnlyAccount*(self: WalletModel, address: string, accountName: strin
return self.addNewGeneratedAccount(account, "", accountName, color, constants.WATCH, false) return self.addNewGeneratedAccount(account, "", accountName, color, constants.WATCH, false)
proc hasAsset*(self: WalletModel, account: string, symbol: string): bool = proc hasAsset*(self: WalletModel, account: string, symbol: string): bool =
self.tokens = status_tokens.getVisibleTokens()
self.tokens.anyIt(it["symbol"].getStr == symbol) self.tokens.anyIt(it["symbol"].getStr == symbol)
proc changeAccountSettings*(self: WalletModel, address: string, accountName: string, color: string): string = 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()) updateBalance(account, self.getDefaultCurrency())
self.events.emit("assetChanged", Args()) 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) = proc addCustomToken*(self: WalletModel, symbol: string, enable: bool, address: string, name: string, decimals: int, color: string) =
addCustomToken(address, name, symbol, decimals, color) addCustomToken(address, name, symbol, decimals, color)

View File

@ -75,6 +75,8 @@ ModalPopup {
changeError.open() changeError.open()
return return
} }
walletModel.loadCustomTokens()
popup.close(); popup.close();
} }
} }

View File

@ -20,10 +20,7 @@ ModalPopup {
//% "Add custom token" //% "Add custom token"
label: qsTrId("add-custom-token") label: qsTrId("add-custom-token")
anchors.top: parent.top anchors.top: parent.top
onClicked: { onClicked: addCustomTokenModal.open()
popup.close()
addCustomTokenModal.open()
}
} }
} }

View File

@ -158,6 +158,7 @@ Item {
icon.source: "../../img/add_remove_token.svg" icon.source: "../../img/add_remove_token.svg"
onTriggered: { onTriggered: {
tokenSettingsModal.open() tokenSettingsModal.open()
walletModel.loadCustomTokens()
} }
} }
Action { Action {

View File

@ -1,5 +1,7 @@
import QtQuick 2.13 import QtQuick 2.13
import QtQuick.Controls 2.13 import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import "../../../../imports" import "../../../../imports"
import "../../../../shared" import "../../../../shared"
import "../../Chat/ContactsColumn" import "../../Chat/ContactsColumn"
@ -16,70 +18,139 @@ Item {
anchors.top: modalBody.top anchors.top: modalBody.top
} }
ListView { Component {
id: tokenListView id: tokenComponent
spacing: 0 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 clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
contentHeight: tokenList.height
anchors.top: searchBox.bottom anchors.top: searchBox.bottom
anchors.topMargin: Style.current.smallPadding anchors.topMargin: Style.current.smallPadding
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom 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 Item {
height: isVisible ? 40 + Style.current.smallPadding : 0 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 { StyledText {
id: assetSymbol id: customLbl
text: symbol text: qsTr("Custom")
anchors.left: assetInfoImage.right font.pixelSize: 13
anchors.leftMargin: Style.current.smallPadding color: Style.current.secondaryText
anchors.top: assetInfoImage.top height: 20
anchors.topMargin: 0
font.pixelSize: 15
} }
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 { StyledText {
id: assetFullTokenName id: defaultLbl
text: name || "" text: qsTr("Default")
anchors.bottom: assetInfoImage.bottom font.pixelSize: 13
anchors.bottomMargin: 0 color: Style.current.secondaryText
anchors.left: assetInfoImage.right height: 20
anchors.leftMargin: Style.current.smallPadding
color: Style.current.darkGrey
font.pixelSize: 15
} }
CheckBox {
id: assetCheck Repeater {
checked: walletModel.hasAsset("0x123", symbol) model: walletModel.defaultTokenList
anchors.right: parent.right delegate: tokenComponent
anchors.rightMargin: Style.current.smallPadding anchors.top: defaultLbl.bottom
onClicked: walletModel.toggleAsset(symbol) anchors.topMargin: Style.current.smallPadding
} }
} }
} }
highlightFollowsCurrentItem: true
} }
} }