monero-gui/pages/Transfer.qml

1233 lines
56 KiB
QML
Raw Normal View History

// Copyright (c) 2014-2019, The Monero Project
2015-04-01 08:56:05 +00:00
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import QtQml.Models 2.2
2019-04-11 01:17:29 +00:00
import QtQuick 2.9
import QtQuick.Controls 1.4
2016-12-17 15:57:42 +00:00
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.2
2017-12-06 13:09:37 +00:00
import moneroComponents.Clipboard 1.0
import moneroComponents.PendingTransaction 1.0
import moneroComponents.Wallet 1.0
import moneroComponents.NetworkType 1.0
import FontAwesome 1.0
2017-11-25 21:53:07 +00:00
import "../components"
import "../components" as MoneroComponents
import "." 1.0
import "../js/TxUtils.js" as TxUtils
import "../js/Utils.js" as Utils
2014-07-07 17:08:30 +00:00
2016-06-27 12:45:48 +00:00
2014-07-07 17:08:30 +00:00
Rectangle {
2016-08-23 13:07:52 +00:00
id: root
signal paymentClicked(var recipients, string paymentId, int mixinCount, int priority, string description)
signal sweepUnmixableClicked()
2017-11-25 21:53:07 +00:00
color: "transparent"
2019-05-07 05:19:40 +00:00
property alias transferHeight1: pageRoot.height
property alias transferHeight2: advancedLayout.height
property int mixin: 10 // (ring size 11)
property string warningContent: ""
property string sendButtonWarning: {
// Currently opened wallet is not view-only
if (appWindow.viewOnly) {
return qsTr("Wallet is view-only and sends are only possible by using offline transaction signing. " +
"Unless key images are imported, the balance reflects only incoming but not outgoing transactions.") + translationManager.emptyString;
}
// There are sufficient unlocked funds available
if (recipientModel.getAmountTotal() > appWindow.getUnlockedBalance()) {
return qsTr("Amount is more than unlocked balance.") + translationManager.emptyString;
}
if (!recipientModel.hasEmptyAddress()) {
// Address is valid
if (recipientModel.hasInvalidAddress()) {
return qsTr("Address is invalid.") + translationManager.emptyString;
}
// Amount is nonzero
if (recipientModel.hasEmptyAmount()) {
return qsTr("Enter an amount.") + translationManager.emptyString;
}
}
return "";
}
property string startLinkText: "<style type='text/css'>a {text-decoration: none; color: #FF6C3C; font-size: 14px;}</style><a href='#'>(%1)</a>".arg(qsTr("Start daemon")) + translationManager.emptyString
property bool warningLongPidDescription: descriptionLine.text.match(/^[0-9a-f]{64}$/i)
2014-07-10 22:18:36 +00:00
2017-12-06 13:09:37 +00:00
Clipboard { id: clipboard }
2017-11-25 21:53:07 +00:00
2016-12-17 15:57:42 +00:00
function oa_message(text) {
oaPopup.title = qsTr("OpenAlias error") + translationManager.emptyString
oaPopup.text = text
oaPopup.icon = StandardIcon.Information
oaPopup.onCloseCallback = null
oaPopup.open()
}
function fillPaymentDetails(address, payment_id, amount, tx_description, recipient_name) {
if (recipientModel.count > 0) {
const last = recipientModel.count - 1;
if (recipientModel.get(recipientModel.count - 1).address == "") {
recipientModel.remove(last);
}
}
recipientModel.newRecipient(address, Utils.removeTrailingZeros(amount || ""));
setPaymentId(payment_id || "");
2021-07-31 08:42:13 +00:00
setDescription((recipient_name ? recipient_name + (tx_description ? " (" + tx_description + ")" : "") : (tx_description || "")));
}
function updateFromQrCode(address, payment_id, amount, tx_description, recipient_name) {
console.log("updateFromQrCode")
fillPaymentDetails(address, payment_id, amount, tx_description, recipient_name);
2017-01-31 04:36:08 +00:00
cameraUi.qrcode_decoded.disconnect(updateFromQrCode)
}
function setDescription(value) {
descriptionLine.text = value;
descriptionCheckbox.checked = descriptionLine.text != "";
}
function setPaymentId(value) {
paymentIdLine.text = value;
paymentIdCheckbox.checked = paymentIdLine.text != "";
}
2017-02-04 13:44:30 +00:00
function clearFields() {
recipientModel.clear();
fillPaymentDetails("", "", "", "", "");
priorityDropdown.currentIndex = 0
2017-02-04 13:44:30 +00:00
}
2016-12-17 15:57:42 +00:00
// Information dialog
StandardDialog {
// dynamically change onclose handler
property var onCloseCallback
id: oaPopup
cancelVisible: false
onAccepted: {
if (onCloseCallback) {
onCloseCallback()
}
}
}
2017-08-08 09:10:44 +00:00
ColumnLayout {
id: pageRoot
2019-09-05 22:11:12 +00:00
anchors.margins: 20
2019-04-25 19:09:23 +00:00
anchors.topMargin: 40
2017-12-06 13:09:37 +00:00
2017-01-12 19:53:27 +00:00
anchors.left: parent.left
2017-12-06 13:09:37 +00:00
anchors.top: parent.top
2017-01-12 19:53:27 +00:00
anchors.right: parent.right
2017-08-08 09:10:44 +00:00
2019-04-25 19:09:23 +00:00
spacing: 30
RowLayout {
visible: root.warningContent !== ""
MoneroComponents.WarningBox {
text: warningContent
onLinkActivated: {
appWindow.startDaemon(appWindow.persistentSettings.daemonFlags);
}
}
}
2019-09-06 13:52:45 +00:00
RowLayout {
visible: leftPanel.minutesToUnlock !== ""
MoneroComponents.WarningBox {
text: qsTr("Spendable funds: %1 XMR. Please wait ~%2 minutes for your whole balance to become spendable.").arg(leftPanel.balanceUnlockedString).arg(leftPanel.minutesToUnlock)
}
}
ListModel {
id: recipientModel
readonly property int maxRecipients: 16
ListElement {
address: ""
amount: ""
}
function newRecipient(address, amount) {
if (recipientModel.count < maxRecipients) {
recipientModel.append({address: address, amount: amount});
return true;
}
return false;
}
function getRecipients() {
var recipients = [];
for (var index = 0; index < recipientModel.count; ++index) {
const recipient = recipientModel.get(index);
recipients.push({
address: recipient.address,
amount: recipient.amount,
});
}
return recipients;
}
function getAmountTotal() {
var sum = [];
for (var index = 0; index < recipientModel.count; ++index) {
const amount = recipientModel.get(index).amount;
if (amount == "(all)") {
return appWindow.getUnlockedBalance();
}
sum.push(amount || "0");
}
return walletManager.amountsSumFromStrings(sum);
}
function hasEmptyAmount() {
for (var index = 0; index < recipientModel.count; ++index) {
if (recipientModel.get(index).amount === "") {
return true;
}
}
return false;
}
function hasEmptyAddress() {
for (var index = 0; index < recipientModel.count; ++index) {
if (recipientModel.get(index).address === "") {
return true;
}
}
return false;
}
function hasInvalidAddress() {
for (var index = 0; index < recipientModel.count; ++index) {
if (!TxUtils.checkAddress(recipientModel.get(index).address, appWindow.persistentSettings.nettype)) {
return true;
}
}
return false;
}
}
Item {
Layout.fillWidth: true
implicitHeight: recipientLayout.height
ColumnLayout {
id: recipientLayout
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
readonly property int colSpacing: 10
readonly property int rowSpacing: 10
readonly property int secondRowWidth: 125
readonly property int thirdRowWidth: 50
RowLayout {
Layout.bottomMargin: recipientLayout.rowSpacing / 2
spacing: recipientLayout.colSpacing
RowLayout {
id: addressLabel
spacing: 6
Layout.fillWidth: true
MoneroComponents.TextPlain {
font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 16
color: MoneroComponents.Style.defaultFontColor
text: qsTr("Address") + translationManager.emptyString
}
MoneroComponents.InlineButton {
2021-04-14 19:30:42 +00:00
fontFamily: FontAwesome.fontFamilySolid
fontStyleName: "Solid"
fontPixelSize: 18
text: FontAwesome.desktop
tooltip: qsTr("Grab QR code from screen") + translationManager.emptyString
onClicked: {
clearFields();
const codes = oshelper.grabQrCodesFromScreen();
for (var index = 0; index < codes.length; ++index) {
const parsed = walletManager.parse_uri_to_object(codes[index]);
if (!parsed.error) {
fillPaymentDetails(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description, parsed.recipient_name);
break;
} else if (walletManager.addressValid(codes[index], appWindow.persistentSettings.nettype)) {
fillPaymentDetails(codes[index]);
break;
}
}
}
}
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamilySolid
fontStyleName: "Solid"
text: FontAwesome.qrcode
visible: appWindow.qrScannerEnabled
tooltip: qsTr("Scan QR code") + translationManager.emptyString
onClicked: {
cameraUi.state = "Capture"
cameraUi.qrcode_decoded.connect(updateFromQrCode)
}
}
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily
text: FontAwesome.addressBook
tooltip: qsTr("Import from address book") + translationManager.emptyString
onClicked: {
middlePanel.addressBookView.selectAndSend = true;
appWindow.showPageRequest("AddressBook");
}
}
2017-12-06 13:09:37 +00:00
Item {
Layout.fillWidth: true
}
}
2017-02-27 21:05:28 +00:00
RowLayout {
id: amountLabel
spacing: 6
Layout.preferredWidth: 125
Layout.maximumWidth: recipientLayout.secondRowWidth
MoneroComponents.TextPlain {
font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 16
color: MoneroComponents.Style.defaultFontColor
text: qsTr("Amount") + translationManager.emptyString
}
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamilySolid
fontStyleName: "Solid"
fontPixelSize: 16
text: FontAwesome.infinity
visible: recipientModel.count == 1
tooltip: qsTr("Send all unlocked balance of this account") + translationManager.emptyString
onClicked: recipientRepeater.itemAt(0).children[1].children[2].text = "(all)";
}
Item {
Layout.fillWidth: true
}
}
Item {
Layout.preferredWidth: recipientLayout.thirdRowWidth
}
}
Repeater {
id: recipientRepeater
model: recipientModel
ColumnLayout {
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.topMargin: -1
Layout.leftMargin: 1
Layout.rightMargin: recipientLayout.thirdRowWidth + 1
color: MoneroComponents.Style.inputBorderColorInActive
height: 1
visible: index > 0
}
RowLayout {
spacing: 0
MoneroComponents.LineEditMulti {
KeyNavigation.backtab: index > 0 ? recipientRepeater.itemAt(index - 1).children[1].children[2] : sendButton
KeyNavigation.tab: parent.children[2]
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: index > 0 ? 0 : 1
Layout.bottomMargin: 2
Layout.fillWidth: true
addressValidation: true
borderDisabled: true
fontColor: error && text != "" ? MoneroComponents.Style.errorColor : MoneroComponents.Style.defaultFontColor
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputPaddingBottom: 0
inputPaddingTop: 0
inputPaddingRight: 0
placeholderFontFamily: MoneroComponents.Style.fontMonoRegular.name
placeholderFontSize: 14
spacing: 0
wrapMode: Text.WrapAnywhere
placeholderText: {
if(persistentSettings.nettype == NetworkType.MAINNET){
return "4.. / 8.. / monero:.. / OpenAlias";
} else if (persistentSettings.nettype == NetworkType.STAGENET){
return "5.. / 7.. / monero:..";
} else if(persistentSettings.nettype == NetworkType.TESTNET){
return "9.. / B.. / monero:..";
}
}
onTextChanged: {
const parsed = walletManager.parse_uri_to_object(text);
if (!parsed.error) {
2021-07-31 08:42:13 +00:00
fillPaymentDetails(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description, parsed.recipient_name);
}
address = text;
}
text: address
MoneroComponents.InlineButton {
small: true
text: qsTr("Resolve") + translationManager.emptyString
visible: TxUtils.isValidOpenAliasAddress(address)
onClicked: {
var result = walletManager.resolveOpenAlias(address)
if (result) {
var parts = result.split("|")
if (parts.length == 2) {
var address_ok = walletManager.addressValid(parts[1], appWindow.persistentSettings.nettype)
if (parts[0] === "true") {
if (address_ok) {
// prepend openalias to description
descriptionLine.text = descriptionLine.text ? address + " " + descriptionLine.text : address
descriptionCheckbox.checked = true
recipientRepeater.itemAt(index).children[1].children[0].text = parts[1];
}
else
oa_message(qsTr("No valid address found at this OpenAlias address"))
}
else if (parts[0] === "false") {
if (address_ok) {
recipientRepeater.itemAt(index).children[1].children[0].text = parts[1];
oa_message(qsTr("Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed"))
}
else
{
oa_message(qsTr("No valid address found at this OpenAlias address, but the DNSSEC signatures could not be verified, so this may be spoofed"))
}
}
else {
oa_message(qsTr("Internal error"))
}
}
else {
oa_message(qsTr("Internal error"))
}
}
else {
oa_message(qsTr("No address found"))
}
}
}
}
Rectangle {
Layout.fillHeight: true
Layout.topMargin: index > 0 ? 0 : 1
Layout.bottomMargin: 1
Layout.leftMargin: recipientLayout.colSpacing / 2 - width
Layout.rightMargin: recipientLayout.colSpacing / 2
color: MoneroComponents.Style.inputBorderColorInActive
width: 1
}
MoneroComponents.LineEdit {
KeyNavigation.backtab: parent.children[0]
KeyNavigation.tab: index + 1 < recipientRepeater.count ? recipientRepeater.itemAt(index + 1).children[1].children[0] : sendButton
Layout.alignment: Qt.AlignVCenter
Layout.topMargin: index > 0 ? 0 : 1
Layout.bottomMargin: 2
Layout.rightMargin: recipientLayout.colSpacing / 2
Layout.preferredWidth: 125
Layout.maximumWidth: 125
borderDisabled: true
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputPaddingLeft: 0
inputPaddingRight: 0
inputPaddingTop: 0
inputPaddingBottom: 0
placeholderFontFamily: MoneroComponents.Style.fontMonoRegular.name
placeholderFontSize: 14
placeholderLeftMargin: 0
placeholderText: "0.00"
text: amount
onTextChanged: {
text = text.trim().replace(",", ".");
const match = text.match(/^0+(\d.*)/);
if (match) {
const cursorPosition = cursorPosition;
text = match[1];
cursorPosition = Math.max(cursorPosition, 1) - 1;
} else if(text.indexOf('.') === 0){
text = '0' + text;
if (text.length > 2) {
cursorPosition = 1;
}
}
error = walletManager.amountFromString(text) > appWindow.getUnlockedBalance();
amount = text;
}
validator: RegExpValidator {
regExp: /^\s*(\d{1,8})?([\.,]\d{1,12})?\s*$/
}
}
MoneroComponents.TextPlain {
Layout.leftMargin: recipientLayout.colSpacing / 2
Layout.preferredWidth: recipientLayout.thirdRowWidth
font.family: FontAwesome.fontFamilySolid
font.styleName: "Solid"
horizontalAlignment: Text.AlignHCenter
opacity: mouseArea.containsMouse ? 1 : 0.85
text: FontAwesome.times
tooltip: qsTr("Remove recipient") + translationManager.emptyString
tooltipLeft: true
visible: recipientModel.count > 1
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.tooltipPopup.open()
onExited: parent.tooltipPopup.close()
onClicked: recipientModel.remove(index);
}
2019-12-05 10:13:29 +00:00
}
MoneroComponents.TextPlain {
Layout.leftMargin: recipientLayout.colSpacing / 2
Layout.preferredWidth: recipientLayout.thirdRowWidth
horizontalAlignment: Text.AlignHCenter
font.family: MoneroComponents.Style.fontRegular.name
text: "XMR"
visible: recipientModel.count == 1
}
2019-12-05 10:13:29 +00:00
}
}
}
GridLayout {
id: totalLayout
Layout.topMargin: recipientLayout.rowSpacing / 2
Layout.fillWidth: true
columns: 3
columnSpacing: recipientLayout.colSpacing
rowSpacing: 0
RowLayout {
Layout.column: 0
Layout.row: 0
Layout.fillWidth: true
Layout.topMargin: recipientModel.count > 1 ? 0 : -1
spacing: 0
CheckBox {
border: false
checked: false
enabled: {
if (recipientModel.count > 0 && recipientModel.get(0).amount == "(all)") {
return false;
}
if (recipientModel.count >= recipientModel.maxRecipients) {
return false;
}
return true;
}
fontAwesomeIcons: true
fontSize: descriptionLine.labelFontSize
iconOnTheLeft: true
text: qsTr("Add recipient") + translationManager.emptyString
toggleOnClick: false
uncheckedIcon: FontAwesome.plusCircle
onClicked: {
recipientModel.newRecipient("", "");
}
}
MoneroComponents.TextPlain {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 16
text: recipientModel.count > 1 ? qsTr("Total") + translationManager.emptyString : ""
}
}
MoneroComponents.LineEdit {
id: totalValue
Layout.column: 1
Layout.row: 0
Layout.preferredWidth: recipientLayout.secondRowWidth
Layout.topMargin: recipientModel.count > 1 ? 0 : -1
Layout.maximumWidth: recipientLayout.secondRowWidth
borderDisabled: true
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputHeight: 30
inputPaddingLeft: 0
inputPaddingRight: 0
inputPaddingTop: 0
inputPaddingBottom: 0
readOnly: true
text: Utils.removeTrailingZeros(walletManager.displayAmount(recipientModel.getAmountTotal()))
visible: recipientModel.count > 1
}
MoneroComponents.TextPlain {
Layout.column: 2
Layout.row: 0
Layout.preferredWidth: recipientLayout.thirdRowWidth
Layout.maximumWidth: recipientLayout.thirdRowWidth
horizontalAlignment: Text.AlignHCenter
font.family: MoneroComponents.Style.fontRegular.name
text: "XMR"
visible: recipientModel.count > 1
}
MoneroComponents.LineEdit {
Layout.column: 1
Layout.row: recipientModel.count > 1 ? 1 : 0
Layout.preferredWidth: recipientLayout.secondRowWidth
Layout.topMargin: recipientModel.count > 1 ? 0 : -1
Layout.maximumWidth: recipientLayout.secondRowWidth
borderDisabled: true
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputHeight: 30
inputPaddingLeft: 0
inputPaddingRight: 0
inputPaddingTop: 0
inputPaddingBottom: 0
opacity: 0.7
readOnly: true
text: fiatApiConvertToFiat(walletManager.displayAmount(recipientModel.getAmountTotal()))
visible: persistentSettings.fiatPriceEnabled
}
MoneroComponents.TextPlain {
Layout.column: 2
Layout.row: recipientModel.count > 1 ? 1 : 0
Layout.preferredWidth: recipientLayout.thirdRowWidth
Layout.topMargin: recipientModel.count > 1 ? 0 : -1
Layout.maximumWidth: recipientLayout.thirdRowWidth
font.family: MoneroComponents.Style.fontRegular.name
horizontalAlignment: Text.AlignHCenter
opacity: 0.7
text: fiatApiCurrencySymbol()
visible: persistentSettings.fiatPriceEnabled
}
}
}
Rectangle {
anchors.top: recipientLayout.top
anchors.topMargin: addressLabel.height + recipientLayout.rowSpacing / 2
anchors.bottom: recipientLayout.bottom
anchors.bottomMargin: totalLayout.height + recipientLayout.rowSpacing / 2
anchors.left: recipientLayout.left
anchors.right: recipientLayout.right
anchors.rightMargin: recipientLayout.thirdRowWidth
color: "transparent"
border.color: MoneroComponents.Style.inputBorderColorInActive
border.width: 1
radius: 4
}
}
ColumnLayout {
spacing: 0
visible: appWindow.walletMode >= 2
// Note: workaround for translations in listElements
// ListElement: cannot use script for property value, so
// code like this wont work:
// ListElement { column1: qsTr("LOW") + translationManager.emptyString ; column2: ""; priority: PendingTransaction.Priority_Low }
// For translations to work, the strings need to be listed in
// the file components/StandardDropdown.qml too.
// Priorites after v5
ListModel {
id: priorityModelV5
ListElement { column1: qsTr("Automatic") ; column2: ""; priority: 0}
ListElement { column1: qsTr("Slow (x0.2 fee)") ; column2: ""; priority: 1}
ListElement { column1: qsTr("Normal (x1 fee)") ; column2: ""; priority: 2 }
ListElement { column1: qsTr("Fast (x5 fee)") ; column2: ""; priority: 3 }
ListElement { column1: qsTr("Fastest (x200 fee)") ; column2: ""; priority: 4 }
}
RowLayout {
Layout.topMargin: 5
spacing: 10
StandardDropdown {
Layout.maximumWidth: 200
id: priorityDropdown
currentIndex: 0
dataModel: priorityModelV5
labelText: qsTr("Transaction priority") + translationManager.emptyString
labelFontSize: 16
}
MoneroComponents.TextPlain {
id: feeLabel
2021-06-30 23:19:57 +00:00
Layout.alignment: Qt.AlignBottom
Layout.bottomMargin: 11
font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 14
color: MoneroComponents.Style.defaultFontColor
opacity: 0.7
property bool estimating: false
property var estimatedFee: null
property string estimatedFeeFiat: {
if (!persistentSettings.fiatPriceEnabled || estimatedFee == null) {
return "";
}
const fiatFee = fiatApiConvertToFiat(estimatedFee);
return " (%1 %3)".arg(fiatFee < 0.01 ? "<0.01" : "~" + fiatFee).arg(fiatApiCurrencySymbol());
}
property var fee: {
estimatedFee = null;
estimating = sendButton.enabled;
if (!sendButton.enabled || !currentWallet) {
return;
}
var addresses = [];
var amounts = [];
for (var index = 0; index < recipientModel.count; ++index) {
const recipient = recipientModel.get(index);
addresses.push(recipient.address);
amounts.push(walletManager.amountFromString(recipient.amount));
}
currentWallet.estimateTransactionFeeAsync(
addresses,
amounts,
priorityModelV5.get(priorityDropdown.currentIndex).priority,
function (amount) {
if (amount) {
estimatedFee = Utils.removeTrailingZeros(amount);
}
estimating = false;
});
}
text: {
if (!sendButton.enabled || estimatedFee == null) {
return ""
}
return "~%1 XMR%2 %3".arg(estimatedFee)
.arg(estimatedFeeFiat)
.arg(qsTr("fee") + translationManager.emptyString);
}
BusyIndicator {
anchors.left: parent.left
running: feeLabel.estimating
height: parent.height
width: height
}
}
}
}
2017-02-27 21:05:28 +00:00
MoneroComponents.WarningBox {
text: qsTr("Description field contents match long payment ID format. \
Please don't paste long payment ID into description field, your funds might be lost.") + translationManager.emptyString;
visible: warningLongPidDescription
}
ColumnLayout {
spacing: 15
ColumnLayout {
CheckBox {
id: descriptionCheckbox
border: false
checkedIcon: FontAwesome.minusCircle
uncheckedIcon: FontAwesome.plusCircle
fontAwesomeIcons: true
fontSize: descriptionLine.labelFontSize
iconOnTheLeft: true
Layout.fillWidth: true
text: qsTr("Add description") + translationManager.emptyString
onClicked: {
if (!descriptionCheckbox.checked) {
descriptionLine.text = "";
}
}
}
LineEdit {
id: descriptionLine
placeholderFontSize: 16
fontSize: 16
placeholderText: qsTr("Saved to local wallet history") + " (" + qsTr("only visible to you") + ")" + translationManager.emptyString
Layout.fillWidth: true
visible: descriptionCheckbox.checked
}
2017-08-08 09:10:44 +00:00
}
2017-02-27 21:05:28 +00:00
ColumnLayout {
2019-09-28 02:00:51 +00:00
visible: paymentIdCheckbox.checked
CheckBox {
id: paymentIdCheckbox
border: false
checkedIcon: FontAwesome.minusCircle
uncheckedIcon: FontAwesome.plusCircle
fontAwesomeIcons: true
fontSize: paymentIdLine.labelFontSize
iconOnTheLeft: true
Layout.fillWidth: true
text: qsTr("Add payment ID") + translationManager.emptyString
onClicked: {
if (!paymentIdCheckbox.checked) {
paymentIdLine.text = "";
}
}
}
// payment id input
LineEditMulti {
id: paymentIdLine
fontBold: true
placeholderText: qsTr("64 hexadecimal characters") + translationManager.emptyString
2019-09-28 02:00:51 +00:00
readOnly: true
Layout.fillWidth: true
wrapMode: Text.WrapAnywhere
addressValidation: false
visible: paymentIdCheckbox.checked
2019-12-19 12:47:19 +00:00
error: paymentIdCheckbox.checked
}
2017-08-08 09:10:44 +00:00
}
2017-12-06 13:09:37 +00:00
}
2017-02-27 21:05:28 +00:00
MoneroComponents.WarningBox {
id: paymentIdWarningBox
2019-09-28 02:00:51 +00:00
text: qsTr("Long payment IDs are obsolete. \
Long payment IDs were not encrypted on the blockchain and would harm your privacy. \
If the party you're sending to still requires a long payment ID, please notify them.") + translationManager.emptyString;
2019-12-19 12:47:19 +00:00
visible: paymentIdCheckbox.checked || warningLongPidDescription
}
MoneroComponents.WarningBox {
id: sendButtonWarningBox
text: root.sendButtonWarning
visible: root.sendButtonWarning !== ""
}
2017-12-06 13:09:37 +00:00
RowLayout {
2017-03-08 19:54:11 +00:00
StandardButton {
id: sendButton
2019-04-11 01:17:29 +00:00
rightIcon: "qrc:///images/rightArrow.png"
2019-04-25 19:09:23 +00:00
Layout.topMargin: 4
2017-03-08 19:54:11 +00:00
text: qsTr("Send") + translationManager.emptyString
enabled: !sendButtonWarningBox.visible && !warningContent && !recipientModel.hasEmptyAddress() && !paymentIdWarningBox.visible
2017-03-08 19:54:11 +00:00
onClicked: {
console.log("Transfer: paymentClicked")
var priority = priorityModelV5.get(priorityDropdown.currentIndex).priority
2017-03-08 19:54:11 +00:00
console.log("priority: " + priority)
setPaymentId(paymentIdLine.text.trim());
root.paymentClicked(recipientModel.getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
2017-03-08 19:54:11 +00:00
}
2017-02-27 21:05:28 +00:00
}
}
2017-03-08 19:54:11 +00:00
function checkInformation() {
return !recipientModel.hasEmptyAmount() &&
recipientModel.getAmountTotal() <= appWindow.getUnlockedBalance() &&
!recipientModel.hasInvalidAddress();
2017-08-08 09:10:44 +00:00
}
2017-03-08 19:54:11 +00:00
2017-01-12 19:53:27 +00:00
} // pageRoot
2017-01-12 19:53:27 +00:00
ColumnLayout {
2019-05-07 05:19:40 +00:00
id: advancedLayout
2017-01-12 19:53:27 +00:00
anchors.top: pageRoot.bottom
anchors.left: parent.left
2019-09-05 22:11:12 +00:00
anchors.margins: 20
2019-04-25 19:09:23 +00:00
anchors.topMargin: 32
2020-04-16 21:55:22 +00:00
spacing: 10
2017-01-12 19:53:27 +00:00
enabled: !viewOnly || pageRoot.enabled
RowLayout {
2019-01-14 00:02:44 +00:00
visible: appWindow.walletMode >= 2
CheckBox2 {
2017-02-27 21:05:28 +00:00
id: showAdvancedCheckbox
checked: persistentSettings.transferShowAdvanced
onClicked: {
persistentSettings.transferShowAdvanced = !persistentSettings.transferShowAdvanced
}
text: qsTr("Advanced options") + translationManager.emptyString
2017-01-12 19:53:27 +00:00
}
}
AdvancedOptionsItem {
visible: persistentSettings.transferShowAdvanced && appWindow.walletMode >= 2
title: qsTr("Outputs") + translationManager.emptyString
button1.text: qsTr("Export") + translationManager.emptyString
button1.enabled: appWindow.viewOnly
button1.onClicked: {
console.log("Transfer: export outputs clicked")
exportOutputsDialog.open();
}
button2.text: qsTr("Import") + translationManager.emptyString
button2.enabled: !appWindow.viewOnly
button2.onClicked: {
console.log("Transfer: import outputs clicked")
importOutputsDialog.open();
}
tooltip: {
var header = qsTr("Required for cold wallets to sign their corresponding key images") + translationManager.emptyString;
return "<style type='text/css'>.header{ font-size: 13px; } p{line-height:20px; margin-top:0px; margin-bottom:0px; " +
";} p.orange{color:#ff9323;}</style>" +
"<div class='header'>" + header + "</div>" +
"<p>" + qsTr("1. Using view-only wallet, export the outputs into a file") + "</p>" +
"<p>" + qsTr("2. Using cold wallet, import the outputs file") + "</p>" +
translationManager.emptyString
}
}
2020-04-16 21:55:22 +00:00
AdvancedOptionsItem {
2019-01-14 00:02:44 +00:00
visible: persistentSettings.transferShowAdvanced && appWindow.walletMode >= 2
2020-04-16 21:55:22 +00:00
title: qsTr("Key images") + translationManager.emptyString
button1.text: qsTr("Export") + translationManager.emptyString
button1.enabled: !appWindow.viewOnly
button1.onClicked: {
console.log("Transfer: export key images clicked")
exportKeyImagesDialog.open();
2017-01-12 19:53:27 +00:00
}
2020-04-16 21:55:22 +00:00
button2.text: qsTr("Import") + translationManager.emptyString
button2.enabled: appWindow.viewOnly && appWindow.isTrustedDaemon()
button2.onClicked: {
console.log("Transfer: import key images clicked")
importKeyImagesDialog.open();
2017-01-12 19:53:27 +00:00
}
tooltip: {
2020-04-16 21:55:22 +00:00
var errorMessage = "";
if (appWindow.viewOnly && !appWindow.isTrustedDaemon()){
errorMessage = "<p class='orange'>" + qsTr("* To import, you must connect to a local node or a trusted remote node") + "</p>";
2017-01-12 19:53:27 +00:00
}
var header = qsTr("Required for view-only wallets to display the real balance") + translationManager.emptyString;
return "<style type='text/css'>.header{ font-size: 13px; } p{line-height:20px; margin-top:0px; margin-bottom:0px; " +
2020-10-07 10:59:33 +00:00
";} p.orange{color:#ff9323;}</style>" +
"<div class='header'>" + header + "</div>" +
2020-04-16 21:55:22 +00:00
"<p>" + qsTr("1. Using cold wallet, export the key images into a file") + "</p>" +
"<p>" + qsTr("2. Using view-only wallet, import the key images file") + "</p>" +
errorMessage + translationManager.emptyString
2017-01-12 19:53:27 +00:00
}
2020-04-16 21:55:22 +00:00
}
2020-04-16 21:55:22 +00:00
AdvancedOptionsItem {
visible: persistentSettings.transferShowAdvanced && appWindow.walletMode >= 2
title: qsTr("Offline transaction signing") + translationManager.emptyString
button1.text: qsTr("Create") + translationManager.emptyString
button1.enabled: appWindow.viewOnly && pageRoot.checkInformation()
2020-04-16 21:55:22 +00:00
button1.onClicked: {
console.log("Transfer: saveTx Clicked")
var priority = priorityModelV5.get(priorityDropdown.currentIndex).priority
console.log("priority: " + priority)
setPaymentId(paymentIdLine.text.trim());
root.paymentClicked(recipientModel.getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
2017-01-12 19:53:27 +00:00
}
2020-04-16 21:55:22 +00:00
button2.text: qsTr("Sign (offline)") + translationManager.emptyString
button2.enabled: !appWindow.viewOnly
button2.onClicked: {
console.log("Transfer: sign tx clicked")
signTxDialog.open();
}
button3.text: qsTr("Submit") + translationManager.emptyString
button3.enabled: appWindow.viewOnly
button3.onClicked: {
console.log("Transfer: submit tx clicked")
submitTxDialog.open();
}
tooltip: {
2020-04-16 21:55:22 +00:00
var errorMessage = "";
if (appWindow.viewOnly && !pageRoot.checkInformation()) {
2020-04-16 21:55:22 +00:00
errorMessage = "<p class='orange'>" + qsTr("* To create a transaction file, please enter address and amount above") + "</p>";
2018-05-14 21:43:52 +00:00
}
var header = qsTr("Spend XMR from a cold (offline) wallet") + translationManager.emptyString;
return "<style type='text/css'>.header{ font-size: 13px; } p{line-height:20px; margin-top:0px; margin-bottom:0px; " +
2020-10-07 10:59:33 +00:00
";} p.orange{color:#ff9323;}</style>" +
"<div class='header'>" + header + "</div>" +
2020-05-10 21:48:48 +00:00
"<p>" + qsTr("1. Using view-only wallet, export the outputs into a file") + "</p>" +
2020-04-16 21:55:22 +00:00
"<p>" + qsTr("2. Using cold wallet, import the outputs file and export the key images") + "</p>" +
"<p>" + qsTr("3. Using view-only wallet, import the key images file and create a transaction file") + "</p>" +
errorMessage +
"<p>" + qsTr("4. Using cold wallet, sign your transaction file") + "</p>" +
"<p>" + qsTr("5. Using view-only wallet, submit your signed transaction") + "</p>" + translationManager.emptyString
2018-05-14 21:43:52 +00:00
}
2020-04-16 21:55:22 +00:00
}
2018-05-14 21:43:52 +00:00
2020-04-16 21:55:22 +00:00
AdvancedOptionsItem {
visible: persistentSettings.transferShowAdvanced && appWindow.walletMode >= 2
title: qsTr("Unmixable outputs") + translationManager.emptyString
button1.text: qsTr("Sweep") + translationManager.emptyString
button1.enabled : pageRoot.enabled
button1.onClicked: {
console.log("Transfer: sweepUnmixableClicked")
root.sweepUnmixableClicked()
2018-05-14 21:43:52 +00:00
}
tooltip: qsTr("Create a transaction that spends old unmovable outputs") + translationManager.emptyString
2017-01-12 19:53:27 +00:00
}
}
//SignTxDialog
FileDialog {
id: signTxDialog
title: qsTr("Please choose a file") + translationManager.emptyString
folder: "file://" + appWindow.accountsDir
2017-01-12 19:53:27 +00:00
nameFilters: [ "Unsigned transfers (*)"]
onAccepted: {
var path = walletManager.urlToLocalPath(fileUrl);
// Load the unsigned tx from file
var transaction = currentWallet.loadTxFile(path);
if (transaction.status !== PendingTransaction.Status_Ok) {
console.error("Can't load unsigned transaction: ", transaction.errorString);
informationPopup.title = qsTr("Error") + translationManager.emptyString;
informationPopup.text = qsTr("Can't load unsigned transaction: ") + transaction.errorString
informationPopup.icon = StandardIcon.Critical
informationPopup.onCloseCallback = null
informationPopup.open();
// deleting transaction object, we don't want memleaks
transaction.destroy();
} else {
confirmationDialog.text = qsTr("\nNumber of transactions: ") + transaction.txCount
for (var i = 0; i < transaction.txCount; ++i) {
confirmationDialog.text += qsTr("\nTransaction #%1").arg(i+1)
+qsTr("\nRecipient: ") + transaction.recipientAddress[i]
+ (transaction.paymentId[i] == "" ? "" : qsTr("\n\payment ID: ") + transaction.paymentId[i])
+ qsTr("\nAmount: ") + walletManager.displayAmount(transaction.amount(i))
+ qsTr("\nFee: ") + walletManager.displayAmount(transaction.fee(i))
+ qsTr("\nRingsize: ") + (transaction.mixin(i)+1)
2017-01-12 19:53:27 +00:00
// TODO: add descriptions to unsigned_tx_set?
// + (transactionDescription === "" ? "" : (qsTr("\n\nDescription: ") + transactionDescription))
+ translationManager.emptyString
if (i > 0) {
confirmationDialog.text += "\n\n"
}
}
console.log(transaction.confirmationMessage);
// Show confirmation dialog
confirmationDialog.title = qsTr("Confirmation") + translationManager.emptyString
confirmationDialog.icon = StandardIcon.Question
confirmationDialog.onAcceptedCallback = function() {
transaction.sign(path+"_signed");
transaction.destroy();
};
confirmationDialog.onRejectedCallback = transaction.destroy;
confirmationDialog.open()
}
}
onRejected: {
// File dialog closed
console.log("Canceled")
}
}
//SignTxDialog
FileDialog {
id: submitTxDialog
title: qsTr("Please choose a file") + translationManager.emptyString
folder: "file://" + appWindow.accountsDir
2017-01-12 19:53:27 +00:00
nameFilters: [ "signed transfers (*)"]
onAccepted: {
if(!currentWallet.submitTxFile(walletManager.urlToLocalPath(fileUrl))){
informationPopup.title = qsTr("Error") + translationManager.emptyString;
informationPopup.text = qsTr("Can't submit transaction: ") + currentWallet.errorString
informationPopup.icon = StandardIcon.Critical
informationPopup.onCloseCallback = null
informationPopup.open();
} else {
informationPopup.title = qsTr("Information") + translationManager.emptyString
2018-03-23 22:53:29 +00:00
informationPopup.text = qsTr("Monero sent successfully") + translationManager.emptyString
2017-01-12 19:53:27 +00:00
informationPopup.icon = StandardIcon.Information
informationPopup.onCloseCallback = null
informationPopup.open();
}
}
onRejected: {
console.log("Canceled")
}
}
2018-05-14 21:43:52 +00:00
FileDialog {
id: exportOutputsDialog
selectMultiple: false
selectExisting: false
onAccepted: {
console.log(walletManager.urlToLocalPath(exportOutputsDialog.fileUrl))
if (currentWallet.exportOutputs(walletManager.urlToLocalPath(exportOutputsDialog.fileUrl), true)) {
appWindow.showStatusMessage(qsTr("Outputs successfully exported to file") + translationManager.emptyString, 3);
} else {
appWindow.showStatusMessage(currentWallet.errorString, 5);
}
}
onRejected: {
console.log("Canceled");
}
}
FileDialog {
id: importOutputsDialog
selectMultiple: false
selectExisting: true
title: qsTr("Please choose a file") + translationManager.emptyString
onAccepted: {
console.log(walletManager.urlToLocalPath(importOutputsDialog.fileUrl))
if (currentWallet.importOutputs(walletManager.urlToLocalPath(importOutputsDialog.fileUrl))) {
appWindow.showStatusMessage(qsTr("Outputs successfully imported to wallet") + translationManager.emptyString, 3);
} else {
appWindow.showStatusMessage(currentWallet.errorString, 5);
}
}
onRejected: {
console.log("Canceled");
}
}
2018-05-14 21:43:52 +00:00
//ExportKeyImagesDialog
FileDialog {
id: exportKeyImagesDialog
selectMultiple: false
selectExisting: false
onAccepted: {
console.log(walletManager.urlToLocalPath(exportKeyImagesDialog.fileUrl))
if (currentWallet.exportKeyImages(walletManager.urlToLocalPath(exportKeyImagesDialog.fileUrl), true)) {
appWindow.showStatusMessage(qsTr("Key images successfully exported to file") + translationManager.emptyString, 3);
} else {
appWindow.showStatusMessage(currentWallet.errorString, 5);
}
2018-05-14 21:43:52 +00:00
}
onRejected: {
console.log("Canceled");
}
}
//ImportKeyImagesDialog
FileDialog {
id: importKeyImagesDialog
selectMultiple: false
selectExisting: true
title: qsTr("Please choose a file") + translationManager.emptyString
onAccepted: {
console.log(walletManager.urlToLocalPath(importKeyImagesDialog.fileUrl))
if (currentWallet.importKeyImages(walletManager.urlToLocalPath(importKeyImagesDialog.fileUrl))) {
appWindow.showStatusMessage(qsTr("Key images successfully imported to wallet") + translationManager.emptyString, 3);
} else {
appWindow.showStatusMessage(currentWallet.errorString, 5);
}
2018-05-14 21:43:52 +00:00
}
onRejected: {
console.log("Canceled");
}
}
Component.onCompleted: {
//Disable password page until enabled by updateStatus
pageRoot.enabled = false
}
// fires on every page load
function onPageCompleted() {
console.log("transfer page loaded")
updateStatus();
}
//TODO: Add daemon sync status
//TODO: enable send page when we're connected and daemon is synced
function updateStatus() {
2019-01-14 00:02:44 +00:00
var messageNotConnected = qsTr("Wallet is not connected to daemon.");
if(appWindow.walletMode >= 2 && !persistentSettings.useRemoteNode) messageNotConnected += root.startLinkText;
pageRoot.enabled = true;
if(typeof currentWallet === "undefined") {
2019-01-14 00:02:44 +00:00
root.warningContent = messageNotConnected;
return;
}
if (currentWallet.viewOnly) {
// warningText.text = qsTr("Wallet is view only.")
2017-01-12 19:53:27 +00:00
//return;
}
//pageRoot.enabled = false;
switch (currentWallet.connected()) {
case Wallet.ConnectionStatus_Connecting:
root.warningContent = qsTr("Wallet is connecting to daemon.")
break
case Wallet.ConnectionStatus_Disconnected:
2019-01-14 00:02:44 +00:00
root.warningContent = messageNotConnected;
break
case Wallet.ConnectionStatus_WrongVersion:
root.warningContent = qsTr("Connected daemon is not compatible with GUI. \n" +
"Please upgrade or connect to another daemon")
break
default:
if(!appWindow.daemonSynced){
2019-01-14 00:02:44 +00:00
root.warningContent = qsTr("Waiting on daemon synchronization to finish.")
} else {
// everything OK, enable transfer page
2017-08-08 09:10:44 +00:00
// Light wallet is always ready
pageRoot.enabled = true;
root.warningContent = "";
}
}
}
2016-12-10 01:01:04 +00:00
// Popuplate fields from addressbook.
function sendTo(address, paymentId, description, amount) {
middlePanel.state = 'Transfer';
fillPaymentDetails(address, paymentId, amount, description);
2016-12-10 01:01:04 +00:00
}
2014-07-07 17:08:30 +00:00
}