feat: list, toggle and remove custom tokens
This commit is contained in:
parent
f3f27a5e59
commit
27abf30fc8
|
@ -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) =
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -75,6 +75,8 @@ ModalPopup {
|
|||
changeError.open()
|
||||
return
|
||||
}
|
||||
|
||||
walletModel.loadCustomTokens()
|
||||
popup.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -158,6 +158,7 @@ Item {
|
|||
icon.source: "../../img/add_remove_token.svg"
|
||||
onTriggered: {
|
||||
tokenSettingsModal.open()
|
||||
walletModel.loadCustomTokens()
|
||||
}
|
||||
}
|
||||
Action {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue