refactor: remove wei2Token in favour of wei2Eth. Essentially de-duplicated very similar procs and lessened errors

fix: SignTransactionModal - set default focused account when none is found

refactor: move token lookup from QML to nim in the toMessage procedure.

fix: 1:1 tx requests - handle case where token contract is not found (ie sending SNT from mainnet and receiving message on testnet)

feat: error checking for building a token transaction

feat: TransactionPreview - add a validation check that disallows continuation if the selected "from" account has insufficient funds
This commit is contained in:
emizzle 2020-10-16 18:37:07 +11:00 committed by Pascal Precht
parent 41e5626bfa
commit e455586990
No known key found for this signature in database
GPG Key ID: 0EE28D8D6FD85D7D
11 changed files with 197 additions and 132 deletions

View File

@ -46,12 +46,6 @@ QtObject:
let uintValue = status_utils.eth2Wei(parseFloat(eth), decimals)
return uintValue.toString()
proc wei2Token*(self: UtilsView, wei: string, decimals: int): string {.slot.} =
var weiValue = wei
if(weiValue.startsWith("0x")):
weiValue = fromHex(Stuint[256], weiValue).toString()
return status_utils.wei2Token(weiValue, decimals)
proc getStickerMarketAddress(self: UtilsView): string {.slot.} =
$self.status.stickers.getStickerMarketAddress

View File

@ -156,8 +156,8 @@ QtObject:
proc setFocusedAccountByAddress*(self: WalletView, address: string) {.slot.} =
if(self.accounts.rowCount() == 0): return
let index = self.accounts.getAccountindexByAddress(address)
if index == -1: return
var index = self.accounts.getAccountindexByAddress(address)
if index == -1: index = 0
let selectedAccount = self.accounts.getAccount(index)
if self.focusedAccount.address == selectedAccount.address: return
self.focusedAccount.setAccountItem(selectedAccount)

View File

@ -70,7 +70,7 @@ type
symbol*: string
hasIcon*: bool
proc newErc20Contract(name: string, network: Network, address: Address, symbol: string, decimals: int, hasIcon: bool): Erc20Contract =
proc newErc20Contract*(name: string, network: Network, address: Address, symbol: string, decimals: int, hasIcon: bool): Erc20Contract =
Erc20Contract(name: name, network: network, address: address, methods: ERC20_METHODS.toTable, symbol: symbol, decimals: decimals, hasIcon: hasIcon)
proc newErc721Contract(name: string, network: Network, address: Address, symbol: string, hasIcon: bool, addlMethods: seq[tuple[name: string, meth: Method]] = @[]): Erc721Contract =

View File

@ -53,27 +53,28 @@ proc eth2Wei*(eth: float, decimals: int = 18): UInt256 =
proc gwei2Wei*(gwei: float): UInt256 =
eth2Wei(gwei, 9)
proc wei2Eth*(input: Stuint[256]): string =
var one_eth = fromHex(Stuint[256], "DE0B6B3A7640000")
proc wei2Eth*(input: Stuint[256], decimals: int = 18): string =
var one_eth = u256(10).pow(decimals) # fromHex(Stuint[256], "DE0B6B3A7640000")
var (eth, remainder) = divmod(input, one_eth)
let leading_zeros = "0".repeat(($one_eth).len - ($remainder).len - 1)
fmt"{eth}.{leading_zeros}{remainder}"
proc wei2Token*(input: string, decimals: int): string =
proc wei2Eth*(input: string, decimals: int): string =
try:
var value = input.parse(Stuint[256])
var p = u256(10).pow(decimals)
var i = value.div(p)
var r = value.mod(p)
var leading_zeros = "0".repeat(decimals - ($r).len)
var d = fmt"{leading_zeros}{$r}"
result = $i
if(r > 0): result = fmt"{result}.{d}"
result. trimZeros()
var input256: Stuint[256]
if input.contains("e+"): # we have a js string BN, ie 1e+21
let
inputSplit = input.split("e+")
whole = inputSplit[0].u256
remainder = u256(10).pow(inputSplit[1].parseInt)
input256 = whole * remainder
else:
input256 = input.u256
result = wei2Eth(input256, decimals)
except Exception as e:
error "Error parsing this wei value", input
error "Error parsing this wei value", input, msg=e.msg
result = "0"

View File

@ -1,11 +1,16 @@
import json, random, strutils, sequtils, sugar, chronicles
import json_serialization
import ../libstatus/accounts as status_accounts
import ../libstatus/accounts/constants as constants
import ../libstatus/settings as status_settings
import ../libstatus/tokens as status_tokens
import ../libstatus/types as status_types
import ../libstatus/eth/contracts as status_contracts
import ../chat/[chat, message]
import ../profile/[profile, devices]
import types
import web3/conversions
from ../libstatus/utils import parseAddress, wei2Eth
proc toMessage*(jsonMsg: JsonNode): Message
@ -201,13 +206,22 @@ proc toMessage*(jsonMsg: JsonNode): Message =
message.stickerHash = jsonMsg["sticker"]["hash"].getStr
if message.contentType == ContentType.Transaction:
let
allContracts = getErc20Contracts().concat(getCustomTokens())
ethereum = newErc20Contract("Ethereum", Network.Mainnet, parseAddress(constants.ZERO_ADDRESS), "ETH", 18, true)
tokenAddress = jsonMsg["commandParameters"]["contract"].getStr
tokenContract = if tokenAddress == "": ethereum else: allContracts.getErc20ContractByAddress(parseAddress(tokenAddress))
tokenContractStr = if tokenContract == nil: "{}" else: $(Json.encode(tokenContract))
var weiStr = if tokenContract == nil: "0" else: wei2Eth(jsonMsg["commandParameters"]["value"].getStr, tokenContract.decimals)
weiStr.trimZeros()
# TODO find a way to use json_seralization for this. When I try, I get an error
message.commandParameters = CommandParameters(
id: jsonMsg["commandParameters"]["id"].getStr,
fromAddress: jsonMsg["commandParameters"]["from"].getStr,
address: jsonMsg["commandParameters"]["address"].getStr,
contract: jsonMsg["commandParameters"]["contract"].getStr,
value: jsonMsg["commandParameters"]["value"].getStr,
contract: tokenContractStr,
value: weiStr,
transactionHash: jsonMsg["commandParameters"]["transactionHash"].getStr,
commandState: jsonMsg["commandParameters"]["commandState"].getInt,
signature: jsonMsg["commandParameters"]["signature"].getStr

View File

@ -64,6 +64,8 @@ proc delete*(self: WalletModel) =
proc buildTokenTransaction(self: WalletModel, source, to, assetAddress: Address, value: float, transfer: var Transfer, contract: var Erc20Contract, gas = "", gasPrice = ""): EthSend =
contract = getErc20Contract(assetAddress)
if contract == nil:
raise newException(ValueError, fmt"Could not find ERC-20 contract with address '{assetAddress}' for the current network")
transfer = Transfer(to: to, value: eth2Wei(value, contract.decimals))
transactions.buildTokenTransaction(source, assetAddress, gas, gasPrice)

View File

@ -12,6 +12,7 @@ ModalPopup {
property var selectedAsset
property var selectedAmount
property var selectedFiatAmount
property bool outgoing: true
property string trxData: ""
@ -61,6 +62,7 @@ ModalPopup {
onClosed: {
stack.reset()
stack.pop(groupPreview, StackView.Immediate)
}
TransactionStackView {
@ -197,6 +199,7 @@ ModalPopup {
onNextClicked: function() {
stack.push(groupSignTx, StackView.Immediate)
}
isValid: groupSelectAcct.isValid && groupSelectGas.isValid && gasValidator.isValid && pvwTransaction.isValid
TransactionPreview {
id: pvwTransaction
@ -211,6 +214,7 @@ ModalPopup {
asset: root.selectedAsset
amount: { "value": root.selectedAmount, "fiatValue": root.selectedFiatAmount }
currency: walletModel.defaultCurrency
outgoing: root.outgoing
reset: function() {
fromAccount = Qt.binding(function() { return root.selectedAccount })
gas = Qt.binding(function() {
@ -224,8 +228,25 @@ ModalPopup {
asset = Qt.binding(function() { return root.selectedAsset })
amount = Qt.binding(function() { return { "value": root.selectedAmount, "fiatValue": root.selectedFiatAmount } })
}
onFromClicked: stack.push(groupSelectAcct, StackView.Immediate)
onGasClicked: stack.push(groupSelectGas, StackView.Immediate)
isFromEditable: true
isGasEditable: true
onFromClicked: { stack.push(groupSelectAcct, StackView.Immediate) }
onGasClicked: { stack.push(groupSelectGas, StackView.Immediate) }
}
GasValidator {
id: gasValidator2
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
selectedAccount: root.selectedAccount
selectedAmount: parseFloat(root.selectedAmount)
selectedAsset: root.selectedAsset
selectedGasEthValue: gasSelector.selectedGasEthValue
reset: function() {
selectedAccount = Qt.binding(function() { return root.selectedAccount })
selectedAmount = Qt.binding(function() { return parseFloat(root.selectedAmount) })
selectedAsset = Qt.binding(function() { return root.selectedAsset })
selectedGasEthValue = Qt.binding(function() { return gasSelector.selectedGasEthValue })
}
}
}
TransactionFormGroup {

View File

@ -24,53 +24,9 @@ Item {
}
}
}
property var tokens: {
const count = walletModel.defaultTokenList.rowCount()
const toks = []
for (var i = 0; i < count; i++) {
toks.push({
"address": walletModel.defaultTokenList.rowData(i, 'address'),
"name": walletModel.defaultTokenList.rowData(i, 'name'),
"decimals": parseInt(walletModel.defaultTokenList.rowData(i, 'decimals'), 10),
"symbol": walletModel.defaultTokenList.rowData(i, 'symbol')
})
}
return toks
}
property var token: {
if (commandParametersObject.contract === "") {
return {
symbol: "ETH",
name: "Ethereum",
address: Constants.zeroAddress,
decimals: 18,
hasIcon: true
}
}
const count = root.tokens.length
for (var i = 0; i < count; i++) {
let token = root.tokens[i]
if (token.address === commandParametersObject.contract) {
return token
}
}
return {}
}
property string tokenAmount: {
if (!commandParametersObject.value) {
return "0"
}
try {
return utilsModel.wei2Token(commandParametersObject.value.toString(), token.decimals)
} catch (e) {
console.error("Error getting the ETH value of:", commandParametersObject.value)
console.error("Error:", e.message)
return "0"
}
}
property string tokenSymbol: token.symbol
property var token: JSON.parse(commandParametersObject.contract) // TODO: handle {}
property string tokenAmount: commandParametersObject.value
property string tokenSymbol: token.symbol || ""
property string fiatValue: {
if (!tokenAmount || !token.symbol) {
return "0"
@ -83,8 +39,8 @@ Item {
switch (root.state) {
case Constants.pending:
case Constants.confirmed:
case Constants.transactionRequested:
case Constants.addressRequested: return isCurrentUser
case Constants.transactionRequested:
case Constants.declined:
case Constants.transactionDeclined:
case Constants.addressReceived: return !isCurrentUser
@ -92,6 +48,12 @@ Item {
}
}
property int innerMargin: 12
property bool isError: commandParametersObject.contract === "{}"
onTokenSymbolChanged: {
if (!!tokenSymbol) {
tokenImage.source = `../../../../img/tokens/${root.tokenSymbol}.png`
}
}
id: root
anchors.left: parent.left
@ -118,11 +80,17 @@ Item {
StyledText {
id: title
color: Style.current.secondaryText
//% " Outgoing transaction"
text: root.outgoing ?
qsTrId("--outgoing-transaction") :
//% " Incoming transaction"
qsTrId("--incoming-transaction")
text: {
if (root.state === Constants.transactionRequested) {
let prefix = root.outgoing ? "↑ " : "↓ "
return prefix + qsTr("Transaction request")
}
return root.outgoing ?
//% " Outgoing transaction"
qsTrId("--outgoing-transaction") :
//% " Incoming transaction"
qsTrId("--incoming-transaction")
}
font.weight: Font.Medium
anchors.top: parent.top
anchors.topMargin: Style.current.halfPadding
@ -140,9 +108,16 @@ Item {
anchors.left: parent.left
anchors.leftMargin: root.innerMargin
StyledText {
id: txtError
color: Style.current.danger
visible: root.isError
text: qsTr("Something has gone wrong")
}
Image {
id: tokenImage
source: `../../../../img/tokens/${root.tokenSymbol}.png`
visible: !root.isError
width: 24
height: 24
anchors.verticalCenter: parent.verticalCenter
@ -150,6 +125,7 @@ Item {
StyledText {
id: tokenText
visible: !root.isError
color: Style.current.textColor
text: `${root.tokenAmount} ${root.tokenSymbol}`
anchors.left: tokenImage.right
@ -159,6 +135,7 @@ Item {
StyledText {
id: fiatText
visible: !root.isError
color: Style.current.secondaryText
text: root.fiatValue
anchors.top: tokenText.bottom
@ -169,7 +146,15 @@ Item {
Loader {
id: bubbleLoader
active: isCurrentUser || (!isCurrentUser && !(root.state === Constants.addressRequested || root.state === Constants.transactionRequested))
active: {
return !root.isError && (
isCurrentUser ||
(!isCurrentUser &&
!(root.state === Constants.addressRequested ||
root.state === Constants.transactionRequested)
)
)
}
sourceComponent: stateBubbleComponent
anchors.top: valueContainer.bottom
anchors.topMargin: Style.current.halfPadding
@ -188,9 +173,11 @@ Item {
Loader {
id: buttonsLoader
active: (root.state === Constants.addressRequested && !root.outgoing) ||
active: !root.isError && (
(root.state === Constants.addressRequested && !root.outgoing) ||
(root.state === Constants.addressReceived && root.outgoing) ||
(root.state === Constants.transactionRequested && !root.outgoing)
(root.state === Constants.transactionRequested && root.outgoing)
)
sourceComponent: root.outgoing ? signAndSendComponent : acceptTransactionComponent
anchors.top: bubbleLoader.active ? bubbleLoader.bottom : valueContainer.bottom
anchors.topMargin: bubbleLoader.active ? root.innerMargin : 20
@ -208,7 +195,9 @@ Item {
Component {
id: signAndSendComponent
SendTransactionButton {}
SendTransactionButton {
outgoing: root.outgoing
}
}
StyledText {

View File

@ -4,8 +4,10 @@ import "../../../../../../imports"
import "../../ChatComponents"
Item {
id: root
width: parent.width
height: childrenRect.height + Style.current.halfPadding
property bool outgoing: true
Separator {
id: separator
@ -55,15 +57,10 @@ Item {
type: RecipientSelector.Type.Contact
}
}
selectedAsset: {
return {
name: token.name,
symbol: token.symbol,
address: commandParametersObject.contract
}
}
selectedAsset: token
selectedAmount: tokenAmount
selectedFiatAmount: fiatValue
outgoing: root.outgoing
}
}

View File

@ -13,6 +13,7 @@ Rectangle {
border.width: 1
border.color: Style.current.border
radius: 24
color: Style.current.background
SVGImage {
id: stateImage

View File

@ -17,6 +17,14 @@ Item {
property var reset: function() {}
signal fromClicked
signal gasClicked
// Creates a mouse area around the "from account". When clicked, triggers
// the "fromClicked" signal
property bool isFromEditable: false
// Creates a mouse area around the "network fee". When clicked, triggers
// the "gasClicked" signal
property bool isGasEditable: false
property bool isValid: true
property bool outgoing: true
function resetInternal() {
fromAccount = undefined
@ -24,7 +32,32 @@ Item {
asset = undefined
amount = undefined
gas = undefined
isValid = true
}
function validate() {
let isValid = true
imgInsufficientBalance.visible = false
console.log(">>> [TransactionPreview.validate] outgoing:", outgoing)
if (outgoing && hasInsufficientBalance()) {
isValid = false
imgInsufficientBalance.visible = true
}
root.isValid = isValid
return isValid
}
function hasInsufficientBalance() {
if (!root.asset || !root.fromAccount || !root.fromAccount.assets || !root.amount) {
return true
}
const currAcctAsset = Utils.findAssetBySymbol(root.fromAccount.assets, root.asset.symbol)
if (!currAcctAsset) return true
return currAcctAsset.value < root.amount.value
}
onAssetChanged: validate()
onFromAccountChanged: validate()
Column {
id: content
@ -36,51 +69,64 @@ Item {
//% "From"
label: qsTrId("from")
value: Item {
id: itmFromValue
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
StyledText {
font.pixelSize: 15
height: 22
text: root.fromAccount ? root.fromAccount.name : ""
elide: Text.ElideRight
anchors.left: parent.left
anchors.right: imgFromWallet.left
anchors.rightMargin: Style.current.halfPadding
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
function needsRightPadding() {
return imgInsufficientBalance.visible || fromArrow.visible
}
SVGImage {
id: imgFromWallet
sourceSize.height: 18
sourceSize.width: 18
anchors.right: fromArrow.visible ? fromArrow.left : parent.right
anchors.rightMargin: fromArrow.visible ? Style.current.padding : 0
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: "../app/img/walletIcon.svg"
ColorOverlay {
anchors.fill: parent
source: parent
color: root.fromAccount ? root.fromAccount.iconColor : Style.current.blue
}
}
SVGImage {
id: fromArrow
width: 13
visible: typeof root.fromClicked === "function"
Row {
spacing: Style.current.halfPadding
rightPadding: itmFromValue.needsRightPadding() ? Style.current.halfPadding : 0
anchors.right: parent.right
anchors.rightMargin: 7
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: "../app/img/caret.svg"
rotation: 270
ColorOverlay {
anchors.fill: parent
visible: parent.visible
source: parent
color: Style.current.secondaryText
StyledText {
font.pixelSize: 15
height: 22
text: root.fromAccount ? root.fromAccount.name : ""
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
}
SVGImage {
id: imgFromWallet
sourceSize.height: 18
sourceSize.width: 18
horizontalAlignment: Image.AlignLeft
width: itmFromValue.needsRightPadding() ? (Style.current.halfPadding + sourceSize.width) : undefined // adding width to add addl spacing to image
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: "../app/img/walletIcon.svg"
ColorOverlay {
anchors.fill: parent
source: parent
color: root.fromAccount ? root.fromAccount.iconColor : Style.current.blue
}
}
SVGImage {
id: imgInsufficientBalance
width: 13
visible: false
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: "../app/img/exclamation_outline.svg"
}
SVGImage {
id: fromArrow
width: 13
visible: root.isFromEditable
anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectFit
source: "../app/img/caret.svg"
rotation: 270
ColorOverlay {
anchors.fill: parent
visible: parent.visible
source: parent
color: Style.current.secondaryText
}
}
}
MouseArea {
@ -398,7 +444,7 @@ Item {
SVGImage {
id: gasArrow
width: 13
visible: typeof root.gasClicked === "function"
visible: root.isGasEditable
anchors.right: parent.right
anchors.rightMargin: 7
anchors.verticalCenter: parent.verticalCenter