diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include
index 306dbe2aa6..b37cad3144 100644
--- a/src/Makefile.qt.include
+++ b/src/Makefile.qt.include
@@ -44,7 +44,9 @@ QT_MOC_CPP = \
qml/models/moc_nodemodel.cpp \
qml/models/moc_options_model.cpp \
qml/models/moc_peerdetailsmodel.cpp \
- qml/models/moc_peerlistsortproxy.cpp \
+ qml/models/moc_peerlistsortproxy.cpp \\
+ qml/models/moc_sendrecipient.cpp \
+ qml/models/moc_sendrecipientslistmodel.cpp \
qml/models/moc_transaction.cpp \
qml/models/moc_sendrecipient.cpp \
qml/models/moc_walletlistmodel.cpp \
@@ -138,6 +140,7 @@ BITCOIN_QT_H = \
qml/models/peerlistsortproxy.h \
qml/models/transaction.h \
qml/models/sendrecipient.h \
+ qml/models/sendrecipientslistmodel.h \
qml/models/walletlistmodel.h \
qml/models/walletqmlmodel.h \
qml/models/walletqmlmodeltransaction.h \
@@ -339,6 +342,7 @@ BITCOIN_QML_BASE_CPP = \
qml/models/peerlistsortproxy.cpp \
qml/models/transaction.cpp \
qml/models/sendrecipient.cpp \
+ qml/models/sendrecipientslistmodel.cpp \
qml/models/walletlistmodel.cpp \
qml/models/walletqmlmodel.cpp \
qml/models/walletqmlmodeltransaction.cpp \
@@ -353,6 +357,7 @@ QML_RES_FONTS = \
QML_RES_ICONS = \
qml/res/icons/add-wallet-dark.png \
+ qml/res/icons/alert-filled.png \
qml/res/icons/arrow-down.png \
qml/res/icons/arrow-up.png \
qml/res/icons/bitcoin-circle.png \
@@ -379,6 +384,7 @@ QML_RES_ICONS = \
qml/res/icons/network-dark.png \
qml/res/icons/network-light.png \
qml/res/icons/plus.png \
+ qml/res/icons/plus-big-filled.png \
qml/res/icons/pending.png \
qml/res/icons/shutdown.png \
qml/res/icons/singlesig-wallet.png \
@@ -420,12 +426,12 @@ QML_RES_QML = \
qml/controls/CoreCheckBox.qml \
qml/controls/CoreText.qml \
qml/controls/CoreTextField.qml \
- qml/controls/EllipsisMenuButton.qml \
qml/controls/EllipsisMenuToggleItem.qml \
qml/controls/ExternalLink.qml \
qml/controls/FocusBorder.qml \
qml/controls/Header.qml \
qml/controls/Icon.qml \
+ qml/controls/IconButton.qml \
qml/controls/InformationPage.qml \
qml/controls/IPAddressValueInput.qml \
qml/controls/KeyValueRow.qml \
@@ -484,6 +490,7 @@ QML_RES_QML = \
qml/pages/wallet/CreatePassword.qml \
qml/pages/wallet/CreateWalletWizard.qml \
qml/pages/wallet/DesktopWallets.qml \
+ qml/pages/wallet/MultipleSendReview.qml \
qml/pages/wallet/RequestPayment.qml \
qml/pages/wallet/Send.qml \
qml/pages/wallet/SendResult.qml \
diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc
index 8557ebe0e6..a4c5b03d01 100644
--- a/src/qml/bitcoin_qml.qrc
+++ b/src/qml/bitcoin_qml.qrc
@@ -30,12 +30,12 @@
controls/FocusBorder.qml
controls/Header.qml
controls/Icon.qml
+ controls/IconButton.qml
controls/InformationPage.qml
controls/IPAddressValueInput.qml
controls/KeyValueRow.qml
controls/LabeledTextInput.qml
controls/LabeledCoinControlButton.qml
- controls/EllipsisMenuButton.qml
controls/EllipsisMenuToggleItem.qml
controls/NavButton.qml
controls/NavigationBar.qml
@@ -90,6 +90,7 @@
pages/wallet/CreatePassword.qml
pages/wallet/CreateWalletWizard.qml
pages/wallet/DesktopWallets.qml
+ pages/wallet/MultipleSendReview.qml
pages/wallet/RequestPayment.qml
pages/wallet/Send.qml
pages/wallet/SendResult.qml
@@ -99,6 +100,7 @@
res/icons/add-wallet-dark.png
+ res/icons/alert-filled.png
res/icons/arrow-down.png
res/icons/arrow-up.png
res/icons/bitcoin-circle.png
@@ -126,6 +128,7 @@
res/icons/network-dark.png
res/icons/network-light.png
res/icons/plus.png
+ res/icons/plus-big-filled.png
res/icons/pending.png
res/icons/shutdown.png
res/icons/singlesig-wallet.png
diff --git a/src/qml/bitcoinamount.cpp b/src/qml/bitcoinamount.cpp
index 153d8fabae..3cc31605d8 100644
--- a/src/qml/bitcoinamount.cpp
+++ b/src/qml/bitcoinamount.cpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 The Bitcoin Core developers
+// Copyright (c) 2024-2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
@@ -7,19 +7,9 @@
#include
#include
-
-BitcoinAmount::BitcoinAmount(QObject *parent) : QObject(parent)
+BitcoinAmount::BitcoinAmount(QObject* parent)
+ : QObject(parent)
{
- m_unit = Unit::BTC;
-}
-
-int BitcoinAmount::decimals(Unit unit)
-{
- switch (unit) {
- case Unit::BTC: return 8;
- case Unit::SAT: return 0;
- } // no default case, so the compiler can warn about missing cases
- assert(false);
}
QString BitcoinAmount::sanitize(const QString &text)
@@ -43,6 +33,30 @@ QString BitcoinAmount::sanitize(const QString &text)
return result;
}
+qint64 BitcoinAmount::satoshi() const
+{
+ return m_satoshi;
+}
+
+void BitcoinAmount::setSatoshi(qint64 new_amount)
+{
+ m_isSet = true;
+ if (m_satoshi != new_amount) {
+ m_satoshi = new_amount;
+ Q_EMIT amountChanged();
+ }
+}
+
+void BitcoinAmount::clear()
+{
+ if (!m_isSet && m_satoshi == 0) {
+ return;
+ }
+ m_satoshi = 0;
+ m_isSet = false;
+ Q_EMIT amountChanged();
+}
+
BitcoinAmount::Unit BitcoinAmount::unit() const
{
return m_unit;
@@ -52,103 +66,94 @@ void BitcoinAmount::setUnit(const Unit unit)
{
m_unit = unit;
Q_EMIT unitChanged();
+ Q_EMIT displayChanged();
}
QString BitcoinAmount::unitLabel() const
{
switch (m_unit) {
case Unit::BTC: return "₿";
- case Unit::SAT: return "Sat";
+ case Unit::SAT: return "sat";
}
assert(false);
}
-QString BitcoinAmount::amount() const
+void BitcoinAmount::flipUnit()
{
- return m_amount;
+ if (m_unit == Unit::BTC) {
+ m_unit = Unit::SAT;
+ } else {
+ m_unit = Unit::BTC;
+ }
+ Q_EMIT unitChanged();
+ Q_EMIT displayChanged();
}
-QString BitcoinAmount::satoshiAmount() const
+QString BitcoinAmount::satsToBtcString(qint64 sat)
{
- return toSatoshis(m_amount);
-}
+ const bool negative = sat < 0;
+ qint64 absSat = negative ? -sat : sat;
-void BitcoinAmount::setAmount(const QString& new_amount)
-{
- m_amount = sanitize(new_amount);
- Q_EMIT amountChanged();
+ const qint64 wholePart = absSat / COIN;
+ const qint64 fracInt = absSat % COIN;
+ QString fracPart = QString("%1").arg(fracInt, 8, 10, QLatin1Char('0'));
+
+ QString result = QString::number(wholePart) + '.' + fracPart;
+ if (negative) {
+ result.prepend('-');
+ }
+ return result;
}
-QString BitcoinAmount::toSatoshis(const QString& text) const
+QString BitcoinAmount::toDisplay() const
{
+ if (!m_isSet) {
+ return "";
+ }
if (m_unit == Unit::SAT) {
- return text;
+ return QString::number(m_satoshi);
} else {
- return convert(text, m_unit);
+ return satsToBtcString(m_satoshi);
}
}
-long long BitcoinAmount::toSatoshis(QString& amount, const Unit unit)
+qint64 BitcoinAmount::btcToSats(const QString& btcSanitized)
{
- int num_decimals = decimals(unit);
+ if (btcSanitized.isEmpty() || btcSanitized == ".") return 0;
- QStringList parts = amount.remove(' ').split(".");
+ QString cleaned = btcSanitized;
+ if (cleaned.startsWith('.')) cleaned.prepend('0');
- QString whole = parts[0];
- QString decimals;
-
- if(parts.size() > 1)
- {
- decimals = parts[1];
+ QStringList parts = cleaned.split('.');
+ const qint64 whole = parts[0].isEmpty() ? 0 : parts[0].toLongLong();
+ qint64 frac = 0;
+ if (parts.size() == 2) {
+ frac = parts[1].leftJustified(8, '0').toLongLong();
}
- QString str = whole + decimals.leftJustified(num_decimals, '0', true);
- return str.toLongLong();
+ return whole * COIN + frac;
}
-QString BitcoinAmount::convert(const QString& amount, Unit unit) const
+void BitcoinAmount::fromDisplay(const QString& text)
{
- if (amount == "") {
- return amount;
- }
-
- QString result = amount;
- int decimalPosition = result.indexOf(".");
-
- if (decimalPosition == -1) {
- decimalPosition = result.length();
- result.append(".");
+ if (text.trimmed().isEmpty()) {
+ clear();
+ return;
}
- if (unit == Unit::BTC) {
- int numDigitsAfterDecimal = result.length() - decimalPosition - 1;
- if (numDigitsAfterDecimal < 8) {
- result.append(QString(8 - numDigitsAfterDecimal, '0'));
- }
- result.remove(decimalPosition, 1);
-
- while (result.startsWith('0') && result.length() > 1) {
- result.remove(0, 1);
- }
- } else if (unit == Unit::SAT) {
- result.remove(decimalPosition, 1);
- int newDecimalPosition = decimalPosition - 8;
- if (newDecimalPosition < 1) {
- result = QString("0").repeated(-newDecimalPosition) + result;
- newDecimalPosition = 0;
- }
- result.insert(newDecimalPosition, ".");
-
- while (result.endsWith('0') && result.contains('.')) {
- result.chop(1);
- }
- if (result.endsWith('.')) {
- result.chop(1);
- }
- if (result.startsWith('.')) {
- result.insert(0, "0");
- }
+ qint64 newSat = 0;
+ if (m_unit == Unit::BTC) {
+ QString sanitized = sanitize(text);
+ newSat = btcToSats(sanitized);
+ } else {
+ QString digitsOnly = text;
+ digitsOnly.remove(QRegExp("[^0-9]"));
+ newSat = digitsOnly.trimmed().isEmpty() ? 0 : digitsOnly.toLongLong();
}
+ setSatoshi(newSat);
+}
- return result;
+void BitcoinAmount::format()
+{
+ Q_EMIT displayChanged();
}
diff --git a/src/qml/bitcoinamount.h b/src/qml/bitcoinamount.h
index 0631a05b87..c451b34fd3 100644
--- a/src/qml/bitcoinamount.h
+++ b/src/qml/bitcoinamount.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 The Bitcoin Core developers
+// Copyright (c) 2024-2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
@@ -15,8 +15,8 @@ class BitcoinAmount : public QObject
Q_OBJECT
Q_PROPERTY(Unit unit READ unit WRITE setUnit NOTIFY unitChanged)
Q_PROPERTY(QString unitLabel READ unitLabel NOTIFY unitChanged)
- Q_PROPERTY(QString amount READ amount WRITE setAmount NOTIFY amountChanged)
- Q_PROPERTY(QString satoshiAmount READ satoshiAmount NOTIFY amountChanged)
+ Q_PROPERTY(QString display READ toDisplay WRITE fromDisplay NOTIFY displayChanged)
+ Q_PROPERTY(qint64 satoshi READ satoshi WRITE setSatoshi NOTIFY amountChanged)
public:
enum class Unit {
@@ -30,27 +30,34 @@ class BitcoinAmount : public QObject
Unit unit() const;
void setUnit(Unit unit);
QString unitLabel() const;
- QString amount() const;
- void setAmount(const QString& new_amount);
- QString satoshiAmount() const;
+
+ QString toDisplay() const;
+ void fromDisplay(const QString& new_amount);
+ qint64 satoshi() const;
+ void setSatoshi(qint64 new_amount);
+
+ bool isSet() const { return m_isSet; }
+
+ Q_INVOKABLE void format();
+
+ static QString satsToBtcString(qint64 sat);
public Q_SLOTS:
- QString sanitize(const QString& text);
- QString convert(const QString& text, Unit unit) const;
- QString toSatoshis(const QString& text) const;
+ void flipUnit();
+ void clear();
Q_SIGNALS:
void unitChanged();
- void unitLabelChanged();
void amountChanged();
+ void displayChanged();
private:
- long long toSatoshis(QString &amount, const Unit unit);
- int decimals(Unit unit);
+ QString sanitize(const QString& text);
+ static qint64 btcToSats(const QString& btc);
- Unit m_unit;
- QString m_unitLabel;
- QString m_amount;
+ qint64 m_satoshi{0};
+ bool m_isSet{false};
+ Unit m_unit{Unit::BTC};
};
#endif // BITCOIN_QML_BITCOINAMOUNT_H
diff --git a/src/qml/controls/EllipsisMenuButton.qml b/src/qml/controls/IconButton.qml
similarity index 51%
rename from src/qml/controls/EllipsisMenuButton.qml
rename to src/qml/controls/IconButton.qml
index 593ede0902..0cddb36cdf 100644
--- a/src/qml/controls/EllipsisMenuButton.qml
+++ b/src/qml/controls/IconButton.qml
@@ -11,12 +11,16 @@ import org.bitcoincore.qt 1.0
Button {
id: root
+ property color iconColor: Theme.color.orange
property color hoverColor: Theme.color.orange
property color activeColor: Theme.color.orange
+ property int size: 35
+ property alias iconSource: icon.source
hoverEnabled: AppMode.isDesktop
- implicitHeight: 35
- implicitWidth: 35
+ height: root.size
+ width: root.size
+ padding: 0
MouseArea {
anchors.fill: parent
@@ -25,28 +29,44 @@ Button {
cursorShape: Qt.PointingHandCursor
}
- background: null
+ background: Rectangle {
+ id: bg
+ anchors.fill: parent
+ radius: 5
+ color: Theme.color.background
+
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+ }
contentItem: Icon {
- id: ellipsisIcon
+ id: icon
anchors.fill: parent
source: "image://images/ellipsis"
- color: Theme.color.neutral9
- size: 35
+ size: root.size
+ color: iconColor
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
}
states: [
State {
name: "CHECKED"; when: root.checked
- PropertyChanges { target: ellipsisIcon; color: activeColor }
+ PropertyChanges { target: icon; color: activeColor }
},
State {
name: "HOVER"; when: root.hovered
- PropertyChanges { target: ellipsisIcon; color: hoverColor }
+ PropertyChanges { target: icon; color: hoverColor }
+ PropertyChanges { target: bg; color: Theme.color.neutral2 }
},
State {
name: "DISABLED"; when: !root.enabled
- PropertyChanges { target: ellipsisIcon; color: Theme.color.neutral4 }
+ PropertyChanges { target: icon; color: Theme.color.neutral2 }
+ PropertyChanges { target: bg; color: Theme.color.background }
}
]
}
diff --git a/src/qml/controls/LabeledTextInput.qml b/src/qml/controls/LabeledTextInput.qml
index c257ec9a7d..419c98d422 100644
--- a/src/qml/controls/LabeledTextInput.qml
+++ b/src/qml/controls/LabeledTextInput.qml
@@ -13,6 +13,8 @@ Item {
property alias iconSource: icon.source
property alias customIcon: iconContainer.data
property alias enabled: input.enabled
+ property alias validator: input.validator
+ property alias maximumLength: input.maximumLength
signal iconClicked
signal textEdited
diff --git a/src/qml/controls/NavButton.qml b/src/qml/controls/NavButton.qml
index 965161b983..37e4114a03 100644
--- a/src/qml/controls/NavButton.qml
+++ b/src/qml/controls/NavButton.qml
@@ -53,6 +53,7 @@ AbstractButton {
}
contentItem: RowLayout {
spacing: 0
+ anchors.fill: parent
Loader {
id: button_background
active: root.iconSource.toString().length > 0
diff --git a/src/qml/controls/SendOptionsPopup.qml b/src/qml/controls/SendOptionsPopup.qml
index f67ff139ec..b96ffc5ec8 100644
--- a/src/qml/controls/SendOptionsPopup.qml
+++ b/src/qml/controls/SendOptionsPopup.qml
@@ -13,14 +13,36 @@ OptionPopup {
id: root
property alias coinControlEnabled: coinControlToggle.checked
+ property alias multipleRecipientsEnabled: multipleRecipientsToggle.checked
+
+ implicitWidth: 300
+ implicitHeight: 100
clip: true
modal: true
dim: false
- EllipsisMenuToggleItem {
- id: coinControlToggle
+ ColumnLayout {
+ id: columnLayout
anchors.centerIn: parent
- text: qsTr("Enable Coin control")
+ anchors.margins: 10
+ spacing: 0
+
+ EllipsisMenuToggleItem {
+ id: coinControlToggle
+ Layout.fillWidth: true
+ text: qsTr("Enable Coin control")
+ }
+
+ Separator {
+ id: separator
+ Layout.fillWidth: true
+ }
+
+ EllipsisMenuToggleItem {
+ id: multipleRecipientsToggle
+ Layout.fillWidth: true
+ text: qsTr("Multiple Recipients")
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/qml/models/coinslistmodel.cpp b/src/qml/models/coinslistmodel.cpp
index 76142e74f3..a34449111f 100644
--- a/src/qml/models/coinslistmodel.cpp
+++ b/src/qml/models/coinslistmodel.cpp
@@ -122,14 +122,14 @@ QString CoinsListModel::totalSelected() const
QString CoinsListModel::changeAmount() const
{
- CAmount change = m_total_amount - m_wallet_model->sendRecipient()->cAmount();
+ CAmount change = m_total_amount - m_wallet_model->sendRecipientList()->totalAmountSatoshi();
change = std::abs(change);
return BitcoinUnits::format(BitcoinUnits::Unit::BTC, change);
}
bool CoinsListModel::overRequiredAmount() const
{
- return m_total_amount > m_wallet_model->sendRecipient()->cAmount();
+ return m_total_amount > m_wallet_model->sendRecipientList()->totalAmountSatoshi();
}
int CoinsListModel::coinCount() const
diff --git a/src/qml/models/sendrecipient.cpp b/src/qml/models/sendrecipient.cpp
index 138bea6559..e98c9b9aff 100644
--- a/src/qml/models/sendrecipient.cpp
+++ b/src/qml/models/sendrecipient.cpp
@@ -3,11 +3,16 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include
-#include
-SendRecipient::SendRecipient(QObject* parent)
- : QObject(parent), m_address(""), m_label(""), m_amount(""), m_message("")
+#include
+#include
+
+#include
+
+SendRecipient::SendRecipient(WalletQmlModel* wallet, QObject* parent)
+ : QObject(parent), m_wallet(wallet), m_amount(new BitcoinAmount(this))
{
+ connect(m_amount, &BitcoinAmount::amountChanged, this, &SendRecipient::validateAmount);
}
QString SendRecipient::address() const
@@ -20,6 +25,20 @@ void SendRecipient::setAddress(const QString& address)
if (m_address != address) {
m_address = address;
Q_EMIT addressChanged();
+ validateAddress();
+ }
+}
+
+QString SendRecipient::addressError() const
+{
+ return m_addressError;
+}
+
+void SendRecipient::setAddressError(const QString& error)
+{
+ if (m_addressError != error) {
+ m_addressError = error;
+ Q_EMIT addressErrorChanged();
}
}
@@ -36,16 +55,21 @@ void SendRecipient::setLabel(const QString& label)
}
}
-QString SendRecipient::amount() const
+BitcoinAmount* SendRecipient::amount() const
{
return m_amount;
}
-void SendRecipient::setAmount(const QString& amount)
+QString SendRecipient::amountError() const
+{
+ return m_amountError;
+}
+
+void SendRecipient::setAmountError(const QString& error)
{
- if (m_amount != amount) {
- m_amount = amount;
- Q_EMIT amountChanged();
+ if (m_amountError != error) {
+ m_amountError = error;
+ Q_EMIT amountErrorChanged();
}
}
@@ -69,22 +93,57 @@ bool SendRecipient::subtractFeeFromAmount() const
CAmount SendRecipient::cAmount() const
{
- // TODO: Figure out who owns the parsing of SendRecipient::amount to CAmount
- if (m_amount == "") {
- return 0;
- }
- return m_amount.toLongLong();
+ return m_amount->satoshi();
}
void SendRecipient::clear()
{
m_address = "";
m_label = "";
- m_amount = "";
+ m_amount->setSatoshi(0);
m_message = "";
m_subtractFeeFromAmount = false;
Q_EMIT addressChanged();
Q_EMIT labelChanged();
- Q_EMIT amountChanged();
Q_EMIT messageChanged();
+ Q_EMIT amount()->amountChanged();
+}
+
+void SendRecipient::validateAddress()
+{
+ if (!m_address.isEmpty() && !IsValidDestinationString(m_address.toStdString())) {
+ if (IsValidDestinationString(m_address.toStdString(), *CChainParams::Main())) {
+ setAddressError(tr("Address is valid for mainnet, not the current network"));
+ } else if (IsValidDestinationString(m_address.toStdString(), *CChainParams::TestNet())) {
+ setAddressError(tr("Address is valid for testnet, not the current network"));
+ } else {
+ setAddressError(tr("Invalid address format"));
+ }
+ } else {
+ setAddressError("");
+ }
+
+ Q_EMIT isValidChanged();
+}
+
+void SendRecipient::validateAmount()
+{
+ if (m_amount->isSet()) {
+ if (m_amount->satoshi() <= 0) {
+ setAmountError(tr("Amount must be greater than zero"));
+ } else if (m_amount->satoshi() > MAX_MONEY) {
+ setAmountError(tr("Amount exceeds maximum limit of 21,000,000 BTC"));
+ } else if (m_amount->satoshi() > m_wallet->balanceSatoshi()) {
+ setAmountError(tr("Amount exceeds available balance"));
+ } else {
+ setAmountError("");
+ }
+ }
+
+ Q_EMIT isValidChanged();
+}
+
+bool SendRecipient::isValid() const
+{
+ return m_addressError.isEmpty() && m_amountError.isEmpty() && m_amount->satoshi() > 0 && !m_address.isEmpty();
}
diff --git a/src/qml/models/sendrecipient.h b/src/qml/models/sendrecipient.h
index 042e97c9de..972914a354 100644
--- a/src/qml/models/sendrecipient.h
+++ b/src/qml/models/sendrecipient.h
@@ -5,29 +5,40 @@
#ifndef BITCOIN_QML_MODELS_SENDRECIPIENT_H
#define BITCOIN_QML_MODELS_SENDRECIPIENT_H
+#include
+
#include
#include
-#include
+
+class WalletQmlModel;
class SendRecipient : public QObject
{
Q_OBJECT
Q_PROPERTY(QString address READ address WRITE setAddress NOTIFY addressChanged)
Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)
- Q_PROPERTY(QString amount READ amount WRITE setAmount NOTIFY amountChanged)
Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged)
+ Q_PROPERTY(BitcoinAmount* amount READ amount CONSTANT)
+
+ Q_PROPERTY(QString addressError READ addressError NOTIFY addressErrorChanged)
+ Q_PROPERTY(QString amountError READ amountError NOTIFY amountErrorChanged)
+ Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged)
public:
- explicit SendRecipient(QObject* parent = nullptr);
+ explicit SendRecipient(WalletQmlModel* wallet, QObject* parent = nullptr);
QString address() const;
void setAddress(const QString& address);
+ QString addressError() const;
+ void setAddressError(const QString& error);
QString label() const;
void setLabel(const QString& label);
- QString amount() const;
+ BitcoinAmount* amount() const;
void setAmount(const QString& amount);
+ QString amountError() const;
+ void setAmountError(const QString& error);
QString message() const;
void setMessage(const QString& message);
@@ -36,19 +47,29 @@ class SendRecipient : public QObject
bool subtractFeeFromAmount() const;
+ bool isValid() const;
+
Q_INVOKABLE void clear();
Q_SIGNALS:
void addressChanged();
+ void addressErrorChanged();
+ void amountErrorChanged();
void labelChanged();
- void amountChanged();
void messageChanged();
+ void isValidChanged();
private:
- QString m_address;
- QString m_label;
- QString m_amount;
- QString m_message;
+ void validateAddress();
+ void validateAmount();
+
+ const WalletQmlModel* m_wallet;
+ QString m_address{""};
+ QString m_addressError{""};
+ QString m_label{""};
+ QString m_message{""};
+ BitcoinAmount* m_amount;
+ QString m_amountError{""};
bool m_subtractFeeFromAmount{false};
};
diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp
new file mode 100644
index 0000000000..d32aa4c82c
--- /dev/null
+++ b/src/qml/models/sendrecipientslistmodel.cpp
@@ -0,0 +1,144 @@
+// Copyright (c) 2025 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+#include
+
+#include
+
+SendRecipientsListModel::SendRecipientsListModel(QObject* parent)
+ : QAbstractListModel(parent)
+{
+ m_wallet = qobject_cast(parent);
+ auto* recipient = new SendRecipient(m_wallet, this);
+ connect(recipient->amount(), &BitcoinAmount::amountChanged,
+ this, &SendRecipientsListModel::updateTotalAmount);
+ m_recipients.append(recipient);
+}
+
+int SendRecipientsListModel::rowCount(const QModelIndex&) const
+{
+ return m_recipients.size();
+}
+
+QVariant SendRecipientsListModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid() || index.row() >= m_recipients.size())
+ return {};
+
+ const auto& r = m_recipients[index.row()];
+ switch (role) {
+ case AddressRole: return r->address();
+ case LabelRole: return r->label();
+ case AmountRole: return r->amount()->toDisplay();
+ case MessageRole: return r->message();
+ default: return {};
+ }
+ return {};
+}
+
+QHash SendRecipientsListModel::roleNames() const
+{
+ return {
+ {AddressRole, "address"},
+ {LabelRole, "label"},
+ {AmountRole, "amount"},
+ {MessageRole, "message"},
+ };
+}
+
+void SendRecipientsListModel::add()
+{
+ const int row = m_recipients.size();
+ beginInsertRows(QModelIndex(), row, row);
+ auto* recipient = new SendRecipient(m_wallet, this);
+ connect(recipient->amount(), &BitcoinAmount::amountChanged,
+ this, &SendRecipientsListModel::updateTotalAmount);
+ m_recipients.append(recipient);
+ endInsertRows();
+ Q_EMIT countChanged();
+ setCurrentIndex(row);
+}
+
+void SendRecipientsListModel::setCurrentIndex(int row)
+{
+ if (row < 0 || row >= m_recipients.size())
+ return;
+
+ if (row == m_current)
+ return;
+
+ m_current = row;
+
+ Q_EMIT currentIndexChanged();
+ Q_EMIT currentRecipientChanged();
+}
+
+void SendRecipientsListModel::next()
+{
+ setCurrentIndex(m_current + 1);
+}
+
+void SendRecipientsListModel::prev()
+{
+ setCurrentIndex(m_current - 1);
+}
+
+void SendRecipientsListModel::remove()
+{
+ if (m_recipients.size() == 1) {
+ return;
+ }
+ beginRemoveRows(QModelIndex(), m_current, m_current);
+ delete m_recipients.takeAt(m_current);
+ endRemoveRows();
+ Q_EMIT countChanged();
+
+ setCurrentIndex(m_current - 1);
+}
+
+SendRecipient* SendRecipientsListModel::currentRecipient() const
+{
+ if (m_current < 0 || m_current >= m_recipients.size())
+ return nullptr;
+
+ return m_recipients[m_current];
+}
+
+void SendRecipientsListModel::updateTotalAmount()
+{
+ qint64 total = 0;
+ for (const auto& recipient : m_recipients) {
+ total += recipient->amount()->satoshi();
+ }
+ m_totalAmount = total;
+ Q_EMIT totalAmountChanged();
+}
+
+QString SendRecipientsListModel::totalAmount() const
+{
+ return BitcoinAmount::satsToBtcString(m_totalAmount);
+}
+
+void SendRecipientsListModel::clear()
+{
+ beginResetModel();
+ for (auto* recipient : m_recipients) {
+ delete recipient;
+ }
+ m_recipients.clear();
+ m_current = 0;
+ m_totalAmount = 0;
+
+ auto* recipient = new SendRecipient(m_wallet, this);
+ connect(recipient->amount(), &BitcoinAmount::amountChanged,
+ this, &SendRecipientsListModel::updateTotalAmount);
+ m_recipients.append(recipient);
+ endResetModel();
+
+ Q_EMIT countChanged();
+ Q_EMIT totalAmountChanged();
+ Q_EMIT currentRecipientChanged();
+ Q_EMIT currentIndexChanged();
+}
diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h
new file mode 100644
index 0000000000..2e9f2f7643
--- /dev/null
+++ b/src/qml/models/sendrecipientslistmodel.h
@@ -0,0 +1,64 @@
+// Copyright (c) 2025 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H
+#define BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H
+
+#include
+
+class SendRecipient;
+class WalletQmlModel;
+
+class SendRecipientsListModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged)
+ Q_PROPERTY(int count READ count NOTIFY countChanged)
+ Q_PROPERTY(SendRecipient* current READ currentRecipient NOTIFY currentRecipientChanged)
+ Q_PROPERTY(QString totalAmount READ totalAmount NOTIFY totalAmountChanged)
+
+public:
+ enum Roles {
+ AddressRole = Qt::UserRole + 1,
+ LabelRole,
+ AmountRole,
+ MessageRole
+ };
+
+ explicit SendRecipientsListModel(QObject* parent = nullptr);
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ QHash roleNames() const override;
+
+ Q_INVOKABLE void add();
+ Q_INVOKABLE void next();
+ Q_INVOKABLE void prev();
+ Q_INVOKABLE void remove();
+ Q_INVOKABLE void clear();
+
+ int currentIndex() const { return m_current + 1; }
+ void setCurrentIndex(int row);
+ SendRecipient* currentRecipient() const;
+ int count() const { return m_recipients.size(); }
+ QList recipients() const { return m_recipients; }
+ QString totalAmount() const;
+ qint64 totalAmountSatoshi() const { return m_totalAmount; }
+
+Q_SIGNALS:
+ void currentIndexChanged();
+ void currentRecipientChanged();
+ void countChanged();
+ void totalAmountChanged();
+
+private:
+ void updateTotalAmount();
+
+ WalletQmlModel* m_wallet;
+ QList m_recipients;
+ int m_current{0};
+ qint64 m_totalAmount{0};
+};
+
+#endif // BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H
diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp
index cdce215608..dcbbd26d37 100644
--- a/src/qml/models/walletqmlmodel.cpp
+++ b/src/qml/models/walletqmlmodel.cpp
@@ -1,3 +1,4 @@
+
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
@@ -6,6 +7,7 @@
#include
#include
+#include
#include
#include
@@ -24,7 +26,7 @@ WalletQmlModel::WalletQmlModel(std::unique_ptr wallet, QObje
m_wallet = std::move(wallet);
m_activity_list_model = new ActivityListModel(this);
m_coins_list_model = new CoinsListModel(this);
- m_current_recipient = new SendRecipient(this);
+ m_send_recipients = new SendRecipientsListModel(this);
}
WalletQmlModel::WalletQmlModel(QObject* parent)
@@ -32,14 +34,14 @@ WalletQmlModel::WalletQmlModel(QObject* parent)
{
m_activity_list_model = new ActivityListModel(this);
m_coins_list_model = new CoinsListModel(this);
- m_current_recipient = new SendRecipient(this);
+ m_send_recipients = new SendRecipientsListModel(this);
}
WalletQmlModel::~WalletQmlModel()
{
delete m_activity_list_model;
delete m_coins_list_model;
- delete m_current_recipient;
+ delete m_send_recipients;
if (m_current_transaction) {
delete m_current_transaction;
}
@@ -53,6 +55,14 @@ QString WalletQmlModel::balance() const
return BitcoinUnits::format(BitcoinUnits::Unit::BTC, m_wallet->getBalance());
}
+CAmount WalletQmlModel::balanceSatoshi() const
+{
+ if (!m_wallet) {
+ return 0;
+ }
+ return m_wallet->getBalance();
+}
+
QString WalletQmlModel::name() const
{
if (!m_wallet) {
@@ -98,20 +108,25 @@ std::unique_ptr WalletQmlModel::handleTransactionChanged(Tr
bool WalletQmlModel::prepareTransaction()
{
- if (!m_wallet || !m_current_recipient) {
+ if (!m_wallet || !m_send_recipients || m_send_recipients->recipients().empty()) {
return false;
}
- CScript scriptPubKey = GetScriptForDestination(DecodeDestination(m_current_recipient->address().toStdString()));
- wallet::CRecipient recipient = {scriptPubKey, m_current_recipient->cAmount(), m_current_recipient->subtractFeeFromAmount()};
- m_coin_control.m_feerate = CFeeRate(1000);
+ std::vector vecSend;
+ CAmount total = 0;
+ for (auto* recipient : m_send_recipients->recipients()) {
+ CScript scriptPubKey = GetScriptForDestination(DecodeDestination(recipient->address().toStdString()));
+ wallet::CRecipient c_recipient = {scriptPubKey, recipient->cAmount(), recipient->subtractFeeFromAmount()};
+ m_coin_control.m_feerate = CFeeRate(1000);
+ vecSend.push_back(c_recipient);
+ total += recipient->cAmount();
+ }
CAmount balance = m_wallet->getBalance();
- if (balance < recipient.nAmount) {
+ if (balance < total) {
return false;
}
- std::vector vecSend{recipient};
int nChangePosRet = -1;
CAmount nFeeRequired = 0;
const auto& res = m_wallet->createTransaction(vecSend, m_coin_control, true, nChangePosRet, nFeeRequired);
@@ -120,7 +135,7 @@ bool WalletQmlModel::prepareTransaction()
delete m_current_transaction;
}
CTransactionRef newTx = *res;
- m_current_transaction = new WalletQmlModelTransaction(m_current_recipient, this);
+ m_current_transaction = new WalletQmlModelTransaction(m_send_recipients, this);
m_current_transaction->setWtx(newTx);
m_current_transaction->setTransactionFee(nFeeRequired);
Q_EMIT currentTransactionChanged();
diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h
index d97cd0851f..6c8e35d09b 100644
--- a/src/qml/models/walletqmlmodel.h
+++ b/src/qml/models/walletqmlmodel.h
@@ -5,19 +5,21 @@
#ifndef BITCOIN_QML_MODELS_WALLETQMLMODEL_H
#define BITCOIN_QML_MODELS_WALLETQMLMODEL_H
-#include
-#include
#include
#include
#include
+#include
#include
+
+#include
+#include
+#include
#include
-#include
#include
#include
-class ActivityListModel;
+#include
class WalletQmlModel : public QObject
{
@@ -26,7 +28,7 @@ class WalletQmlModel : public QObject
Q_PROPERTY(QString balance READ balance NOTIFY balanceChanged)
Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel CONSTANT)
Q_PROPERTY(CoinsListModel* coinsListModel READ coinsListModel CONSTANT)
- Q_PROPERTY(SendRecipient* sendRecipient READ sendRecipient CONSTANT)
+ Q_PROPERTY(SendRecipientsListModel* recipients READ sendRecipientList CONSTANT)
Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged)
public:
@@ -36,8 +38,14 @@ class WalletQmlModel : public QObject
QString name() const;
QString balance() const;
+ CAmount balanceSatoshi() const;
+
ActivityListModel* activityListModel() const { return m_activity_list_model; }
CoinsListModel* coinsListModel() const { return m_coins_list_model; }
+ SendRecipientsListModel* sendRecipientList() const { return m_send_recipients; }
+ WalletQmlModelTransaction* currentTransaction() const { return m_current_transaction; }
+ Q_INVOKABLE bool prepareTransaction();
+ Q_INVOKABLE void sendTransaction();
std::set getWalletTxs() const;
interfaces::WalletTx getWalletTx(const uint256& hash) const;
@@ -46,11 +54,6 @@ class WalletQmlModel : public QObject
int& num_blocks,
int64_t& block_time) const;
- SendRecipient* sendRecipient() const { return m_current_recipient; }
- WalletQmlModelTransaction* currentTransaction() const { return m_current_transaction; }
- Q_INVOKABLE bool prepareTransaction();
- Q_INVOKABLE void sendTransaction();
-
using TransactionChangedFn = std::function;
virtual std::unique_ptr handleTransactionChanged(TransactionChangedFn fn);
@@ -73,7 +76,7 @@ class WalletQmlModel : public QObject
std::unique_ptr m_wallet;
ActivityListModel* m_activity_list_model{nullptr};
CoinsListModel* m_coins_list_model{nullptr};
- SendRecipient* m_current_recipient{nullptr};
+ SendRecipientsListModel* m_send_recipients{nullptr};
WalletQmlModelTransaction* m_current_transaction{nullptr};
wallet::CCoinControl m_coin_control;
};
diff --git a/src/qml/models/walletqmlmodeltransaction.cpp b/src/qml/models/walletqmlmodeltransaction.cpp
index 199103377a..11cc34d08c 100644
--- a/src/qml/models/walletqmlmodeltransaction.cpp
+++ b/src/qml/models/walletqmlmodeltransaction.cpp
@@ -4,11 +4,13 @@
#include
+#include
+#include
+
#include
-#include
-WalletQmlModelTransaction::WalletQmlModelTransaction(const SendRecipient* recipient, QObject* parent)
- : QObject(parent), m_address(recipient->address()), m_amount(recipient->cAmount()), m_fee(0), m_label(recipient->label()), m_wtx(nullptr)
+WalletQmlModelTransaction::WalletQmlModelTransaction(const SendRecipientsListModel* recipient, QObject* parent)
+ : QObject(parent), m_address(recipient->recipients().at(0)->address()), m_amount(recipient->totalAmountSatoshi()), m_fee(0), m_label(recipient->recipients().at(0)->label()), m_wtx(nullptr)
{
}
diff --git a/src/qml/models/walletqmlmodeltransaction.h b/src/qml/models/walletqmlmodeltransaction.h
index 7bf914e06a..35112249de 100644
--- a/src/qml/models/walletqmlmodeltransaction.h
+++ b/src/qml/models/walletqmlmodeltransaction.h
@@ -5,12 +5,10 @@
#ifndef BITCOIN_QML_MODELS_WALLETQMLMODELTRANSACTION_H
#define BITCOIN_QML_MODELS_WALLETQMLMODELTRANSACTION_H
-#include
-#include
+#include
#include
-
-#include
+#include
class WalletQmlModelTransaction : public QObject
@@ -22,7 +20,7 @@ class WalletQmlModelTransaction : public QObject
Q_PROPERTY(QString fee READ fee NOTIFY feeChanged)
Q_PROPERTY(QString total READ total NOTIFY totalChanged)
public:
- explicit WalletQmlModelTransaction(const SendRecipient* recipient, QObject* parent = nullptr);
+ explicit WalletQmlModelTransaction(const SendRecipientsListModel* recipient, QObject* parent = nullptr);
QString address() const;
QString amount() const;
@@ -30,8 +28,6 @@ class WalletQmlModelTransaction : public QObject
QString label() const;
QString total() const;
- QList getRecipients() const;
-
CTransactionRef& getWtx();
void setWtx(const CTransactionRef&);
diff --git a/src/qml/pages/main.qml b/src/qml/pages/main.qml
index 60aa6c2705..710e2c3bae 100644
--- a/src/qml/pages/main.qml
+++ b/src/qml/pages/main.qml
@@ -85,7 +85,11 @@ ApplicationWindow {
main.push(createWalletWizard)
}
onSendTransaction: {
- main.push(sendReviewPage)
+ if (multipleRecipientsEnabled) {
+ main.push(multipleSendReviewPage)
+ } else {
+ main.push(sendReviewPage)
+ }
}
}
}
@@ -106,7 +110,21 @@ ApplicationWindow {
main.pop()
}
onTransactionSent: {
- walletController.selectedWallet.sendRecipient.clear()
+ walletController.selectedWallet.recipients.clear()
+ main.pop()
+ sendResult.open()
+ }
+ }
+ }
+
+ Component {
+ id: multipleSendReviewPage
+ MultipleSendReview {
+ onBack: {
+ main.pop()
+ }
+ onTransactionSent: {
+ walletController.selectedWallet.recipients.clear()
main.pop()
sendResult.open()
}
diff --git a/src/qml/pages/wallet/DesktopWallets.qml b/src/qml/pages/wallet/DesktopWallets.qml
index d2bf0469b3..bdb6bb4ba6 100644
--- a/src/qml/pages/wallet/DesktopWallets.qml
+++ b/src/qml/pages/wallet/DesktopWallets.qml
@@ -20,7 +20,7 @@ Page {
ButtonGroup { id: navigationTabs }
signal addWallet()
- signal sendTransaction()
+ signal sendTransaction(bool multipleRecipientsEnabled)
header: NavigationBar2 {
id: navBar
@@ -132,7 +132,7 @@ Page {
Activity {
}
Send {
- onTransactionPrepared: root.sendTransaction()
+ onTransactionPrepared: root.sendTransaction(multipleRecipientsEnabled)
}
RequestPayment {
}
diff --git a/src/qml/pages/wallet/MultipleSendReview.qml b/src/qml/pages/wallet/MultipleSendReview.qml
new file mode 100644
index 0000000000..91f6ee2d96
--- /dev/null
+++ b/src/qml/pages/wallet/MultipleSendReview.qml
@@ -0,0 +1,155 @@
+// Copyright (c) 2024 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import org.bitcoincore.qt 1.0
+
+import "../../controls"
+import "../../components"
+
+Page {
+ id: root
+ background: null
+
+ property WalletQmlModel wallet: walletController.selectedWallet
+ property WalletQmlModelTransaction transaction: walletController.selectedWallet.currentTransaction
+
+ signal finished()
+ signal back()
+ signal transactionSent()
+
+ header: NavigationBar2 {
+ id: navbar
+ leftItem: NavButton {
+ iconSource: "image://images/caret-left"
+ text: qsTr("Back")
+ onClicked: {
+ root.back()
+ }
+ }
+ }
+
+ ScrollView {
+ clip: true
+ width: parent.width
+ height: parent.height
+ contentWidth: width
+
+ ColumnLayout {
+ id: columnLayout
+ width: 450
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ spacing: 15
+
+ CoreText {
+ id: title
+ Layout.topMargin: 30
+ Layout.bottomMargin: 15
+ text: qsTr("Transaction details")
+ font.pixelSize: 21
+ bold: true
+ }
+
+ ListView {
+ id: inputsList
+ Layout.fillWidth: true
+ Layout.preferredHeight: contentHeight
+ model: root.wallet.recipients
+ delegate: Item {
+ id: delegate
+ height: 55
+ width: ListView.view.width
+
+ required property string address;
+ required property string label;
+ required property string amount;
+
+ RowLayout {
+ spacing: 10
+ anchors.fill: parent
+ CoreText {
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ text: label == "" ? address : label
+ font.pixelSize: 18
+ elide: Text.ElideMiddle
+ }
+
+ CoreText {
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
+ text: amount
+ font.pixelSize: 18
+ }
+ }
+
+ Separator {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ color: Theme.color.neutral3
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.topMargin: 20
+ CoreText {
+ text: qsTr("Total amount")
+ font.pixelSize: 20
+ color: Theme.color.neutral9
+ horizontalAlignment: Text.AlignLeft
+ }
+ Item {
+ Layout.fillWidth: true
+ }
+ CoreText {
+ text: root.transaction.total
+ font.pixelSize: 20
+ color: Theme.color.neutral9
+ }
+ }
+
+ Separator {
+ Layout.fillWidth: true
+ color: Theme.color.neutral3
+ }
+
+ RowLayout {
+ CoreText {
+ text: qsTr("Fee")
+ font.pixelSize: 18
+ Layout.preferredWidth: 110
+ horizontalAlignment: Text.AlignLeft
+ }
+ Item {
+ Layout.fillWidth: true
+ }
+ CoreText {
+ text: root.transaction.fee
+ font.pixelSize: 15
+ }
+ }
+
+ Separator {
+ Layout.fillWidth: true
+ color: Theme.color.neutral3
+ }
+
+ ContinueButton {
+ id: confimationButton
+ Layout.fillWidth: true
+ Layout.topMargin: 30
+ text: qsTr("Send")
+ onClicked: {
+ root.wallet.sendTransaction()
+ root.transactionSent()
+ }
+ }
+ }
+ }
+}
diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml
index 9c1905db99..acc47a5240 100644
--- a/src/qml/pages/wallet/Send.qml
+++ b/src/qml/pages/wallet/Send.qml
@@ -16,9 +16,9 @@ PageStack {
vertical: true
property WalletQmlModel wallet: walletController.selectedWallet
- property SendRecipient recipient: wallet.sendRecipient
+ property SendRecipient recipient: wallet.recipients.current
- signal transactionPrepared()
+ signal transactionPrepared(bool multipleRecipientsEnabled)
Connections {
target: walletController
@@ -33,12 +33,14 @@ PageStack {
Settings {
id: settings
property alias coinControlEnabled: sendOptionsPopup.coinControlEnabled
+ property alias multipleRecipientsEnabled: sendOptionsPopup.multipleRecipientsEnabled
}
ScrollView {
clip: true
width: parent.width
height: parent.height
+
contentWidth: width
ColumnLayout {
@@ -55,6 +57,7 @@ PageStack {
Layout.fillWidth: true
Layout.topMargin: 30
Layout.bottomMargin: 20
+
CoreText {
id: title
anchors.left: parent.left
@@ -64,11 +67,13 @@ PageStack {
color: Theme.color.neutral9
bold: true
}
- EllipsisMenuButton {
+
+ IconButton {
id: menuButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: sendOptionsPopup.opened
+ iconSource: "image://images/ellipsis"
onClicked: {
sendOptionsPopup.open()
}
@@ -78,91 +83,203 @@ PageStack {
id: sendOptionsPopup
x: menuButton.x - width + menuButton.width
y: menuButton.y + menuButton.height
- width: 300
- height: 50
}
}
- LabeledTextInput {
- id: address
+ RowLayout {
+ id: selectAndAddRecipients
Layout.fillWidth: true
- labelText: qsTr("Send to")
- placeholderText: qsTr("Enter address...")
- text: root.recipient.address
- onTextEdited: root.recipient.address = address.text
+ Layout.topMargin: 10
+ Layout.bottomMargin: 10
+ visible: settings.multipleRecipientsEnabled
+
+ CoreText {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignLeft
+ id: selectAndAddRecipientsLabel
+ text: qsTr("Recipient %1 of %2").arg(wallet.recipients.currentIndex).arg(wallet.recipients.count)
+ horizontalAlignment: Text.AlignLeft
+ font.pixelSize: 18
+ color: Theme.color.neutral9
+ }
+
+ IconButton {
+ Layout.preferredWidth: 30
+ Layout.preferredHeight: 30
+ size: 30
+ iconSource: "image://images/caret-left"
+ enabled: wallet.recipients.currentIndex - 1 > 0
+ onClicked: {
+ wallet.recipients.prev()
+ }
+ }
+
+ IconButton {
+ Layout.preferredWidth: 30
+ Layout.preferredHeight: 30
+ size: 30
+ iconSource: "image://images/caret-right"
+ enabled: wallet.recipients.currentIndex < wallet.recipients.count
+ onClicked: {
+ wallet.recipients.next()
+ }
+ }
+
+ IconButton {
+ Layout.preferredWidth: 30
+ Layout.preferredHeight: 30
+ size: 30
+ iconSource: "image://images/plus-big-filled"
+ onClicked: {
+ wallet.recipients.add()
+ }
+ }
+
+ IconButton {
+ Layout.preferredWidth: 30
+ Layout.preferredHeight: 30
+ size: 30
+ iconSource: "image://images/minus"
+ enabled: wallet.recipients.count > 1
+ onClicked: {
+ wallet.recipients.remove()
+ }
+ }
}
Separator {
+ visible: settings.multipleRecipientsEnabled
Layout.fillWidth: true
}
- Item {
- BitcoinAmount {
- id: bitcoinAmount
- }
-
- height: amountInput.height
+ ColumnLayout {
Layout.fillWidth: true
- CoreText {
- id: amountLabel
- width: 110
- anchors.left: parent.left
- anchors.verticalCenter: parent.verticalCenter
- horizontalAlignment: Text.AlignLeft
- text: qsTr("Amount")
- font.pixelSize: 18
+
+ LabeledTextInput {
+ id: address
+ Layout.fillWidth: true
+ labelText: qsTr("Send to")
+ placeholderText: qsTr("Enter address...")
+ text: root.recipient.address
+ onTextEdited: root.recipient.address = address.text
+ validator: RegExpValidator {
+ regExp: /^[1-9A-HJ-NP-Za-km-zac-hj-np-z02-9]+$/
+ }
+ maximumLength: 62
}
- TextField {
- id: amountInput
- anchors.left: amountLabel.right
- anchors.verticalCenter: parent.verticalCenter
- leftPadding: 0
- font.family: "Inter"
- font.styleName: "Regular"
- font.pixelSize: 18
- color: Theme.color.neutral9
- placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4
- background: Item {}
- placeholderText: "0.00000000"
- selectByMouse: true
- onTextEdited: {
- amountInput.text = bitcoinAmount.amount = bitcoinAmount.sanitize(amountInput.text)
- root.recipient.amount = bitcoinAmount.satoshiAmount
+ RowLayout {
+ id: addressIssue
+ Layout.fillWidth: true
+ visible: root.recipient.addressError.length > 0
+
+ Icon {
+ source: "image://images/alert-filled"
+ size: 22
+ color: Theme.color.red
+ }
+
+ CoreText {
+ id: warningText
+ text: root.recipient.addressError
+ font.pixelSize: 15
+ color: Theme.color.red
+ horizontalAlignment: Text.AlignLeft
+ Layout.fillWidth: true
}
}
+ }
+
+ Separator {
+ Layout.fillWidth: true
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+
Item {
- width: unitLabel.width + flipIcon.width
- height: Math.max(unitLabel.height, flipIcon.height)
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (bitcoinAmount.unit == BitcoinAmount.BTC) {
- amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.BTC)
- bitcoinAmount.unit = BitcoinAmount.SAT
- } else {
- amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.SAT)
- bitcoinAmount.unit = BitcoinAmount.BTC
- }
- }
- }
+ height: amountInput.height
+ Layout.fillWidth: true
CoreText {
- id: unitLabel
- anchors.right: flipIcon.left
+ id: amountLabel
+ width: 110
+ anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
- text: bitcoinAmount.unitLabel
+ horizontalAlignment: Text.AlignLeft
+ text: qsTr("Amount")
font.pixelSize: 18
- color: enabled ? Theme.color.neutral7 : Theme.color.neutral4
}
- Icon {
- id: flipIcon
+
+ TextField {
+ id: amountInput
+ anchors.left: amountLabel.right
+ anchors.verticalCenter: parent.verticalCenter
+ leftPadding: 0
+ font.family: "Inter"
+ font.styleName: "Regular"
+ font.pixelSize: 18
+ color: Theme.color.neutral9
+ placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4
+ background: Item {}
+ placeholderText: "0.00000000"
+ selectByMouse: true
+ text: root.recipient.amount.display
+ onTextEdited: root.recipient.amount.display = text
+ onEditingFinished: root.recipient.amount.format()
+ onActiveFocusChanged: {
+ if (!activeFocus) {
+ root.recipient.amount.format()
+ }
+ }
+ validator: RegExpValidator {
+ regExp: /^(0|[1-9]\d*)(\.\d{0,8})?$/
+ }
+ maximumLength: 17
+ }
+ Item {
+ width: unitLabel.width + flipIcon.width
+ height: Math.max(unitLabel.height, flipIcon.height)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
- source: "image://images/flip-vertical"
- icon.color: unitLabel.enabled ? Theme.color.neutral8 : Theme.color.neutral4
- size: 30
+ MouseArea {
+ anchors.fill: parent
+ onClicked: root.recipient.amount.flipUnit()
+ }
+ CoreText {
+ id: unitLabel
+ anchors.right: flipIcon.left
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.recipient.amount.unitLabel
+ font.pixelSize: 18
+ color: enabled ? Theme.color.neutral7 : Theme.color.neutral4
+ }
+ Icon {
+ id: flipIcon
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ source: "image://images/flip-vertical"
+ icon.color: unitLabel.enabled ? Theme.color.neutral8 : Theme.color.neutral4
+ size: 30
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ visible: root.recipient.amountError.length > 0
+
+ Icon {
+ source: "image://images/alert-filled"
+ size: 22
+ color: Theme.color.red
+ }
+
+ CoreText {
+ text: root.recipient.amountError
+ font.pixelSize: 15
+ color: Theme.color.red
+ horizontalAlignment: Text.AlignLeft
+ Layout.fillWidth: true
}
}
}
@@ -224,9 +341,10 @@ PageStack {
Layout.fillWidth: true
Layout.topMargin: 30
text: qsTr("Review")
+ enabled: root.recipient.isValid
onClicked: {
if (root.wallet.prepareTransaction()) {
- root.transactionPrepared()
+ root.transactionPrepared(settings.multipleRecipientsEnabled);
}
}
}
diff --git a/src/qml/res/icons/alert-filled.png b/src/qml/res/icons/alert-filled.png
new file mode 100644
index 0000000000..a097bea42c
Binary files /dev/null and b/src/qml/res/icons/alert-filled.png differ
diff --git a/src/qml/res/icons/plus-big-filled.png b/src/qml/res/icons/plus-big-filled.png
new file mode 100644
index 0000000000..365ed049e5
Binary files /dev/null and b/src/qml/res/icons/plus-big-filled.png differ
diff --git a/src/qml/res/src/alert-filled.svg b/src/qml/res/src/alert-filled.svg
new file mode 100644
index 0000000000..f556ef71ca
--- /dev/null
+++ b/src/qml/res/src/alert-filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/qml/res/src/plus-big-filled.svg b/src/qml/res/src/plus-big-filled.svg
new file mode 100644
index 0000000000..2efe7ba2c5
--- /dev/null
+++ b/src/qml/res/src/plus-big-filled.svg
@@ -0,0 +1,3 @@
+
diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py
index fa98b6fd69..b2b7540d97 100755
--- a/test/lint/lint-circular-dependencies.py
+++ b/test/lint/lint-circular-dependencies.py
@@ -17,6 +17,10 @@
"node/utxo_snapshot -> validation -> node/utxo_snapshot",
"qml/models/activitylistmodel -> qml/models/walletqmlmodel -> qml/models/activitylistmodel",
"qml/models/coinslistmodel -> qml/models/walletqmlmodel -> qml/models/coinslistmodel",
+ "qml/models/sendrecipient -> qml/models/walletqmlmodel -> qml/models/sendrecipient",
+ "qml/models/sendrecipient -> qml/models/walletqmlmodel -> qml/models/walletqmlmodeltransaction -> qml/models/sendrecipient",
+ "qml/models/sendrecipientslistmodel -> qml/models/walletqmlmodel -> qml/models/sendrecipientslistmodel",
+ "qml/models/sendrecipientslistmodel -> qml/models/walletqmlmodel -> qml/models/walletqmlmodeltransaction -> qml/models/sendrecipientslistmodel",
"qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel",
"qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel",
"qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog",