feat: Add Sticker purchase transaction modal
Add gas estimate for sticker pack purchase. Update transaction for sticker pack purchase. Add GasValidator component which validates gas is selected correctly and displays an error message if not. This component is not visible until it is not valid (at which point the valdiation error message is displayed). In a future PR, need to: 1. estimate gas for token txfer (sendTransaction) via a normalised method for estimating gas for EthSend 2. move sticker pack purchase to use an EthSend object so gas can be estimated and tx sent
This commit is contained in:
parent
75cb28b24d
commit
d8b0145eb3
|
@ -4,7 +4,6 @@ import ../../status/mailservers as mailserver_model
|
|||
import ../../status/messages as messages_model
|
||||
import ../../signals/types
|
||||
import ../../status/libstatus/types as status_types
|
||||
import ../../status/libstatus/wallet as status_wallet
|
||||
import ../../status/libstatus/settings as status_settings
|
||||
import ../../status/[chat, contacts, status]
|
||||
import view, views/channels_list, views/message_list
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import NimQml, Tables, json, sequtils, chronicles, times, re, sugar, strutils, os
|
||||
import NimQml, Tables, json, sequtils, chronicles, times, re, sugar, strutils, os, strformat
|
||||
import ../../status/status
|
||||
import ../../status/libstatus/accounts/constants
|
||||
import ../../status/accounts as status_accounts
|
||||
|
@ -71,6 +71,24 @@ QtObject:
|
|||
QtProperty[QVariant] stickerPacks:
|
||||
read = getStickerPackList
|
||||
|
||||
proc getStickerMarketAddress(self: ChatsView): QVariant {.slot.} =
|
||||
newQVariant($self.status.chat.getStickerMarketAddress)
|
||||
|
||||
QtProperty[QVariant] stickerMarketAddress:
|
||||
read = getStickerMarketAddress
|
||||
|
||||
proc getStickerBuyPackGasEstimate*(self: ChatsView, packId: int, address: string, price: string): string {.slot.} =
|
||||
try:
|
||||
result = self.status.chat.buyPackGasEstimate(packId, address, price)
|
||||
except:
|
||||
result = "400000"
|
||||
|
||||
proc buyStickerPack*(self: ChatsView, packId: int, address: string, price: string, gas: string, gasPrice: string, password: string): string {.slot.} =
|
||||
try:
|
||||
result = $(%self.status.chat.buyStickerPack(packId, address, price, gas, gasPrice, password))
|
||||
except RpcException as e:
|
||||
result = fmt"""{{ "error": {{ "message": "{e.msg}" }} }}"""
|
||||
|
||||
proc obtainAvailableStickerPacks*(self: ChatsView) =
|
||||
spawnAndSend(self, "setAvailableStickerPacks") do:
|
||||
let availableStickerPacks = status_chat.getAvailableStickerPacks()
|
||||
|
|
|
@ -82,6 +82,8 @@ QtObject:
|
|||
read = getSigningPhrase
|
||||
notify = signingPhraseChanged
|
||||
|
||||
proc getStatusTokenSymbol*(self: WalletView): string {.slot.} = self.status.wallet.getStatusTokenSymbol
|
||||
|
||||
proc setCurrentAssetList*(self: WalletView, assetList: seq[Asset])
|
||||
|
||||
proc currentCollectiblesListsChanged*(self: WalletView) {.signal.}
|
||||
|
@ -182,6 +184,7 @@ QtObject:
|
|||
self.accountListChanged()
|
||||
|
||||
proc getFiatValue*(self: WalletView, cryptoBalance: string, cryptoSymbol: string, fiatSymbol: string): string {.slot.} =
|
||||
if (cryptoBalance == "" or cryptoSymbol == "" or fiatSymbol == ""): return "0.00"
|
||||
let val = self.status.wallet.convertValue(cryptoBalance, cryptoSymbol, fiatSymbol)
|
||||
result = fmt"{val:.2f}"
|
||||
|
||||
|
@ -236,13 +239,10 @@ QtObject:
|
|||
notify = accountListChanged
|
||||
|
||||
proc sendTransaction*(self: WalletView, from_addr: string, to: string, assetAddress: string, value: string, gas: string, gasPrice: string, password: string): string {.slot.} =
|
||||
let resultJson = %*{}
|
||||
try:
|
||||
resultJson{"result"} = %self.status.wallet.sendTransaction(from_addr, to, assetAddress, value, gas, gasPrice, password)
|
||||
except StatusGoException as e:
|
||||
resultJson{"error"} = %e.msg
|
||||
finally:
|
||||
result = $resultJson
|
||||
result = $(%self.status.wallet.sendTransaction(from_addr, to, assetAddress, value, gas, gasPrice, password))
|
||||
except RpcException as e:
|
||||
result = fmt"""{{ "error": {{ "message": "{e.msg}" }} }}"""
|
||||
|
||||
proc getDefaultAccount*(self: WalletView): string {.slot.} =
|
||||
self.currentAccount.address
|
||||
|
|
|
@ -8,13 +8,6 @@ type SignalSubscriber* = ref object of RootObj
|
|||
type Signal* = ref object of RootObj
|
||||
signalType* {.serializedFieldName("type").}: SignalType
|
||||
|
||||
type StatusGoErrorDetail* = object
|
||||
message*: string
|
||||
code*: int
|
||||
|
||||
type StatusGoErrorExtended* = object
|
||||
error*: StatusGoErrorDetail
|
||||
|
||||
type StatusGoError* = object
|
||||
error*: string
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import eventemitter, json, strutils, sequtils, tables, chronicles, sugar
|
||||
import libstatus/settings as status_settings
|
||||
import libstatus/contracts as status_contracts
|
||||
import libstatus/chat as status_chat
|
||||
import libstatus/mailservers as status_mailservers
|
||||
import libstatus/stickers as status_stickers
|
||||
|
@ -10,6 +10,8 @@ import chat/[chat, message]
|
|||
import ../signals/messages
|
||||
import ens
|
||||
import eth/common/eth_types
|
||||
from eth/common/utils import parseAddress
|
||||
from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, toUInt64
|
||||
|
||||
logScope:
|
||||
topics = "chat-model"
|
||||
|
@ -112,17 +114,32 @@ proc join*(self: ChatModel, chatId: string, chatType: ChatType) =
|
|||
|
||||
self.events.emit("channelJoined", ChannelArgs(chat: chat))
|
||||
|
||||
# TODO: Replace this with a more generalised way of estimating gas so can be used for token transfers
|
||||
proc buyPackGasEstimate*(self: ChatModel, packId: int, address: string, price: string): string =
|
||||
let
|
||||
priceTyped = eth2Wei(parseFloat(price), 18) # SNT
|
||||
hexGas = status_stickers.buyPackGasEstimate(packId.u256, parseAddress(address), priceTyped)
|
||||
result = $fromHex[int](hexGas)
|
||||
|
||||
proc getStickerMarketAddress*(self: ChatModel): EthAddress =
|
||||
result = status_contracts.getContract("sticker-market").address
|
||||
|
||||
proc buyStickerPack*(self: ChatModel, packId: int, address, price, gas, gasPrice, password: string): RpcResponse =
|
||||
try:
|
||||
let
|
||||
addressTyped = parseAddress(address)
|
||||
priceTyped = eth2Wei(parseFloat(price), 18) # SNT
|
||||
gasTyped = cast[uint64](parseFloat(gas).toUInt64)
|
||||
gasPriceTyped = gwei2Wei(parseFloat(gasPrice)).truncate(int)
|
||||
result = status_stickers.buyPack(packId.u256, addressTyped, priceTyped, gasTyped, gasPriceTyped, password)
|
||||
except RpcException as e:
|
||||
raise
|
||||
|
||||
proc getPurchasedStickerPacks*(self: ChatModel, address: EthAddress): seq[int] =
|
||||
if self.purchasedStickerPacks != @[]:
|
||||
return self.purchasedStickerPacks
|
||||
|
||||
try:
|
||||
# Buy the "Toozeman" sticker pack on testnet
|
||||
# Ensure there is enough STT and ETHro in the account first before uncommenting.
|
||||
# STT faucet: simpledapp.eth
|
||||
# NOTE: don't forget to update your account password!
|
||||
# if status_settings.getCurrentNetwork() == Network.Testnet:
|
||||
# debugEcho ">>> [getPurchasedStickerPacks] buy Toozeman sticker pack, response/txid: ", status_stickers.buyPack(1.u256, address, "20000000000000000000".u256, "<your password here>")
|
||||
var
|
||||
balance = status_stickers.getBalance(address)
|
||||
tokenIds = toSeq[0..<balance].map(idx => status_stickers.tokenOfOwnerByIndex(address, idx.u256))
|
||||
|
|
|
@ -124,10 +124,7 @@ proc getPackData*(id: Stuint[256]): StickerPack =
|
|||
result.id = truncate(id, int)
|
||||
result.price = packData.price
|
||||
|
||||
# Buys a sticker pack for user
|
||||
# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Buy-a-Sticker-Pack for more
|
||||
# details
|
||||
proc buyPack*(packId: Stuint[256], address: EthAddress, price: Stuint[256], password: string): string =
|
||||
proc buyPackPayload(packId: Stuint[256], address: EthAddress, price: Stuint[256]): JsonNode =
|
||||
let
|
||||
stickerMktContract = contracts.getContract("sticker-market")
|
||||
sntContract = contracts.getContract("snt")
|
||||
|
@ -136,19 +133,34 @@ proc buyPack*(packId: Stuint[256], address: EthAddress, price: Stuint[256], pass
|
|||
let
|
||||
approveAndCallObj = ApproveAndCall(to: stickerMktContract.address, value: price, data: DynamicBytes[100].fromHex(buyTxAbiEncoded))
|
||||
approveAndCallAbiEncoded = sntContract.methods["approveAndCall"].encodeAbi(approveAndCallObj)
|
||||
let payload = %* {
|
||||
"from": $address,
|
||||
"to": $sntContract.address,
|
||||
# "gas": 200000, # leave out for now
|
||||
"data": approveAndCallAbiEncoded
|
||||
}
|
||||
|
||||
let responseStr = status.sendTransaction($payload, password)
|
||||
result = %* {
|
||||
"from": $address,
|
||||
"to": $sntContract.address,
|
||||
"data": approveAndCallAbiEncoded
|
||||
}
|
||||
|
||||
proc buyPackGasEstimate*(packId: Stuint[256], address: EthAddress, price: Stuint[256]): string =
|
||||
# TODO: pass in an EthSend object instead
|
||||
let payload = buyPackPayload(packId, address, price)
|
||||
let responseStr = status.callPrivateRPC("eth_estimateGas", %*[payload])
|
||||
let response = Json.decode(responseStr, RpcResponse)
|
||||
if not response.error.isNil:
|
||||
raise newException(RpcException, "Error getting stickers balance: " & response.error.message)
|
||||
raise newException(RpcException, "Error getting stickers buy pack gas estimate: " & response.error.message)
|
||||
result = response.result # should be a tx receipt
|
||||
|
||||
# Buys a sticker pack for user
|
||||
# See https://notes.status.im/Q-sQmQbpTOOWCQcYiXtf5g#Buy-a-Sticker-Pack for more
|
||||
# details
|
||||
proc buyPack*(packId: Stuint[256], address: EthAddress, price: Stuint[256], gas: uint64, gasPrice: int, password: string): RpcResponse =
|
||||
# TODO: pass in an EthSend object instead
|
||||
let payload = buyPackPayload(packId, address, price)
|
||||
payload{"gas"} = %gas.encodeQuantity
|
||||
payload{"gasPrice"} = %("0x" & gasPrice.toHex.stripLeadingZeros)
|
||||
let responseStr = status.sendTransaction($payload, password)
|
||||
result = Json.decode(responseStr, RpcResponse)
|
||||
if not result.error.isNil:
|
||||
raise newException(RpcException, "Error buying sticker pack: " & result.error.message)
|
||||
|
||||
proc tokenOfOwnerByIndex*(address: EthAddress, idx: Stuint[256]): int =
|
||||
let
|
||||
contract = contracts.getContract("sticker-pack")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import json, chronicles, strformat, stint, strutils
|
||||
import core, wallet
|
||||
import contracts
|
||||
import eth/common/eth_types, eth/common/utils, stew/byteutils
|
||||
import eth/common/eth_types, eth/common/utils
|
||||
import json_serialization
|
||||
import settings
|
||||
from types import Setting, Network
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import eventemitter, json, options, typetraits, strutils
|
||||
import eth/common/eth_types, stew/byteutils, json_serialization, stint, faststreams/textio
|
||||
import eth/common/eth_types, stew/byteutils, json_serialization, stint
|
||||
import accounts/constants
|
||||
|
||||
type SignalType* {.pure.} = enum
|
||||
|
@ -190,7 +190,7 @@ type
|
|||
nonce*: Option[int] # (optional) integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce
|
||||
|
||||
# TODO: Remove this when nim-web3 is added as a dependency
|
||||
template stripLeadingZeros(value: string): string =
|
||||
template stripLeadingZeros*(value: string): string =
|
||||
var cidx = 0
|
||||
# ignore the last character so we retain '0' on zero value
|
||||
while cidx < value.len - 1 and value[cidx] == '0':
|
||||
|
|
|
@ -5,7 +5,6 @@ import ../wallet/account
|
|||
import ./contracts as contractMethods
|
||||
import eth/common/eth_types
|
||||
import ./types
|
||||
import ../../signals/types as signal_types
|
||||
|
||||
proc getWalletAccounts*(): seq[WalletAccount] =
|
||||
try:
|
||||
|
@ -62,15 +61,11 @@ proc getTransfersByAddress*(address: string): seq[types.Transaction] =
|
|||
let msg = getCurrentExceptionMsg()
|
||||
error "Failed getting wallet account transactions", msg
|
||||
|
||||
proc sendTransaction*(tx: EthSend, password: string): string =
|
||||
let response = core.sendTransaction($(%tx), password)
|
||||
|
||||
try:
|
||||
let parsedResponse = parseJson(response)
|
||||
result = parsedResponse["result"].getStr
|
||||
except:
|
||||
let err = Json.decode(response, StatusGoErrorExtended)
|
||||
raise newException(StatusGoException, "Error sending transaction: " & err.error.message)
|
||||
proc sendTransaction*(tx: EthSend, password: string): RpcResponse =
|
||||
let responseStr = core.sendTransaction($(%tx), password)
|
||||
result = Json.decode(responseStr, RpcResponse)
|
||||
if not result.error.isNil:
|
||||
raise newException(RpcException, "Error sending transaction: " & result.error.message)
|
||||
|
||||
trace "Transaction sent succesfully", hash=result
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import libstatus/settings as status_settings
|
|||
import libstatus/wallet as status_wallet
|
||||
import libstatus/accounts/constants as constants
|
||||
import libstatus/contracts as contracts
|
||||
from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction, EthSend, Quantity, `%`, StatusGoException
|
||||
from libstatus/types import GeneratedAccount, DerivedAccount, Transaction, Setting, GasPricePrediction, EthSend, Quantity, `%`, StatusGoException, Network, RpcResponse, RpcException
|
||||
from libstatus/utils as libstatus_utils import eth2Wei, gwei2Wei, first, toUInt64
|
||||
import wallet/balance_manager
|
||||
import wallet/account
|
||||
|
@ -50,7 +50,7 @@ proc initEvents*(self: WalletModel) =
|
|||
proc delete*(self: WalletModel) =
|
||||
discard
|
||||
|
||||
proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, gasPrice, password: string): string =
|
||||
proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, gasPrice, password: string): RpcResponse =
|
||||
var
|
||||
weiValue = eth2Wei(parseFloat(value), 18) # ETH
|
||||
data = ""
|
||||
|
@ -63,8 +63,8 @@ proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, g
|
|||
let
|
||||
token = self.tokens.first("address", assetAddress)
|
||||
contract = getContract("snt")
|
||||
transfer = Transfer(to: toAddr, value: weiValue)
|
||||
weiValue = eth2Wei(parseFloat(value), token["decimals"].getInt)
|
||||
transfer = Transfer(to: toAddr, value: eth2Wei(parseFloat(value), token["decimals"].getInt))
|
||||
weiValue = 0.u256
|
||||
data = contract.methods["transfer"].encodeAbi(transfer)
|
||||
toAddr = parseAddress(assetAddress)
|
||||
|
||||
|
@ -78,7 +78,7 @@ proc sendTransaction*(self: WalletModel, source, to, assetAddress, value, gas, g
|
|||
)
|
||||
try:
|
||||
result = status_wallet.sendTransaction(tx, password)
|
||||
except StatusGoException as e:
|
||||
except RpcException as e:
|
||||
raise
|
||||
|
||||
proc getDefaultCurrency*(self: WalletModel): string =
|
||||
|
@ -86,6 +86,13 @@ proc getDefaultCurrency*(self: WalletModel): string =
|
|||
# profile section and ideally we should not call the settings more than once
|
||||
status_settings.getSetting[string](Setting.Currency, "usd")
|
||||
|
||||
# TODO: This needs to be removed or refactored so that test tokens are shown
|
||||
# when on testnet https://github.com/status-im/nim-status-client/issues/613.
|
||||
proc getStatusTokenSymbol*(self: WalletModel): string =
|
||||
if status_settings.getCurrentNetwork() == Network.Testnet:
|
||||
return "STT"
|
||||
"SNT"
|
||||
|
||||
proc setDefaultCurrency*(self: WalletModel, currency: string) =
|
||||
discard status_settings.saveSetting(Setting.Currency, currency)
|
||||
self.events.emit("currencyChanged", CurrencyArgs(currency: currency))
|
||||
|
@ -221,6 +228,8 @@ proc validateMnemonic*(self: WalletModel, mnemonic: string): string =
|
|||
result = status_wallet.validateMnemonic(mnemonic).parseJSON()["error"].getStr
|
||||
|
||||
proc getGasPricePredictions*(self: WalletModel): GasPricePrediction =
|
||||
if status_settings.getCurrentNetwork() == Network.Testnet:
|
||||
return GasPricePrediction(safeLow: "1.0", standard: "2.0", fast: "3.0", fastest: "4.0")
|
||||
try:
|
||||
let url: string = fmt"https://etherchain.org/api/gasPriceOracle"
|
||||
let client = newHttpClient()
|
||||
|
|
|
@ -16,6 +16,10 @@ Item {
|
|||
signal updateClicked(int packId)
|
||||
signal buyClicked(int packId)
|
||||
|
||||
Component.onCompleted: {
|
||||
walletModel.getGasPricePredictions()
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: availableStickerPacks
|
||||
width: parent.width
|
||||
|
@ -73,7 +77,10 @@ Item {
|
|||
onUninstallClicked: root.uninstallClicked(packId)
|
||||
onCancelClicked: root.cancelClicked(packId)
|
||||
onUpdateClicked: root.updateClicked(packId)
|
||||
onBuyClicked: root.buyClicked(packId)
|
||||
onBuyClicked: {
|
||||
stickerPackPurchaseModal.open()
|
||||
root.buyClicked(packId)
|
||||
}
|
||||
}
|
||||
contentWrapper.anchors.topMargin: 0
|
||||
contentWrapper.anchors.bottomMargin: 0
|
||||
|
@ -83,6 +90,14 @@ Item {
|
|||
height: 350
|
||||
}
|
||||
}
|
||||
StickerPackPurchaseModal {
|
||||
id: stickerPackPurchaseModal
|
||||
stickerPackId: packId
|
||||
packPrice: price
|
||||
width: stickerPackDetailsPopup.width
|
||||
height: stickerPackDetailsPopup.height
|
||||
showBackBtn: stickerPackDetailsPopup.opened
|
||||
}
|
||||
StickerPackDetails {
|
||||
id: stickerPackDetails
|
||||
height: 64 - (Style.current.smallPadding * 2)
|
||||
|
@ -106,9 +121,11 @@ Item {
|
|||
onUninstallClicked: root.uninstallClicked(packId)
|
||||
onCancelClicked: root.cancelClicked(packId)
|
||||
onUpdateClicked: root.updateClicked(packId)
|
||||
onBuyClicked: root.buyClicked(packId)
|
||||
onBuyClicked: {
|
||||
stickerPackPurchaseModal.open()
|
||||
root.buyClicked(packId)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import QtQuick.Dialogs 1.3
|
||||
import "../../../../imports"
|
||||
import "../../../../shared"
|
||||
|
||||
ModalPopup {
|
||||
id: root
|
||||
property var asset: { "name": "Status", "symbol": walletModel.getStatusTokenSymbol() }
|
||||
property int stickerPackId: -1
|
||||
property string packPrice
|
||||
property bool showBackBtn: false
|
||||
title: qsTr("Authorize %1 %2").arg(Utils.stripTrailingZeros(packPrice)).arg(asset.symbol)
|
||||
|
||||
property MessageDialog sendingError: MessageDialog {
|
||||
id: sendingError
|
||||
title: qsTr("Error sending the transaction")
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
property MessageDialog sendingSuccess: MessageDialog {
|
||||
id: sendingSuccess
|
||||
//% "Success sending the transaction"
|
||||
title: qsTrId("success-sending-the-transaction")
|
||||
icon: StandardIcon.NoIcon
|
||||
standardButtons: StandardButton.Ok
|
||||
onAccepted: {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
stack.reset()
|
||||
}
|
||||
|
||||
function sendTransaction() {
|
||||
let responseStr = chatsModel.buyStickerPack(root.stickerPackId,
|
||||
selectFromAccount.selectedAccount.address,
|
||||
root.packPrice,
|
||||
gasSelector.selectedGasLimit,
|
||||
gasSelector.selectedGasPrice,
|
||||
transactionSigner.enteredPassword)
|
||||
let response = JSON.parse(responseStr)
|
||||
|
||||
if (response.error) {
|
||||
if (response.error.message.includes("could not decrypt key with given password")){
|
||||
transactionSigner.validationError = qsTr("Wrong password")
|
||||
return
|
||||
}
|
||||
sendingError.text = response.error.message
|
||||
return sendingError.open()
|
||||
}
|
||||
|
||||
sendingSuccess.text = qsTr("Transaction sent to the blockchain. You can watch the progress on Etherscan: %2/%1").arg(response.result).arg(walletModel.etherscanLink)
|
||||
sendingSuccess.open()
|
||||
}
|
||||
|
||||
TransactionStackView {
|
||||
id: stack
|
||||
height: parent.height
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.current.padding
|
||||
anchors.rightMargin: Style.current.padding
|
||||
onGroupActivated: {
|
||||
root.title = group.headerText
|
||||
btnNext.label = group.footerText
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group1
|
||||
headerText: qsTr("Authorize %1 %2").arg(Utils.stripTrailingZeros(root.packPrice)).arg(root.asset.symbol)
|
||||
footerText: qsTr("Continue")
|
||||
|
||||
StackView.onActivated: {
|
||||
btnBack.visible = root.showBackBtn
|
||||
}
|
||||
|
||||
AccountSelector {
|
||||
id: selectFromAccount
|
||||
accounts: walletModel.accounts
|
||||
selectedAccount: walletModel.currentAccount
|
||||
currency: walletModel.defaultCurrency
|
||||
width: stack.width
|
||||
label: qsTr("Choose account")
|
||||
showBalanceForAssetSymbol: root.asset.symbol
|
||||
minRequiredAssetBalance: root.packPrice
|
||||
reset: function() {
|
||||
accounts = Qt.binding(function() { return walletModel.accounts })
|
||||
selectedAccount = Qt.binding(function() { return walletModel.currentAccount })
|
||||
showBalanceForAssetSymbol = Qt.binding(function() { return root.asset.symbol })
|
||||
minRequiredAssetBalance = Qt.binding(function() { return root.packPrice })
|
||||
}
|
||||
}
|
||||
RecipientSelector {
|
||||
id: selectRecipient
|
||||
visible: false
|
||||
accounts: walletModel.accounts
|
||||
contacts: profileModel.addedContacts
|
||||
selectedRecipient: { "address": chatsModel.stickerMarketAddress, "type": RecipientSelector.Type.Address }
|
||||
readOnly: true
|
||||
}
|
||||
GasSelector {
|
||||
id: gasSelector
|
||||
visible: false
|
||||
slowestGasPrice: parseFloat(walletModel.safeLowGasPrice)
|
||||
fastestGasPrice: parseFloat(walletModel.fastestGasPrice)
|
||||
getGasEthValue: walletModel.getGasEthValue
|
||||
getFiatValue: walletModel.getFiatValue
|
||||
defaultCurrency: walletModel.defaultCurrency
|
||||
selectedGasLimit: { return getDefaultGasLimit() }
|
||||
reset: function() {
|
||||
slowestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.safeLowGasPrice) })
|
||||
fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) })
|
||||
selectedGasLimit = Qt.binding(getDefaultGasLimit)
|
||||
}
|
||||
|
||||
function getDefaultGasLimit() {
|
||||
if (root.stickerPackId > -1 && selectFromAccount.selectedAccount && root.packPrice && parseFloat(root.packPrice) > 0) {
|
||||
return chatsModel.getStickerBuyPackGasEstimate(root.stickerPackId, selectFromAccount.selectedAccount.address, root.packPrice)
|
||||
}
|
||||
return 200000
|
||||
}
|
||||
}
|
||||
GasValidator {
|
||||
id: gasValidator
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 8
|
||||
selectedAccount: selectFromAccount.selectedAccount
|
||||
selectedAsset: root.asset
|
||||
selectedAmount: parseFloat(packPrice)
|
||||
selectedGasEthValue: gasSelector.selectedGasEthValue
|
||||
reset: function() {
|
||||
selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount })
|
||||
selectedAsset = Qt.binding(function() { return root.asset })
|
||||
selectedAmount = Qt.binding(function() { return parseFloat(packPrice) })
|
||||
selectedGasEthValue = Qt.binding(function() { return gasSelector.selectedGasEthValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group3
|
||||
headerText: qsTr("Authorize %1 %2").arg(Utils.stripTrailingZeros(root.packPrice)).arg(root.asset.symbol)
|
||||
footerText: qsTr("Sign with password")
|
||||
|
||||
StackView.onActivated: {
|
||||
btnBack.visible = true
|
||||
}
|
||||
|
||||
TransactionPreview {
|
||||
id: pvwTransaction
|
||||
width: stack.width
|
||||
fromAccount: selectFromAccount.selectedAccount
|
||||
gas: {
|
||||
"value": gasSelector.selectedGasEthValue,
|
||||
"symbol": "ETH",
|
||||
"fiatValue": gasSelector.selectedGasFiatValue
|
||||
}
|
||||
toAccount: selectRecipient.selectedRecipient
|
||||
asset: root.asset
|
||||
currency: walletModel.defaultCurrency
|
||||
amount: {
|
||||
const fiatValue = walletModel.getFiatValue(root.packPrice || 0, root.asset.symbol, currency)
|
||||
return { "value": root.packPrice, "fiatValue": fiatValue }
|
||||
}
|
||||
reset: function() {
|
||||
fromAccount = Qt.binding(function() { return selectFromAccount.selectedAccount })
|
||||
toAccount = Qt.binding(function() { return selectRecipient.selectedRecipient })
|
||||
asset = Qt.binding(function() { return root.asset })
|
||||
amount = Qt.binding(function() { return { "value": root.packPrice, "fiatValue": walletModel.getFiatValue(root.packPrice, root.asset.symbol, currency) } })
|
||||
gas = Qt.binding(function() {
|
||||
return {
|
||||
"value": gasSelector.selectedGasEthValue,
|
||||
"symbol": "ETH",
|
||||
"fiatValue": gasSelector.selectedGasFiatValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group4
|
||||
headerText: qsTr("Send %1 %2").arg(Utils.stripTrailingZeros(root.packPrice)).arg(root.asset.symbol)
|
||||
footerText: qsTr("Sign with password")
|
||||
|
||||
TransactionSigner {
|
||||
id: transactionSigner
|
||||
width: stack.width
|
||||
signingPhrase: walletModel.signingPhrase
|
||||
reset: function() {
|
||||
signingPhrase = Qt.binding(function() { return walletModel.signingPhrase })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: Item {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
StyledButton {
|
||||
id: btnBack
|
||||
anchors.left: parent.left
|
||||
//% "Back"
|
||||
label: qsTrId("back")
|
||||
onClicked: {
|
||||
if (stack.isFirstGroup) {
|
||||
return root.close()
|
||||
}
|
||||
stack.back()
|
||||
}
|
||||
}
|
||||
StyledButton {
|
||||
id: btnNext
|
||||
anchors.right: parent.right
|
||||
label: qsTr("Next")
|
||||
disabled: !stack.currentGroup.isValid
|
||||
onClicked: {
|
||||
const isValid = stack.currentGroup.validate()
|
||||
|
||||
if (stack.currentGroup.validate()) {
|
||||
if (stack.isLastGroup) {
|
||||
return root.sendTransaction()
|
||||
}
|
||||
stack.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*##^##
|
||||
Designer {
|
||||
D{i:0;autoSize:true;height:480;width:640}
|
||||
}
|
||||
##^##*/
|
||||
|
|
@ -46,11 +46,11 @@ ModalPopup {
|
|||
let response = JSON.parse(responseStr)
|
||||
|
||||
if (response.error) {
|
||||
if (response.error.includes("could not decrypt key with given password")){
|
||||
if (response.error.message.includes("could not decrypt key with given password")){
|
||||
transactionSigner.validationError = qsTr("Wrong password")
|
||||
return
|
||||
}
|
||||
sendingError.text = response.error
|
||||
sendingError.text = response.error.message
|
||||
return sendingError.open()
|
||||
}
|
||||
|
||||
|
@ -123,7 +123,7 @@ ModalPopup {
|
|||
GasSelector {
|
||||
id: gasSelector
|
||||
anchors.top: txtAmount.bottom
|
||||
anchors.topMargin: Style.current.bigPadding
|
||||
anchors.topMargin: Style.current.bigPadding * 2
|
||||
slowestGasPrice: parseFloat(walletModel.safeLowGasPrice)
|
||||
fastestGasPrice: parseFloat(walletModel.fastestGasPrice)
|
||||
getGasEthValue: walletModel.getGasEthValue
|
||||
|
@ -135,6 +135,21 @@ ModalPopup {
|
|||
fastestGasPrice = Qt.binding(function(){ return parseFloat(walletModel.fastestGasPrice) })
|
||||
}
|
||||
}
|
||||
GasValidator {
|
||||
id: gasValidator
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 8
|
||||
selectedAccount: selectFromAccount.selectedAccount
|
||||
selectedAmount: parseFloat(txtAmount.selectedAmount)
|
||||
selectedAsset: txtAmount.selectedAsset
|
||||
selectedGasEthValue: gasSelector.selectedGasEthValue
|
||||
reset: function() {
|
||||
selectedAccount = Qt.binding(function() { return selectFromAccount.selectedAccount })
|
||||
selectedAmount = Qt.binding(function() { return parseFloat(txtAmount.selectedAmount) })
|
||||
selectedAsset = Qt.binding(function() { return txtAmount.selectedAsset })
|
||||
selectedGasEthValue = Qt.binding(function() { return gasSelector.selectedGasEthValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
TransactionFormGroup {
|
||||
id: group3
|
||||
|
@ -146,9 +161,9 @@ ModalPopup {
|
|||
width: stack.width
|
||||
fromAccount: selectFromAccount.selectedAccount
|
||||
gas: {
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
return { value, "symbol": "ETH", fiatValue }
|
||||
"value": gasSelector.selectedGasEthValue,
|
||||
"symbol": "ETH",
|
||||
"fiatValue": gasSelector.selectedGasFiatValue
|
||||
}
|
||||
toAccount: selectRecipient.selectedRecipient
|
||||
asset: txtAmount.selectedAsset
|
||||
|
@ -159,12 +174,12 @@ ModalPopup {
|
|||
toAccount = Qt.binding(function() { return selectRecipient.selectedRecipient })
|
||||
asset = Qt.binding(function() { return txtAmount.selectedAsset })
|
||||
amount = Qt.binding(function() { return { "value": txtAmount.selectedAmount, "fiatValue": txtAmount.selectedFiatAmount } })
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
gas = Qt.binding(function() {
|
||||
const value = walletModel.getGasEthValue(gasSelector.selectedGasPrice, gasSelector.selectedGasLimit)
|
||||
const fiatValue = walletModel.getFiatValue(value, "ETH", walletModel.defaultCurrency)
|
||||
return { value, "symbol": "ETH", fiatValue }
|
||||
return {
|
||||
"value": gasSelector.selectedGasEthValue,
|
||||
"symbol": "ETH",
|
||||
"fiatValue": gasSelector.selectedGasFiatValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
Tokens 1.0 Tokens.qml
|
||||
Currencies 1.0 Currencies.qml
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.99992 3.16671C7.27606 3.16671 7.49992 3.39057 7.49992 3.66671V8.33337C7.49992 8.60952 7.27606 8.83337 6.99992 8.83337C6.72378 8.83337 6.49992 8.60952 6.49992 8.33337V3.66671C6.49992 3.39057 6.72378 3.16671 6.99992 3.16671Z" fill="#FF2D55"/>
|
||||
<path d="M6.99992 11.5C7.46016 11.5 7.83325 11.1269 7.83325 10.6667C7.83325 10.2065 7.46016 9.83337 6.99992 9.83337C6.53968 9.83337 6.16659 10.2065 6.16659 10.6667C6.16659 11.1269 6.53968 11.5 6.99992 11.5Z" fill="#FF2D55"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.333252 7.00004C0.333252 10.6819 3.31802 13.6667 6.99992 13.6667C10.6818 13.6667 13.6666 10.6819 13.6666 7.00004C13.6666 3.31814 10.6818 0.333374 6.99992 0.333374C3.31802 0.333374 0.333252 3.31814 0.333252 7.00004ZM1.33325 7.00004C1.33325 10.1297 3.8703 12.6667 6.99992 12.6667C10.1295 12.6667 12.6666 10.1297 12.6666 7.00004C12.6666 3.87043 10.1295 1.33337 6.99992 1.33337C3.8703 1.33337 1.33325 3.87043 1.33325 7.00004Z" fill="#FF2D55"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -67,7 +67,7 @@ QtObject {
|
|||
}
|
||||
|
||||
function isValidAddress(inputValue) {
|
||||
return /0x[a-fA-F0-9]{40}/.test(inputValue)
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(inputValue)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,7 +76,11 @@ QtObject {
|
|||
*/
|
||||
function stripTrailingZeros(strNumber) {
|
||||
if (!(typeof strNumber === "string")) {
|
||||
throw "must be a string"
|
||||
try {
|
||||
strNumber = strNumber.toString()
|
||||
} catch(e) {
|
||||
throw "[Utils.stripTrailingZeros] input parameter must be a string"
|
||||
}
|
||||
}
|
||||
return strNumber.replace(/(\.[0-9]*[1-9])0+$|\.0*$/,'$1')
|
||||
}
|
||||
|
@ -91,4 +95,20 @@ QtObject {
|
|||
let hours = messageDate.getHours();
|
||||
return (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes)
|
||||
}
|
||||
|
||||
function findAssetBySymbol(assets, symbolToFind) {
|
||||
for(var i=0; i<assets.rowCount(); i++) {
|
||||
const symbol = assets.rowData(i, "symbol")
|
||||
if (symbol.toLowerCase() === symbolToFind.toLowerCase()) {
|
||||
return {
|
||||
name: assets.rowData(i, "name"),
|
||||
symbol,
|
||||
value: assets.rowData(i, "value"),
|
||||
fiatBalanceDisplay: assets.rowData(i, "fiatBalanceDisplay"),
|
||||
address: assets.rowData(i, "address"),
|
||||
fiatBalance: assets.rowData(i, "fiatBalance")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@ Item {
|
|||
// set to asset symbol to display asset's balance top right
|
||||
// NOTE: if this asset is not selected as a wallet token in the UI, then
|
||||
// nothing will be displayed
|
||||
property string showAssetBalance: ""
|
||||
property string showBalanceForAssetSymbol: ""
|
||||
property var assetFound
|
||||
property double minRequiredAssetBalance: 0
|
||||
property int dropdownWidth: width
|
||||
property alias dropdownAlignment: select.menuAlignment
|
||||
property bool isValid: true
|
||||
|
@ -26,9 +28,20 @@ Item {
|
|||
function resetInternal() {
|
||||
accounts = undefined
|
||||
selectedAccount = undefined
|
||||
showBalanceForAssetSymbol = ""
|
||||
minRequiredAssetBalance = 0
|
||||
assetFound = undefined
|
||||
isValid = true
|
||||
}
|
||||
|
||||
function validate() {
|
||||
if (showBalanceForAssetSymbol == "" || minRequiredAssetBalance == 0 || !assetFound) {
|
||||
return root.isValid
|
||||
}
|
||||
root.isValid = assetFound.value > minRequiredAssetBalance
|
||||
return root.isValid
|
||||
}
|
||||
|
||||
onSelectedAccountChanged: {
|
||||
if (!selectedAccount) {
|
||||
return
|
||||
|
@ -45,24 +58,32 @@ Item {
|
|||
if (selectedAccount.fiatBalance) {
|
||||
textSelectedAddressFiatBalance.text = selectedAccount.fiatBalance + " " + currency.toUpperCase()
|
||||
}
|
||||
if (selectedAccount.assets) {
|
||||
rptAccounts.model = selectedAccount.assets
|
||||
if (selectedAccount.assets && showBalanceForAssetSymbol) {
|
||||
assetFound = Utils.findAssetBySymbol(selectedAccount.assets, showBalanceForAssetSymbol)
|
||||
if (!assetFound) {
|
||||
console.warn(qsTr("Cannot find asset '%1'. Ensure this asset has been added to the token list.").arg(showBalanceForAssetSymbol))
|
||||
}
|
||||
}
|
||||
validate()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: rptAccounts
|
||||
visible: showAssetBalance !== ""
|
||||
delegate: StyledText {
|
||||
visible: symbol === root.showAssetBalance.toUpperCase()
|
||||
anchors.bottom: select.top
|
||||
anchors.bottomMargin: -18
|
||||
anchors.right: parent.right
|
||||
text: "Balance: " + (parseFloat(value) === 0.0 ? "0" : value) + " " + symbol
|
||||
color: parseFloat(value) === 0.0 ? Style.current.danger : Style.current.secondaryText
|
||||
font.pixelSize: 13
|
||||
height: 18
|
||||
onAssetFoundChanged: {
|
||||
if (!assetFound) {
|
||||
return
|
||||
}
|
||||
txtAssetBalance.text = "Balance: " + (parseFloat(assetFound.value) === 0.0 ? "0" : Utils.stripTrailingZeros(assetFound.value)) + " " + assetFound.symbol
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: txtAssetBalance
|
||||
visible: root.assetFound !== undefined
|
||||
anchors.bottom: select.top
|
||||
anchors.bottomMargin: -18
|
||||
anchors.right: parent.right
|
||||
|
||||
color: !root.isValid ? Style.current.danger : Style.current.secondaryText
|
||||
font.pixelSize: 13
|
||||
height: 18
|
||||
}
|
||||
Select {
|
||||
id: select
|
||||
|
|
|
@ -17,6 +17,8 @@ Item {
|
|||
property string defaultCurrency: "USD"
|
||||
property alias selectedGasPrice: inputGasPrice.text
|
||||
property alias selectedGasLimit: inputGasLimit.text
|
||||
property double selectedGasEthValue
|
||||
property double selectedGasFiatValue
|
||||
property string greaterThan0ErrorMessage: qsTr("Must be greater than 0")
|
||||
//% "This needs to be a number"
|
||||
property string invalidInputErrorMessage: qsTrId("this-needs-to-be-a-number")
|
||||
|
@ -47,6 +49,8 @@ Item {
|
|||
let summary = Utils.stripTrailingZeros(ethValue) + " ETH ~" + fiatValue + " " + root.defaultCurrency.toUpperCase()
|
||||
labelGasPriceSummary.text = summary
|
||||
labelGasPriceSummaryAdvanced.text = summary
|
||||
selectedGasEthValue = ethValue
|
||||
selectedGasFiatValue = fiatValue
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
@ -237,7 +241,7 @@ Item {
|
|||
width: 130
|
||||
customHeight: 56
|
||||
text: root.defaultGasPrice()
|
||||
placeholderText: "21000"
|
||||
placeholderText: "20"
|
||||
onTextChanged: {
|
||||
if (inputGasPrice.text.trim() === "") {
|
||||
inputGasPrice.text = root.defaultGasPrice()
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.13
|
||||
import QtQuick.Layouts 1.13
|
||||
import "../imports"
|
||||
import "./"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: colValidation.height
|
||||
property string notEnoughEthForGasMessage: qsTr("Not enough ETH for gas")
|
||||
property var selectedAccount
|
||||
property double selectedAmount
|
||||
property var selectedAsset
|
||||
property double selectedGasEthValue
|
||||
property bool isValid: false
|
||||
property var reset: function() {}
|
||||
|
||||
onSelectedAccountChanged: validate()
|
||||
onSelectedAmountChanged: validate()
|
||||
onSelectedAssetChanged: validate()
|
||||
onSelectedGasEthValueChanged: validate()
|
||||
|
||||
function resetInternal() {
|
||||
selectedAccount = undefined
|
||||
selectedAmount = 0
|
||||
selectedAsset = undefined
|
||||
selectedGasEthValue = 0
|
||||
isValid = true
|
||||
}
|
||||
|
||||
function validate() {
|
||||
let isValid = true
|
||||
if (!(selectedAccount && selectedAccount.assets && selectedAmount > 0 && selectedAsset && selectedGasEthValue > 0)) {
|
||||
return root.isValid
|
||||
}
|
||||
txtValidationError.text = ""
|
||||
let gasTotal = selectedGasEthValue
|
||||
if (selectedAsset && selectedAsset.symbol.toUpperCase() === "ETH") {
|
||||
gasTotal += selectedAmount
|
||||
}
|
||||
const currAcctGasAsset = Utils.findAssetBySymbol(selectedAccount.assets, "ETH")
|
||||
if (currAcctGasAsset.value < gasTotal) {
|
||||
isValid = false
|
||||
txtValidationError.text = notEnoughEthForGasMessage
|
||||
}
|
||||
root.isValid = isValid
|
||||
return isValid
|
||||
}
|
||||
|
||||
Column {
|
||||
id: colValidation
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: txtValidationError.text !== ""
|
||||
spacing: 5
|
||||
|
||||
SVGImage {
|
||||
id: imgExclamation
|
||||
width: 13.33
|
||||
height: 13.33
|
||||
sourceSize.height: height * 2
|
||||
sourceSize.width: width * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../app/img/exclamation_outline.svg"
|
||||
}
|
||||
StyledText {
|
||||
id: txtValidationError
|
||||
text: ""
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 13
|
||||
height: 18
|
||||
color: Style.current.danger
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ Item {
|
|||
height: (readOnly ? inpReadOnly.height : inpAddress.height) + txtLabel.height
|
||||
//% "Invalid ethereum address"
|
||||
readonly property string addressValidationError: qsTrId("invalid-ethereum-address")
|
||||
property bool isValid: false
|
||||
property bool isValid: false || readOnly
|
||||
property var reset: function() {}
|
||||
readonly property var sources: [
|
||||
qsTr("Address"),
|
||||
|
@ -34,7 +34,7 @@ Item {
|
|||
selContact.reset()
|
||||
selAccount.reset()
|
||||
selAddressSource.reset()
|
||||
isValid = false
|
||||
isValid = Qt.binding(function() { return false || readOnly })
|
||||
}
|
||||
|
||||
enum Type {
|
||||
|
|
|
@ -77,6 +77,7 @@ Item {
|
|||
text: root.toAccount ? root.toAccount.address : ""
|
||||
elide: Text.ElideMiddle
|
||||
anchors.leftMargin: 190
|
||||
anchors.right: parent.right
|
||||
}
|
||||
PropertyChanges {
|
||||
target: txtToSecondary
|
||||
|
@ -92,7 +93,8 @@ Item {
|
|||
}
|
||||
PropertyChanges {
|
||||
target: txtToSecondary
|
||||
anchors.rightMargin: Style.current.padding + idtToContact.width + 8
|
||||
anchors.right: idtToContact.left
|
||||
anchors.rightMargin: Style.current.halfPadding
|
||||
width: metSecondary.elidedWidth
|
||||
text: metSecondary.elidedText
|
||||
}
|
||||
|
@ -115,7 +117,8 @@ Item {
|
|||
}
|
||||
PropertyChanges {
|
||||
target: txtToSecondary
|
||||
anchors.rightMargin: Style.current.padding + imgToWallet.width + Style.current.halfPadding
|
||||
anchors.right: imgToWallet.left
|
||||
anchors.rightMargin: Style.current.halfPadding
|
||||
text: metSecondary.elidedText
|
||||
width: metSecondary.elidedWidth
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue