Fix precision loss when passing amounts from QML
What changed, and why it matters
This commit fixes a precision-loss bug when the Monero GUI wallet passes payment amounts from the user interface (QML) to the underlying wallet code. Previously, amounts were converted too early from human-readable XMR strings into 64-bit integers, which could silently drop very small fractions of a Monero amount. The patch moves the conversion into the C++ backend where the wallet's own parsing function is used, preserving precision. This mainly affects QR-code/payment URIs and reserve proofs, not a remote exploit.
Treat as a routine bug-fix commit with minor security relevance. Review whether any other QML/C++ amount conversions still use the old pattern. No urgent incident response is indicated, but users generating payment requests or reserve proofs with very small amounts should update to avoid incorrect values.
Security signals we found
Precision loss in monetary amount handling
Type-safety improvement at QML/C++ boundary
Fixes incorrect amount shown in generated payment URIs/QR codes
Potential incorrect amount used in reserve proofs
Evidence from the diff
The patch changes the QML-exposed Wallet::make_uri and Wallet::getReserveProof signatures so the amount parameter is a QString instead of quint64. QML callers now pass the raw string (e.g., amountToReceiveXMR.text) and the C++ backend calls Monero::Wallet::amountFromString() before forwarding the atomic uint64_t value to the wallet implementation. This prevents intermediate precision loss that occurred when QML’s walletManager.amountFromString() converted the string to quint64 and the value was then passed through the QML/C++ boundary as a number. The fix is localized to amount handling for URIs/QR codes and reserve proofs.
Changed components
main.qmlpages/Receive.qmlpages/merchant/Merchant.qmlsrc/libwalletqt/Wallet.cppsrc/libwalletqt/Wallet.hsrc/libwalletqt/WalletManager.cppsrc/libwalletqt/WalletManager.hInspect captured patch +15 / −13
diff --git a/main.qml b/main.qml
index d2fdb7e..f4a7843 100644
--- a/main.qml
+++ b/main.qml
@@ -1072,7 +1072,7 @@ ApplicationWindow {
// called on "getProof"
function handleGetProof(txid, address, message, amount) {
if (amount !== null && amount.length > 0) {
- var result = currentWallet.getReserveProof(false, currentWallet.currentSubaddressAccount, walletManager.amountFromString(amount), message)
+ var result = currentWallet.getReserveProof(false, currentWallet.currentSubaddressAccount, amount, message)
txProofComputed(null, result)
} else {
console.log("Getting payment proof: ")
diff --git a/pages/Receive.qml b/pages/Receive.qml
index 9a36a27..e299404 100644
--- a/pages/Receive.qml
+++ b/pages/Receive.qml
@@ -64,7 +64,7 @@ Rectangle {
function generateQRCodeString() {
if (pageReceive.state == "PaymentRequest") {
return walletManager.make_uri(appWindow.current_address,
- walletManager.amountFromString(amountToReceiveXMR.text),
+ amountToReceiveXMR.text,
txDescriptionInput.text, receiverNameInput.text);
} else {
return walletManager.make_uri(appWindow.current_address);
diff --git a/pages/merchant/Merchant.qml b/pages/merchant/Merchant.qml
index 86d4756..da4d68f 100644
--- a/pages/merchant/Merchant.qml
+++ b/pages/merchant/Merchant.qml
@@ -239,7 +239,7 @@ Item {
smooth: false
fillMode: Image.PreserveAspectFit
- source: "image://qrcode/" + walletManager.make_uri(appWindow.current_address, walletManager.amountFromString(amountToReceive.text))
+ source: "image://qrcode/" + walletManager.make_uri(appWindow.current_address, amountToReceive.text)
MouseArea {
anchors.fill: parent
@@ -428,7 +428,7 @@ Item {
font.pixelSize: 12
font.bold: true
color: _color
- text: walletManager.make_uri(appWindow.current_address, walletManager.amountFromString(amountToReceive.text))
+ text: walletManager.make_uri(appWindow.current_address, amountToReceive.text)
themeTransition: false
MouseArea {
@@ -713,7 +713,7 @@ Item {
selectExisting: false
nameFilters: ["Image (*.png)"]
onAccepted: {
- if (!walletManager.saveQrCode(walletManager.make_uri(appWindow.current_address, walletManager.amountFromString(amountToReceive.text)), walletManager.urlToLocalPath(fileUrl))) {
+ if (!walletManager.saveQrCode(walletManager.make_uri(appWindow.current_address, amountToReceive.text), walletManager.urlToLocalPath(fileUrl))) {
console.log("Failed to save QrCode to file " + walletManager.urlToLocalPath(fileUrl) )
receivePageDialog.title = qsTr("Save QrCode") + translationManager.emptyString;
receivePageDialog.text = qsTr("Failed to save QrCode to ") + walletManager.urlToLocalPath(fileUrl) + translationManager.emptyString;
diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp
index 8e32b52..29cee6d 100644
--- a/src/libwalletqt/Wallet.cpp
+++ b/src/libwalletqt/Wallet.cpp
@@ -945,10 +945,11 @@ Q_INVOKABLE QString Wallet::checkSpendProof(const QString &txid, const QString &
return QString::fromStdString(result);
}
-Q_INVOKABLE QString Wallet::getReserveProof(bool all, quint32 account_index, quint64 amount, const QString &message) const
+Q_INVOKABLE QString Wallet::getReserveProof(bool all, quint32 account_index, const QString &amount, const QString &message) const
{
qDebug("Generating reserve proof");
- std::string result = m_walletImpl->getReserveProof(all, account_index, amount, message.toStdString());
+ const quint64 amountAtomic = Monero::Wallet::amountFromString(amount.toStdString());
+ std::string result = m_walletImpl->getReserveProof(all, account_index, amountAtomic, message.toStdString());
if (result.empty())
result = "error|" + m_walletImpl->errorString();
return QString::fromStdString(result);
@@ -1053,10 +1054,11 @@ bool Wallet::parse_uri(const QString &uri, QString &address, QString &payment_id
return res;
}
-QString Wallet::make_uri(const QString &address, const quint64 &amount, const QString &tx_description, const QString &recipient_name) const
+QString Wallet::make_uri(const QString &address, const QString &amount, const QString &tx_description, const QString &recipient_name) const
{
std::string error;
- return QString::fromStdString(m_walletImpl->make_uri(address.toStdString(), "", amount, tx_description.toStdString(), recipient_name.toStdString(), error));
+ const quint64 amountAtomic = Monero::Wallet::amountFromString(amount.toStdString());
+ return QString::fromStdString(m_walletImpl->make_uri(address.toStdString(), "", amountAtomic, tx_description.toStdString(), recipient_name.toStdString(), error));
}
bool Wallet::rescanSpent()
diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h
index e41540e..1cac75a 100644
--- a/src/libwalletqt/Wallet.h
+++ b/src/libwalletqt/Wallet.h
@@ -323,7 +323,7 @@ public:
//! Parse URI
Q_INVOKABLE bool parse_uri(const QString &uri, QString &address, QString &payment_id, uint64_t &amount, QString &tx_description, QString &recipient_name, QVector<QString> &unknown_parameters, QString &error);
//! Make URI
- Q_INVOKABLE QString make_uri(const QString &address, const quint64 &amount = 0, const QString &tx_description = "", const QString &recipient_name = "") const;
+ Q_INVOKABLE QString make_uri(const QString &address, const QString &amount = "", const QString &tx_description = "", const QString &recipient_name = "") const;
//! Namespace your cacheAttribute keys to avoid collisions
Q_INVOKABLE bool setCacheAttribute(const QString &key, const QString &val);
@@ -340,7 +340,7 @@ public:
Q_INVOKABLE QString getSpendProof(const QString &txid, const QString &message) const;
Q_INVOKABLE void getSpendProofAsync(const QString &txid, const QString &message, const QJSValue &callback);
Q_INVOKABLE QString checkSpendProof(const QString &txid, const QString &message, const QString &signature) const;
- Q_INVOKABLE QString getReserveProof(bool all, quint32 account_index, quint64 amount, const QString &message) const;
+ Q_INVOKABLE QString getReserveProof(bool all, quint32 account_index, const QString &amount, const QString &message) const;
Q_INVOKABLE QString checkReserveProof(const QString &address, const QString &message, const QString &signature) const;
// Rescan spent outputs
Q_INVOKABLE bool rescanSpent();
diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp
index 1b3de87..73046af 100644
--- a/src/libwalletqt/WalletManager.cpp
+++ b/src/libwalletqt/WalletManager.cpp
@@ -441,7 +441,7 @@ QVariantMap WalletManager::parse_uri_to_object(const QString &uri) const
return result;
}
-QString WalletManager::make_uri(const QString &address, const quint64 &amount, const QString &tx_description, const QString &recipient_name) const
+QString WalletManager::make_uri(const QString &address, const QString &amount, const QString &tx_description, const QString &recipient_name) const
{
QMutexLocker locker(&m_mutex);
if (m_currentWallet)
diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h
index e98648c..649a9f9 100644
--- a/src/libwalletqt/WalletManager.h
+++ b/src/libwalletqt/WalletManager.h
@@ -179,7 +179,7 @@ public:
Q_INVOKABLE QString resolveOpenAlias(const QString &address) const;
Q_INVOKABLE bool parse_uri(const QString &uri, QString &address, QString &payment_id, uint64_t &amount, QString &tx_description, QString &recipient_name, QVector<QString> &unknown_parameters, QString &error) const;
Q_INVOKABLE QVariantMap parse_uri_to_object(const QString &uri) const;
- Q_INVOKABLE QString make_uri(const QString &address, const quint64 &amount = 0, const QString &tx_description = "", const QString &recipient_name = "") const;
+ Q_INVOKABLE QString make_uri(const QString &address, const QString &amount = "", const QString &tx_description = "", const QString &recipient_name = "") const;
Q_INVOKABLE bool saveQrCode(const QString &, const QString &) const;
Q_INVOKABLE void saveQrCodeToClipboard(const QString &) const;
Q_INVOKABLE void checkUpdatesAsync(
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.