Transfer: implement sending to multiple recipients

This commit is contained in:
xiphon 2021-02-05 15:28:01 +00:00
parent 34df4e74d4
commit a0a9d9e31e
4 changed files with 520 additions and 247 deletions

View file

@ -34,7 +34,7 @@ jobs:
- name: install monero dependencies - name: install monero dependencies
run: sudo apt -y install build-essential cmake libboost-all-dev miniupnpc libunbound-dev graphviz doxygen libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev libprotobuf-dev protobuf-compiler run: sudo apt -y install build-essential cmake libboost-all-dev miniupnpc libunbound-dev graphviz doxygen libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev libprotobuf-dev protobuf-compiler
- name: install monero gui dependencies - name: install monero gui dependencies
run: sudo apt -y install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev libgcrypt20-dev xvfb run: sudo apt -y install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtqml-models2 qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev libgcrypt20-dev xvfb
- name: build - name: build
run: DEV_MODE=ON make release -j3 run: DEV_MODE=ON make release -j3
- name: test qml - name: test qml

View file

@ -208,7 +208,7 @@ The following instructions will fetch Qt from your distribution's repositories i
- For Ubuntu 17.10+ - For Ubuntu 17.10+
`sudo apt install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev` `sudo apt install qtbase5-dev qt5-default qtdeclarative5-dev qml-module-qtqml-models2 qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev`
- For Gentoo - For Gentoo

View file

@ -403,6 +403,7 @@ Object {
property string inbox : "\uf01c" property string inbox : "\uf01c"
property string indent : "\uf03c" property string indent : "\uf03c"
property string industry : "\uf275" property string industry : "\uf275"
property string infinity : "\uf534"
property string info : "\uf129" property string info : "\uf129"
property string infoCircle : "\uf05a" property string infoCircle : "\uf05a"
property string inr : "\uf156" property string inr : "\uf156"

View file

@ -26,6 +26,7 @@
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // 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. // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import QtQml.Models 2.2
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
@ -60,19 +61,18 @@ Rectangle {
} }
// There are sufficient unlocked funds available // There are sufficient unlocked funds available
if (walletManager.amountFromString(amountLine.text) > appWindow.getUnlockedBalance()) { if (recipientModel.getAmountTotal() > appWindow.getUnlockedBalance()) {
return qsTr("Amount is more than unlocked balance.") + translationManager.emptyString; return qsTr("Amount is more than unlocked balance.") + translationManager.emptyString;
} }
if (addressLine.text) if (!recipientModel.hasEmptyAddress()) {
{
// Address is valid // Address is valid
if (!TxUtils.checkAddress(addressLine.text, appWindow.persistentSettings.nettype)) { if (recipientModel.hasInvalidAddress()) {
return qsTr("Address is invalid.") + translationManager.emptyString; return qsTr("Address is invalid.") + translationManager.emptyString;
} }
// Amount is nonzero // Amount is nonzero
if (!amountLine.text || parseFloat(amountLine.text) <= 0) { if (recipientModel.hasEmptyAmount()) {
return qsTr("Enter an amount.") + translationManager.emptyString; return qsTr("Enter an amount.") + translationManager.emptyString;
} }
} }
@ -93,10 +93,16 @@ Rectangle {
} }
function fillPaymentDetails(address, payment_id, amount, tx_description, recipient_name) { function fillPaymentDetails(address, payment_id, amount, tx_description, recipient_name) {
addressLine.text = address if (recipientModel.count > 0) {
setPaymentId(payment_id); const last = recipientModel.count - 1;
amountLine.text = amount if (recipientModel.get(recipientModel.count - 1).address == "") {
setDescription((recipient_name ? recipient_name + " " : "") + tx_description); recipientModel.remove(last);
}
}
recipientModel.newRecipient(address, Utils.removeTrailingZeros(amount || ""));
setPaymentId(payment_id || "");
setDescription((recipient_name ? recipient_name + " " : "") + (tx_description || ""));
} }
function updateFromQrCode(address, payment_id, amount, tx_description, recipient_name) { function updateFromQrCode(address, payment_id, amount, tx_description, recipient_name) {
@ -116,17 +122,11 @@ Rectangle {
} }
function clearFields() { function clearFields() {
addressLine.text = "" recipientModel.clear();
setPaymentId(""); fillPaymentDetails("", "", "", "", "");
amountLine.text = ""
setDescription("");
priorityDropdown.currentIndex = 0 priorityDropdown.currentIndex = 0
} }
function getRecipients() {
return [{address: addressLine.text, amount: amountLine.text}];
}
// Information dialog // Information dialog
StandardDialog { StandardDialog {
// dynamically change onclose handler // dynamically change onclose handler
@ -170,37 +170,106 @@ Rectangle {
} }
} }
// recipient address input 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 { RowLayout {
id: addressLineRow Layout.bottomMargin: recipientLayout.rowSpacing / 2
spacing: recipientLayout.colSpacing
RowLayout {
id: addressLabel
spacing: 6
Layout.fillWidth: true Layout.fillWidth: true
LineEditMulti { MoneroComponents.TextPlain {
id: addressLine Layout.leftMargin: 10
KeyNavigation.tab: amountLine font.family: MoneroComponents.Style.fontRegular.name
spacing: 0 font.pixelSize: 16
fontBold: true color: MoneroComponents.Style.defaultFontColor
labelText: qsTr("Address") + translationManager.emptyString text: qsTr("Address") + translationManager.emptyString
labelButtonText: qsTr("Resolve") + translationManager.emptyString
placeholderText: {
if(persistentSettings.nettype == NetworkType.MAINNET){
return "4.. / 8.. / OpenAlias";
} else if (persistentSettings.nettype == NetworkType.STAGENET){
return "5.. / 7..";
} else if(persistentSettings.nettype == NetworkType.TESTNET){
return "9.. / B..";
}
}
wrapMode: Text.WrapAnywhere
addressValidation: true
onTextChanged: {
const parsed = walletManager.parse_uri_to_object(text);
if (!parsed.error) {
addressLine.text = parsed.address;
setPaymentId(parsed.payment_id);
amountLine.text = parsed.amount;
setDescription(parsed.tx_description);
}
} }
MoneroComponents.InlineButton { MoneroComponents.InlineButton {
@ -220,15 +289,6 @@ Rectangle {
} }
} }
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily
text: FontAwesome.addressBook
onClicked: {
middlePanel.addressBookView.selectAndSend = true;
appWindow.showPageRequest("AddressBook");
}
}
MoneroComponents.InlineButton { MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily fontFamily: FontAwesome.fontFamily
text: FontAwesome.qrcode text: FontAwesome.qrcode
@ -238,17 +298,94 @@ Rectangle {
cameraUi.qrcode_decoded.connect(updateFromQrCode) cameraUi.qrcode_decoded.connect(updateFromQrCode)
} }
} }
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily
text: FontAwesome.addressBook
onClicked: {
middlePanel.addressBookView.selectAndSend = true;
appWindow.showPageRequest("AddressBook");
} }
} }
StandardButton { Item {
id: resolveButton Layout.fillWidth: true
width: 80 }
}
MoneroComponents.TextPlain {
Layout.preferredWidth: recipientLayout.secondRowWidth
font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 16
color: MoneroComponents.Style.defaultFontColor
text: qsTr("Amount") + translationManager.emptyString
}
Item {
Layout.preferredWidth: recipientLayout.thirdRowWidth
}
}
Repeater {
id: recipientRepeater
model: recipientModel
ColumnLayout {
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.rightMargin: recipientLayout.thirdRowWidth
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 ? recipientLayout.rowSpacing / 2 : 0
Layout.bottomMargin: recipientLayout.rowSpacing / 2
Layout.fillWidth: true
addressValidation: true
borderDisabled: true
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.. / OpenAlias";
} else if (persistentSettings.nettype == NetworkType.STAGENET){
return "5.. / 7..";
} else if(persistentSettings.nettype == NetworkType.TESTNET){
return "9.. / B..";
}
}
onTextChanged: {
const parsed = walletManager.parse_uri_to_object(text);
if (!parsed.error) {
fillPaymentDetails(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description);
}
address = text;
}
text: address
MoneroComponents.InlineButton {
small: true
text: qsTr("Resolve") + translationManager.emptyString text: qsTr("Resolve") + translationManager.emptyString
visible: TxUtils.isValidOpenAliasAddress(addressLine.text) visible: TxUtils.isValidOpenAliasAddress(address)
enabled : visible
onClicked: { onClicked: {
var result = walletManager.resolveOpenAlias(addressLine.text) var result = walletManager.resolveOpenAlias(address)
if (result) { if (result) {
var parts = result.split("|") var parts = result.split("|")
if (parts.length == 2) { if (parts.length == 2) {
@ -256,16 +393,16 @@ Rectangle {
if (parts[0] === "true") { if (parts[0] === "true") {
if (address_ok) { if (address_ok) {
// prepend openalias to description // prepend openalias to description
descriptionLine.text = descriptionLine.text ? addressLine.text + " " + descriptionLine.text : addressLine.text descriptionLine.text = descriptionLine.text ? address + " " + descriptionLine.text : address
descriptionCheckbox.checked = true descriptionCheckbox.checked = true
addressLine.text = parts[1] recipientRepeater.itemAt(index).children[1].children[0].text = parts[1];
} }
else else
oa_message(qsTr("No valid address found at this OpenAlias address")) oa_message(qsTr("No valid address found at this OpenAlias address"))
} }
else if (parts[0] === "false") { else if (parts[0] === "false") {
if (address_ok) { if (address_ok) {
addressLine.text = parts[1] 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")) oa_message(qsTr("Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed"))
} }
else else
@ -286,112 +423,200 @@ Rectangle {
} }
} }
} }
GridLayout {
columns: appWindow.walletMode < 2 ? 1 : 2
Layout.fillWidth: true
columnSpacing: 32
ColumnLayout {
Layout.fillWidth: true
Layout.minimumWidth: 200
// Amount input
LineEdit {
id: amountLine
KeyNavigation.tab: sendButton
Layout.fillWidth: true
inlineIcon: true
labelText: "<style type='text/css'>a {text-decoration: none; color: #858585; font-size: 14px;}</style>\
%1 <a href='#'>(%2)</a>".arg(qsTr("Amount")).arg(qsTr("Change account"))
+ translationManager.emptyString
copyButton: !isNaN(amountLine.text) && persistentSettings.fiatPriceEnabled
copyButtonText: "~%1 %2".arg(fiatApiConvertToFiat(amountLine.text)).arg(fiatApiCurrencySymbol())
copyButtonEnabled: false
onLabelLinkActivated: {
middlePanel.accountView.selectAndSend = true;
appWindow.showPageRequest("Account")
} }
Rectangle {
Layout.fillHeight: true
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: recipientLayout.rowSpacing / 2
Layout.bottomMargin: recipientLayout.rowSpacing / 2
Layout.rightMargin: recipientLayout.colSpacing / 2
Layout.preferredWidth: 125
borderDisabled: true
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputPadding: 0
placeholderFontFamily: MoneroComponents.Style.fontMonoRegular.name
placeholderFontSize: 14
placeholderLeftMargin: 0
placeholderText: "0.00" placeholderText: "0.00"
width: 100 text: amount
fontBold: true
onTextChanged: { onTextChanged: {
amountLine.text = amountLine.text.trim().replace(",", "."); text = text.trim().replace(",", ".");
const match = amountLine.text.match(/^0+(\d.*)/); const match = text.match(/^0+(\d.*)/);
if (match) { if (match) {
const cursorPosition = amountLine.cursorPosition; const cursorPosition = cursorPosition;
amountLine.text = match[1]; text = match[1];
amountLine.cursorPosition = Math.max(cursorPosition, 1) - 1; cursorPosition = Math.max(cursorPosition, 1) - 1;
} else if(amountLine.text.indexOf('.') === 0){ } else if(text.indexOf('.') === 0){
amountLine.text = '0' + amountLine.text; text = '0' + text;
if (amountLine.text.length > 2) { if (text.length > 2) {
amountLine.cursorPosition = 1; cursorPosition = 1;
} }
} }
amountLine.error = walletManager.amountFromString(amountLine.text) > appWindow.getUnlockedBalance() error = walletManager.amountFromString(text) > appWindow.getUnlockedBalance();
amount = text;
} }
validator: RegExpValidator { validator: RegExpValidator {
regExp: /^\s*(\d{1,8})?([\.,]\d{1,12})?\s*$/ regExp: /^\s*(\d{1,8})?([\.,]\d{1,12})?\s*$/
} }
}
MoneroComponents.InlineButton { MoneroComponents.TextPlain {
text: qsTr("All") + translationManager.emptyString Layout.leftMargin: recipientLayout.colSpacing / 2
onClicked: amountLine.text = "(all)" Layout.preferredWidth: recipientLayout.thirdRowWidth
font.family: FontAwesome.fontFamilySolid
font.styleName: "Solid"
horizontalAlignment: Text.AlignHCenter
opacity: mouseArea.containsMouse ? 1 : 0.85
text: recipientModel.count == 1 ? FontAwesome.infinity : FontAwesome.times
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (recipientModel.count == 1) {
parent.parent.children[2].text = "(all)";
} else {
recipientModel.remove(index);
}
}
}
}
}
}
}
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
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 { MoneroComponents.TextPlain {
id: feeLabel Layout.fillWidth: true
Layout.alignment: Qt.AlignRight horizontalAlignment: Text.AlignRight
Layout.topMargin: 12
font.family: MoneroComponents.Style.fontRegular.name font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 14 font.pixelSize: 16
color: MoneroComponents.Style.defaultFontColor text: recipientModel.count > 1 ? qsTr("Total") + translationManager.emptyString : ""
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;
}
currentWallet.estimateTransactionFeeAsync(
[addressLine.text],
[walletManager.amountFromString(amountLine.text)],
priorityModelV5.get(priorityDropdown.currentIndex).priority,
function (amount) {
estimatedFee = Utils.removeTrailingZeros(amount);
estimating = false;
});
}
text: {
if (!sendButton.enabled || estimatedFee == null) {
return ""
}
return "%1: ~%2 XMR".arg(qsTr("Fee")).arg(estimatedFee) +
estimatedFeeFiat +
translationManager.emptyString;
} }
BusyIndicator { MoneroComponents.LineEdit {
anchors.right: parent.right id: totalValue
running: feeLabel.estimating Layout.column: 1
height: parent.height Layout.row: 0
Layout.preferredWidth: recipientLayout.secondRowWidth
borderDisabled: true
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputHeight: 30
inputPadding: 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
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
borderDisabled: true
fontFamily: MoneroComponents.Style.fontMonoRegular.name
fontSize: 14
inputHeight: 30
inputPadding: 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
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 { ColumnLayout {
spacing: 0
visible: appWindow.walletMode >= 2 visible: appWindow.walletMode >= 2
Layout.alignment: Qt.AlignTop
Label { Label {
id: transactionPriority id: transactionPriority
Layout.topMargin: 0 Layout.topMargin: 0
@ -417,13 +642,73 @@ Rectangle {
ListElement { column1: qsTr("Fastest (x200 fee)") ; column2: ""; priority: 4 } ListElement { column1: qsTr("Fastest (x200 fee)") ; column2: ""; priority: 4 }
} }
RowLayout {
Layout.topMargin: 5
spacing: 10
StandardDropdown { StandardDropdown {
Layout.preferredWidth: 200 Layout.preferredWidth: 200
id: priorityDropdown id: priorityDropdown
Layout.topMargin: 5
currentIndex: 0 currentIndex: 0
dataModel: priorityModelV5 dataModel: priorityModelV5
} }
MoneroComponents.TextPlain {
id: feeLabel
Layout.alignment: Qt.AlignVCenter
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
}
}
} }
} }
@ -513,26 +798,25 @@ Rectangle {
RowLayout { RowLayout {
StandardButton { StandardButton {
id: sendButton id: sendButton
KeyNavigation.tab: addressLine
rightIcon: "qrc:///images/rightArrow.png" rightIcon: "qrc:///images/rightArrow.png"
rightIconInactive: "qrc:///images/rightArrowInactive.png" rightIconInactive: "qrc:///images/rightArrowInactive.png"
Layout.topMargin: 4 Layout.topMargin: 4
text: qsTr("Send") + translationManager.emptyString text: qsTr("Send") + translationManager.emptyString
enabled: !sendButtonWarningBox.visible && !warningContent && addressLine.text && !paymentIdWarningBox.visible enabled: !sendButtonWarningBox.visible && !warningContent && !recipientModel.hasEmptyAddress() && !paymentIdWarningBox.visible
onClicked: { onClicked: {
console.log("Transfer: paymentClicked") console.log("Transfer: paymentClicked")
var priority = priorityModelV5.get(priorityDropdown.currentIndex).priority var priority = priorityModelV5.get(priorityDropdown.currentIndex).priority
console.log("priority: " + priority) console.log("priority: " + priority)
console.log("amount: " + amountLine.text)
addressLine.text = addressLine.text.trim()
setPaymentId(paymentIdLine.text.trim()); setPaymentId(paymentIdLine.text.trim());
root.paymentClicked(getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text) root.paymentClicked(recipientModel.getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
} }
} }
} }
function checkInformation(amount, address, nettype) { function checkInformation() {
return amount.length > 0 && walletManager.amountFromString(amountLine.text) <= appWindow.getUnlockedBalance() && TxUtils.checkAddress(address, nettype) return !recipientModel.hasEmptyAmount() &&
recipientModel.getAmountTotal() <= appWindow.getUnlockedBalance() &&
!recipientModel.hasInvalidAddress();
} }
} // pageRoot } // pageRoot
@ -592,15 +876,13 @@ Rectangle {
visible: persistentSettings.transferShowAdvanced && appWindow.walletMode >= 2 visible: persistentSettings.transferShowAdvanced && appWindow.walletMode >= 2
title: qsTr("Offline transaction signing") + translationManager.emptyString title: qsTr("Offline transaction signing") + translationManager.emptyString
button1.text: qsTr("Create") + translationManager.emptyString button1.text: qsTr("Create") + translationManager.emptyString
button1.enabled: appWindow.viewOnly && pageRoot.checkInformation(amountLine.text, addressLine.text, appWindow.persistentSettings.nettype) button1.enabled: appWindow.viewOnly && pageRoot.checkInformation()
button1.onClicked: { button1.onClicked: {
console.log("Transfer: saveTx Clicked") console.log("Transfer: saveTx Clicked")
var priority = priorityModelV5.get(priorityDropdown.currentIndex).priority var priority = priorityModelV5.get(priorityDropdown.currentIndex).priority
console.log("priority: " + priority) console.log("priority: " + priority)
console.log("amount: " + amountLine.text)
addressLine.text = addressLine.text.trim()
setPaymentId(paymentIdLine.text.trim()); setPaymentId(paymentIdLine.text.trim());
root.paymentClicked(getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text) root.paymentClicked(recipientModel.getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text)
} }
button2.text: qsTr("Sign (offline)") + translationManager.emptyString button2.text: qsTr("Sign (offline)") + translationManager.emptyString
button2.enabled: !appWindow.viewOnly button2.enabled: !appWindow.viewOnly
@ -617,7 +899,7 @@ Rectangle {
helpTextLarge.text: qsTr("Spend XMR from a cold (offline) wallet") + translationManager.emptyString helpTextLarge.text: qsTr("Spend XMR from a cold (offline) wallet") + translationManager.emptyString
helpTextSmall.text: { helpTextSmall.text: {
var errorMessage = ""; var errorMessage = "";
if (appWindow.viewOnly && !pageRoot.checkInformation(amountLine.text, addressLine.text, appWindow.persistentSettings.nettype)){ if (appWindow.viewOnly && !pageRoot.checkInformation()) {
errorMessage = "<p class='orange'>" + qsTr("* To create a transaction file, please enter address and amount above") + "</p>"; errorMessage = "<p class='orange'>" + qsTr("* To create a transaction file, please enter address and amount above") + "</p>";
} }
return "<style type='text/css'>p{line-height:20px; margin-top:0px; margin-bottom:0px; color:" + MoneroComponents.Style.defaultFontColor + return "<style type='text/css'>p{line-height:20px; margin-top:0px; margin-bottom:0px; color:" + MoneroComponents.Style.defaultFontColor +
@ -821,16 +1103,6 @@ Rectangle {
function sendTo(address, paymentId, description, amount) { function sendTo(address, paymentId, description, amount) {
middlePanel.state = 'Transfer'; middlePanel.state = 'Transfer';
if(typeof address !== 'undefined') fillPaymentDetails(address, paymentId, amount, description);
addressLine.text = address
if(typeof paymentId !== 'undefined')
setPaymentId(paymentId);
if(typeof description !== 'undefined')
setDescription(description);
if(typeof amount !== 'undefined')
amountLine.text = amount;
} }
} }