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,184 +170,496 @@ Rectangle {
} }
} }
// recipient address input ListModel {
RowLayout { id: recipientModel
id: addressLineRow
Layout.fillWidth: true
LineEditMulti { readonly property int maxRecipients: 16
id: addressLine
KeyNavigation.tab: amountLine
spacing: 0
fontBold: true
labelText: 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 { ListElement {
fontFamily: FontAwesome.fontFamily address: ""
fontPixelSize: 18 amount: ""
text: FontAwesome.desktop }
onClicked: {
clearFields(); function newRecipient(address, amount) {
const codes = oshelper.grabQrCodesFromScreen(); if (recipientModel.count < maxRecipients) {
for (var index = 0; index < codes.length; ++index) { recipientModel.append({address: address, amount: amount});
const parsed = walletManager.parse_uri_to_object(codes[index]); return true;
if (!parsed.error) { }
fillPaymentDetails(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description, parsed.recipient_name); return false;
break; }
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 {
Layout.leftMargin: 10
font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 16
color: MoneroComponents.Style.defaultFontColor
text: qsTr("Address") + translationManager.emptyString
}
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily
fontPixelSize: 18
text: FontAwesome.desktop
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;
}
}
}
}
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily
text: FontAwesome.qrcode
visible: appWindow.qrScannerEnabled
onClicked: {
cameraUi.state = "Capture"
cameraUi.qrcode_decoded.connect(updateFromQrCode)
}
}
MoneroComponents.InlineButton {
fontFamily: FontAwesome.fontFamily
text: FontAwesome.addressBook
onClicked: {
middlePanel.addressBookView.selectAndSend = true;
appWindow.showPageRequest("AddressBook");
}
}
Item {
Layout.fillWidth: true
}
}
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
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.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"
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: 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);
}
}
}
} }
} }
} }
} }
MoneroComponents.InlineButton { GridLayout {
fontFamily: FontAwesome.fontFamily id: totalLayout
text: FontAwesome.addressBook Layout.topMargin: recipientLayout.rowSpacing / 2
onClicked: { Layout.fillWidth: true
middlePanel.addressBookView.selectAndSend = true; columns: 3
appWindow.showPageRequest("AddressBook"); columnSpacing: recipientLayout.colSpacing
} rowSpacing: 0
}
MoneroComponents.InlineButton { RowLayout {
fontFamily: FontAwesome.fontFamily Layout.column: 0
text: FontAwesome.qrcode Layout.row: 0
visible: appWindow.qrScannerEnabled Layout.fillWidth: true
onClicked: { spacing: 0
cameraUi.state = "Capture"
cameraUi.qrcode_decoded.connect(updateFromQrCode)
}
}
}
}
StandardButton { CheckBox {
id: resolveButton border: false
width: 80 checked: false
text: qsTr("Resolve") + translationManager.emptyString enabled: {
visible: TxUtils.isValidOpenAliasAddress(addressLine.text) if (recipientModel.count > 0 && recipientModel.get(0).amount == "(all)") {
enabled : visible return false;
onClicked: { }
var result = walletManager.resolveOpenAlias(addressLine.text) if (recipientModel.count >= recipientModel.maxRecipients) {
if (result) { return false;
var parts = result.split("|") }
if (parts.length == 2) { return true;
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 ? addressLine.text + " " + descriptionLine.text : addressLine.text
descriptionCheckbox.checked = true
addressLine.text = parts[1]
}
else
oa_message(qsTr("No valid address found at this OpenAlias address"))
}
else if (parts[0] === "false") {
if (address_ok) {
addressLine.text = parts[1]
oa_message(qsTr("Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed"))
} }
else fontAwesomeIcons: true
{ fontSize: descriptionLine.labelFontSize
oa_message(qsTr("No valid address found at this OpenAlias address, but the DNSSEC signatures could not be verified, so this may be spoofed")) iconOnTheLeft: true
} text: qsTr("Add recipient") + translationManager.emptyString
} toggleOnClick: false
else { uncheckedIcon: FontAwesome.plusCircle
oa_message(qsTr("Internal error")) onClicked: {
} recipientModel.newRecipient("", "");
}
else {
oa_message(qsTr("Internal error"))
}
}
else {
oa_message(qsTr("No address found"))
}
}
}
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")
}
placeholderText: "0.00"
width: 100
fontBold: true
onTextChanged: {
amountLine.text = amountLine.text.trim().replace(",", ".");
const match = amountLine.text.match(/^0+(\d.*)/);
if (match) {
const cursorPosition = amountLine.cursorPosition;
amountLine.text = match[1];
amountLine.cursorPosition = Math.max(cursorPosition, 1) - 1;
} else if(amountLine.text.indexOf('.') === 0){
amountLine.text = '0' + amountLine.text;
if (amountLine.text.length > 2) {
amountLine.cursorPosition = 1;
} }
} }
amountLine.error = walletManager.amountFromString(amountLine.text) > appWindow.getUnlockedBalance()
}
validator: RegExpValidator {
regExp: /^\s*(\d{1,8})?([\.,]\d{1,12})?\s*$/
}
MoneroComponents.InlineButton { MoneroComponents.TextPlain {
text: qsTr("All") + translationManager.emptyString Layout.fillWidth: true
onClicked: amountLine.text = "(all)" 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
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 {
spacing: 0
visible: appWindow.walletMode >= 2
Label {
id: transactionPriority
Layout.topMargin: 0
text: qsTr("Transaction priority") + translationManager.emptyString
fontBold: false
fontSize: 16
}
// 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.preferredWidth: 200
id: priorityDropdown
currentIndex: 0
dataModel: priorityModelV5
}
MoneroComponents.TextPlain { MoneroComponents.TextPlain {
id: feeLabel id: feeLabel
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignVCenter
Layout.topMargin: 12
font.family: MoneroComponents.Style.fontRegular.name font.family: MoneroComponents.Style.fontRegular.name
font.pixelSize: 14 font.pixelSize: 14
color: MoneroComponents.Style.defaultFontColor color: MoneroComponents.Style.defaultFontColor
opacity: 0.7
property bool estimating: false property bool estimating: false
property var estimatedFee: null property var estimatedFee: null
property string estimatedFeeFiat: { property string estimatedFeeFiat: {
@ -363,12 +675,21 @@ Rectangle {
if (!sendButton.enabled || !currentWallet) { if (!sendButton.enabled || !currentWallet) {
return; 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( currentWallet.estimateTransactionFeeAsync(
[addressLine.text], addresses,
[walletManager.amountFromString(amountLine.text)], amounts,
priorityModelV5.get(priorityDropdown.currentIndex).priority, priorityModelV5.get(priorityDropdown.currentIndex).priority,
function (amount) { function (amount) {
estimatedFee = Utils.removeTrailingZeros(amount); if (amount) {
estimatedFee = Utils.removeTrailingZeros(amount);
}
estimating = false; estimating = false;
}); });
} }
@ -376,56 +697,20 @@ Rectangle {
if (!sendButton.enabled || estimatedFee == null) { if (!sendButton.enabled || estimatedFee == null) {
return "" return ""
} }
return "%1: ~%2 XMR".arg(qsTr("Fee")).arg(estimatedFee) + return "~%1 XMR%2 %3".arg(estimatedFee)
estimatedFeeFiat + .arg(estimatedFeeFiat)
translationManager.emptyString; .arg(qsTr("fee") + translationManager.emptyString);
} }
BusyIndicator { BusyIndicator {
anchors.right: parent.right anchors.left: parent.left
running: feeLabel.estimating running: feeLabel.estimating
height: parent.height height: parent.height
width: height
} }
} }
} }
}
ColumnLayout {
visible: appWindow.walletMode >= 2
Layout.alignment: Qt.AlignTop
Label {
id: transactionPriority
Layout.topMargin: 0
text: qsTr("Transaction priority") + translationManager.emptyString
fontBold: false
fontSize: 16
}
// 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 }
}
StandardDropdown {
Layout.preferredWidth: 200
id: priorityDropdown
Layout.topMargin: 5
currentIndex: 0
dataModel: priorityModelV5
}
}
}
MoneroComponents.WarningBox { MoneroComponents.WarningBox {
text: qsTr("Description field contents match long payment ID format. \ text: qsTr("Description field contents match long payment ID format. \
@ -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 +
@ -818,19 +1100,9 @@ Rectangle {
} }
// Popuplate fields from addressbook. // Popuplate fields from addressbook.
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;
} }
} }