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:
emizzle 2020-09-01 13:49:05 +10:00 committed by Iuri Matias
parent 75cb28b24d
commit d8b0145eb3
21 changed files with 531 additions and 90 deletions

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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")

View File

@ -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

View File

@ -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':

View File

@ -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

View File

@ -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()

View File

@ -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)
}
}
}
}
}

View File

@ -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}
}
##^##*/

View File

@ -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
}
})
}
}

View File

@ -1,2 +1 @@
Tokens 1.0 Tokens.qml
Currencies 1.0 Currencies.qml

View File

@ -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

View File

@ -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")
}
}
}
}
}

View File

@ -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

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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
}