mirror of
https://github.com/monero-project/monero-gui.git
synced 2025-01-22 02:34:36 +00:00
Transfer: implement sending to multiple recipients
This commit is contained in:
parent
34df4e74d4
commit
a0a9d9e31e
4 changed files with 520 additions and 247 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue