summaryrefslogtreecommitdiff
path: root/patches/monero/0005-UR-functions.patch
diff options
context:
space:
mode:
authorcyan <cyjan@mrcyjanek.net>2025-02-20 15:12:58 +0100
committerGitHub <noreply@github.com>2025-02-20 15:12:58 +0100
commit991a0d0c97bce6ae8671dc763dd03876dfa9ea8a (patch)
treedd3ea422437bab03a774676868afb2216e13d5f3 /patches/monero/0005-UR-functions.patch
parent65608c09e9093f1cd42c6afd8e9131016c82574b (diff)
[monero] update to latest v0.18 (#122)v0.18.4.0-RC1
* monero update * update checksum
Diffstat (limited to 'patches/monero/0005-UR-functions.patch')
-rw-r--r--patches/monero/0005-UR-functions.patch1010
1 files changed, 1010 insertions, 0 deletions
diff --git a/patches/monero/0005-UR-functions.patch b/patches/monero/0005-UR-functions.patch
new file mode 100644
index 0000000..c3cca26
--- /dev/null
+++ b/patches/monero/0005-UR-functions.patch
@@ -0,0 +1,1010 @@
+From ff70c74c8b331758ace1f14df0ca107731dda49d Mon Sep 17 00:00:00 2001
+From: tobtoht <tob@featherwallet.org>
+Date: Tue, 12 Mar 2024 10:09:50 +0100
+Subject: [PATCH 05/14] UR functions
+
+This commit adds UR functions for UR tasks,
+I believe that the right place to get
+UR strings is the wallet code itself,
+especially because it allows us to
+skip the part when we have to store
+things to file to encode them later.
+Now we are fully in memory
+
+Things broken in the commit
+- ledger support.
+ AUTO_LOCK_CMD macro causes compile time
+ issues with this patch. I don't know why
+ just yet, this is a issue that I'll fix
+ later. However (considering the purpose
+ of this patch) it is not a dealbreaker.
+---
+ .gitmodules | 4 +
+ CMakeLists.txt | 4 +-
+ external/CMakeLists.txt | 1 +
+ external/bc-ur | 1 +
+ src/device/device_ledger.cpp | 5 +-
+ src/wallet/CMakeLists.txt | 1 +
+ src/wallet/api/pending_transaction.cpp | 33 +++
+ src/wallet/api/pending_transaction.h | 1 +
+ src/wallet/api/unsigned_transaction.cpp | 42 ++++
+ src/wallet/api/unsigned_transaction.h | 1 +
+ src/wallet/api/wallet.cpp | 309 +++++++++++++++++++++++-
+ src/wallet/api/wallet.h | 8 +
+ src/wallet/api/wallet2_api.h | 22 +-
+ src/wallet/wallet2.cpp | 141 +++++++----
+ src/wallet/wallet2.h | 3 +
+ 15 files changed, 519 insertions(+), 57 deletions(-)
+ create mode 160000 external/bc-ur
+
+diff --git a/.gitmodules b/.gitmodules
+index ffb73fe9a..72af74d55 100644
+--- a/.gitmodules
++++ b/.gitmodules
+@@ -15,3 +15,7 @@
+ path = external/supercop
+ url = https://github.com/monero-project/supercop
+ branch = monero
++[submodule "external/bc-ur"]
++ path = external/bc-ur
++ url = https://github.com/MrCyjaneK/bc-ur
++ branch = misc
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index db69b1b04..c73b813d8 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -96,7 +96,8 @@ enable_language(C ASM)
+ set(CMAKE_C_STANDARD 11)
+ set(CMAKE_C_STANDARD_REQUIRED ON)
+ set(CMAKE_C_EXTENSIONS OFF)
+-set(CMAKE_CXX_STANDARD 14)
++set(CMAKE_CXX_STANDARD 17)
++add_definitions(-D_LIBCPP_ENABLE_CXX17_REMOVED_FEATURES) # boost: no template named 'unary_function' in namespace 'std'; did you mean '__unary_function'?
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
+ set(CMAKE_CXX_EXTENSIONS OFF)
+
+@@ -364,6 +365,7 @@ if(NOT MANUAL_SUBMODULES)
+ endfunction ()
+
+ message(STATUS "Checking submodules")
++# check_submodule(external/bc-ur)
+ check_submodule(external/miniupnp)
+ check_submodule(external/rapidjson)
+ check_submodule(external/trezor-common)
+diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
+index 538e4d215..074e23f16 100644
+--- a/external/CMakeLists.txt
++++ b/external/CMakeLists.txt
+@@ -70,4 +70,5 @@ endif()
+ add_subdirectory(db_drivers)
+ add_subdirectory(easylogging++)
+ add_subdirectory(qrcodegen)
++add_subdirectory(bc-ur)
+ add_subdirectory(randomx EXCLUDE_FROM_ALL)
+diff --git a/external/bc-ur b/external/bc-ur
+new file mode 160000
+index 000000000..d82e7c753
+--- /dev/null
++++ b/external/bc-ur
+@@ -0,0 +1 @@
++Subproject commit d82e7c753e710b8000706dc3383b498438795208
+diff --git a/src/device/device_ledger.cpp b/src/device/device_ledger.cpp
+index 5d0afe1ee..bb5b6f497 100644
+--- a/src/device/device_ledger.cpp
++++ b/src/device/device_ledger.cpp
+@@ -313,12 +313,13 @@ namespace hw {
+
+ /* ======================================================================= */
+ /* LOCKER */
+- /* ======================================================================= */
++ /* ======================================================================= */
+
+ //automatic lock one more level on device ensuring the current thread is allowed to use it
++ #pragma message ("Warning AUTO_LOCK_CMD is intentionally left broken. This is yet to be fixed.")
+ #define AUTO_LOCK_CMD() \
+ /* lock both mutexes without deadlock*/ \
+- boost::lock(device_locker, command_locker); \
++ /* boost::lock(device_locker, command_locker); */ \
+ /* make sure both already-locked mutexes are unlocked at the end of scope */ \
+ boost::lock_guard<boost::recursive_mutex> lock1(device_locker, boost::adopt_lock); \
+ boost::lock_guard<boost::mutex> lock2(command_locker, boost::adopt_lock)
+diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt
+index 6095f99d5..b163212b7 100644
+--- a/src/wallet/CMakeLists.txt
++++ b/src/wallet/CMakeLists.txt
+@@ -50,6 +50,7 @@ monero_add_library(wallet
+ target_link_libraries(wallet
+ PUBLIC
+ rpc_base
++ bc-ur
+ multisig
+ common
+ cryptonote_core
+diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp
+index 70a702796..9c3c26ee5 100644
+--- a/src/wallet/api/pending_transaction.cpp
++++ b/src/wallet/api/pending_transaction.cpp
+@@ -42,6 +42,8 @@
+ #include <boost/format.hpp>
+ #include <boost/filesystem.hpp>
+
++#include "bc-ur/src/bc-ur.hpp"
++
+ using namespace std;
+
+ namespace Monero {
+@@ -162,6 +164,37 @@ bool PendingTransactionImpl::commit(const std::string &filename, bool overwrite)
+ return m_status == Status_Ok;
+ }
+
++std::string PendingTransactionImpl::commitUR(int max_fragment_length) {
++
++ LOG_PRINT_L3("m_pending_tx size: " << m_pending_tx.size());
++
++ try {
++ std::string ptx = m_wallet.m_wallet->dump_tx_to_str(m_pending_tx);
++ m_status = Status_Ok;
++ auto urMessage = ur::string_to_bytes(ptx);
++ ur::ByteVector cbor;
++ ur::CborLite::encodeBytes(cbor, urMessage);
++ std::string type;
++ if (m_wallet.watchOnly()) {
++ type = "xmr-txunsigned";
++ } else {
++ type = "xmr-txsigned";
++ }
++ ur::UR urData = ur::UR(type, cbor);
++ auto encoder = ur::UREncoder(urData, max_fragment_length);
++ std::string output;
++ for(size_t i = 0; i < encoder.seq_len(); i++) {
++ output.append("\n"+encoder.next_part());
++ }
++ return output;
++ } catch (const std::exception &e) {
++ m_errorString = string(tr("Unknown exception: ")) + e.what();
++ m_status = Status_Error;
++ return "";
++ }
++}
++
++
+ uint64_t PendingTransactionImpl::amount() const
+ {
+ uint64_t result = 0;
+diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h
+index 0a9779c07..403bfe281 100644
+--- a/src/wallet/api/pending_transaction.h
++++ b/src/wallet/api/pending_transaction.h
+@@ -46,6 +46,7 @@ public:
+ int status() const override;
+ std::string errorString() const override;
+ bool commit(const std::string &filename = "", bool overwrite = false) override;
++ std::string commitUR(int max_fragment_length = 130) override;
+ uint64_t amount() const override;
+ uint64_t dust() const override;
+ uint64_t fee() const override;
+diff --git a/src/wallet/api/unsigned_transaction.cpp b/src/wallet/api/unsigned_transaction.cpp
+index 6165a2240..fd03e959d 100644
+--- a/src/wallet/api/unsigned_transaction.cpp
++++ b/src/wallet/api/unsigned_transaction.cpp
+@@ -40,6 +40,8 @@
+ #include <sstream>
+ #include <boost/format.hpp>
+
++#include "bc-ur/src/bc-ur.hpp"
++
+ using namespace std;
+
+ namespace Monero {
+@@ -96,6 +98,46 @@ bool UnsignedTransactionImpl::sign(const std::string &signedFileName)
+ return true;
+ }
+
++std::string UnsignedTransactionImpl::signUR(int max_fragment_length)
++{
++ if(m_wallet.watchOnly())
++ {
++ m_errorString = tr("This is a watch only wallet");
++ m_status = Status_Error;
++ return "";
++ }
++ std::vector<tools::wallet2::pending_tx> ptx;
++ try
++ {
++ tools::wallet2::signed_tx_set signed_txes;
++ std::string signedTx = m_wallet.m_wallet->sign_tx_dump_to_str(m_unsigned_tx_set, ptx, signed_txes);
++ if (signedTx.empty())
++ {
++ m_errorString = tr("Failed to sign transaction");
++ m_status = Status_Error;
++ return "";
++ }
++ auto urMessage = ur::string_to_bytes(signedTx);
++ ur::ByteVector cbor;
++ ur::CborLite::encodeBytes(cbor, urMessage);
++ std::string type = "xmr-txsigned";
++ ur::UR urData = ur::UR(type, cbor);
++ auto encoder = ur::UREncoder(urData, max_fragment_length);
++ std::string output;
++ for(size_t i = 0; i < encoder.seq_len(); i++) {
++ output.append("\n"+encoder.next_part());
++ }
++ return output;
++ }
++ catch (const std::exception &e)
++ {
++ m_errorString = string(tr("Failed to sign transaction")) + e.what();
++ m_status = Status_Error;
++ return "";
++ }
++ return "";
++}
++
+ //----------------------------------------------------------------------------------------------------
+ bool UnsignedTransactionImpl::checkLoadedTx(const std::function<size_t()> get_num_txes, const std::function<const tools::wallet2::tx_construction_data&(size_t)> &get_tx, const std::string &extra_message)
+ {
+diff --git a/src/wallet/api/unsigned_transaction.h b/src/wallet/api/unsigned_transaction.h
+index 30065a7fa..a94b23f75 100644
+--- a/src/wallet/api/unsigned_transaction.h
++++ b/src/wallet/api/unsigned_transaction.h
+@@ -53,6 +53,7 @@ public:
+ uint64_t txCount() const override;
+ // sign txs and save to file
+ bool sign(const std::string &signedFileName) override;
++ std::string signUR(int max_fragment_length = 130) override;
+ std::string confirmationMessage() const override {return m_confirmationMessage;}
+ uint64_t minMixinCount() const override;
+
+diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
+index 89df5c517..3fcd6f332 100644
+--- a/src/wallet/api/wallet.cpp
++++ b/src/wallet/api/wallet.cpp
+@@ -47,6 +47,7 @@
+
+ #include <boost/locale.hpp>
+ #include <boost/filesystem.hpp>
++#include "bc-ur/src/bc-ur.hpp"
+
+ using namespace std;
+ using namespace cryptonote;
+@@ -1066,6 +1067,24 @@ uint64_t WalletImpl::unlockedBalance(uint32_t accountIndex) const
+ return m_wallet->unlocked_balance(accountIndex, false);
+ }
+
++uint64_t WalletImpl::viewOnlyBalance(uint32_t accountIndex, const std::vector<std::string> &key_images) const
++{
++ clearStatus();
++
++ std::vector<crypto::key_image> kis;
++ for (const auto &key_image : key_images) {
++ crypto::key_image ki;
++ if (!epee::string_tools::hex_to_pod(key_image, ki))
++ {
++ setStatusError(tr("failed to parse key image"));
++ return 0;
++ }
++ kis.push_back(ki);
++ }
++
++ return m_wallet->view_only_balance(accountIndex, kis);
++}
++
+ uint64_t WalletImpl::blockChainHeight() const
+ {
+ if(m_wallet->light_wallet()) {
+@@ -1208,6 +1227,61 @@ UnsignedTransaction *WalletImpl::loadUnsignedTx(const std::string &unsigned_file
+ return transaction;
+ }
+
++
++UnsignedTransaction *WalletImpl::loadUnsignedTxUR(const std::string &input) {
++ clearStatus();
++ UnsignedTransactionImpl * transaction = new UnsignedTransactionImpl(*this);
++ auto decoder = ur::URDecoder();
++
++ std::string delimiter = "\n";
++ std::string inp = input;
++ size_t pos = 0;
++ std::string token;
++ while ((pos = inp.find(delimiter)) != std::string::npos) {
++ token = inp.substr(0, pos);
++ decoder.receive_part(token);
++ inp.erase(0, pos + delimiter.length());
++ }
++ decoder.receive_part(inp);
++
++ if (decoder.is_failure()) {
++ setStatusError(decoder.result_error().what());
++ transaction->m_status = UnsignedTransaction::Status::Status_Error;
++ transaction->m_errorString = errorString();
++ return transaction;
++ }
++
++ if (!decoder.is_complete()) {
++ setStatusError("file ended but ur didn't complete");
++ transaction->m_status = UnsignedTransaction::Status::Status_Error;
++ transaction->m_errorString = errorString();
++ return transaction;
++ }
++
++ std::string data;
++ auto cbor = decoder.result_ur().cbor();
++ auto i = cbor.begin();
++ auto end = cbor.end();
++ ur::CborLite::decodeBytes(i, end, data);
++
++ if (checkBackgroundSync("cannot load tx") || !m_wallet->parse_unsigned_tx_from_str(data, transaction->m_unsigned_tx_set)){
++ setStatusError(tr("Failed to load unsigned transactions"));
++ transaction->m_status = UnsignedTransaction::Status::Status_Error;
++ transaction->m_errorString = errorString();
++
++ return transaction;
++ }
++
++ // Check tx data and construct confirmation message
++ std::string extra_message;
++ if (!std::get<2>(transaction->m_unsigned_tx_set.transfers).empty())
++ extra_message = (boost::format("%u outputs to import. ") % (unsigned)std::get<2>(transaction->m_unsigned_tx_set.transfers).size()).str();
++ transaction->checkLoadedTx([&transaction](){return transaction->m_unsigned_tx_set.txes.size();}, [&transaction](size_t n)->const tools::wallet2::tx_construction_data&{return transaction->m_unsigned_tx_set.txes[n];}, extra_message);
++ setStatus(transaction->status(), transaction->errorString());
++
++ return transaction;
++}
++
+ bool WalletImpl::submitTransaction(const string &fileName) {
+ clearStatus();
+ if (checkBackgroundSync("cannot submit tx"))
+@@ -1219,7 +1293,7 @@ bool WalletImpl::submitTransaction(const string &fileName) {
+ setStatus(Status_Ok, tr("Failed to load transaction from file"));
+ return false;
+ }
+-
++
+ if(!transaction->commit()) {
+ setStatusError(transaction->m_errorString);
+ return false;
+@@ -1228,6 +1302,61 @@ bool WalletImpl::submitTransaction(const string &fileName) {
+ return true;
+ }
+
++
++bool WalletImpl::submitTransactionUR(const string &input) {
++ clearStatus();
++ auto decoder = ur::URDecoder();
++
++ std::string delimiter = "\n";
++ std::string inp = input;
++ size_t pos = 0;
++ std::string token;
++ while ((pos = inp.find(delimiter)) != std::string::npos) {
++ token = inp.substr(0, pos);
++ decoder.receive_part(token);
++ inp.erase(0, pos + delimiter.length());
++ }
++ decoder.receive_part(inp);
++
++ if (decoder.is_failure()) {
++ setStatusError(decoder.result_error().what());
++ return false;
++ }
++
++ if (!decoder.is_complete()) {
++ setStatusError("file ended but ur didn't complete");
++ return false;
++ }
++
++ std::string data;
++ auto cbor = decoder.result_ur().cbor();
++ auto i = cbor.begin();
++ auto end = cbor.end();
++ ur::CborLite::decodeBytes(i, end, data);
++ if (checkBackgroundSync("cannot submit tx"))
++ return false;
++ std::unique_ptr<PendingTransactionImpl> transaction(new PendingTransactionImpl(*this));
++
++ bool r = m_wallet->parse_tx_from_str(data, transaction->m_pending_tx, NULL);
++ if (!r) {
++ setStatus(Status_Ok, tr("Failed to load transaction from file"));
++ return false;
++ }
++
++ if(!transaction->commit()) {
++ setStatusError(transaction->m_errorString);
++ return false;
++ }
++
++ return true;
++}
++
++
++bool WalletImpl::hasUnknownKeyImages() const
++{
++ return m_wallet->has_unknown_key_images();
++}
++
+ bool WalletImpl::exportKeyImages(const string &filename, bool all)
+ {
+ if (m_wallet->watch_only())
+@@ -1255,6 +1384,39 @@ bool WalletImpl::exportKeyImages(const string &filename, bool all)
+ return true;
+ }
+
++std::string WalletImpl::exportKeyImagesUR(size_t max_fragment_length, bool all)
++{
++ if (m_wallet->watch_only())
++ {
++ setStatusError(tr("Wallet is view only"));
++ return "";
++ }
++ if (checkBackgroundSync("cannot export key images"))
++ return "";
++
++ try
++ {
++ std::string keyImages = m_wallet->export_key_images_str(all);
++ auto urMessage = ur::string_to_bytes(keyImages);
++ ur::ByteVector cbor;
++ ur::CborLite::encodeBytes(cbor, urMessage);
++ ur::UR urData = ur::UR("xmr-keyimage", cbor);
++ auto encoder = ur::UREncoder(urData, max_fragment_length);
++ std::string output;
++ for(size_t i = 0; i < encoder.seq_len(); i++) {
++ output.append("\n"+encoder.next_part());
++ }
++ return output;
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Error exporting key images: " << e.what());
++ setStatusError(e.what());
++ return "";
++ }
++ return "";
++}
++
+ bool WalletImpl::importKeyImages(const string &filename)
+ {
+ if (checkBackgroundSync("cannot import key images"))
+@@ -1280,6 +1442,62 @@ bool WalletImpl::importKeyImages(const string &filename)
+ return true;
+ }
+
++
++bool WalletImpl::importKeyImagesUR(const string &input)
++{
++ if (checkBackgroundSync("cannot import key images"))
++ return false;
++ if (!trustedDaemon()) {
++ setStatusError(tr("Key images can only be imported with a trusted daemon"));
++ return false;
++ }
++ try
++ {
++ auto decoder = ur::URDecoder();
++ std::string delimiter = "\n";
++ std::string inp = input;
++ size_t pos = 0;
++ std::string token;
++ while ((pos = inp.find(delimiter)) != std::string::npos) {
++ token = inp.substr(0, pos);
++ decoder.receive_part(token);
++ inp.erase(0, pos + delimiter.length());
++ }
++ decoder.receive_part(inp);
++
++ if (decoder.is_failure()) {
++ setStatusError(decoder.result_error().what());
++ return false;
++ }
++
++ if (!decoder.is_complete()) {
++ setStatusError("file ended but ur didn't complete");
++ return false;
++ }
++
++ std::string data;
++ auto cbor = decoder.result_ur().cbor();
++ auto i = cbor.begin();
++ auto end = cbor.end();
++ ur::CborLite::decodeBytes(i, end, data);
++
++ uint64_t spent = 0, unspent = 0;
++
++ uint64_t height = m_wallet->import_key_images_str(data, spent, unspent);
++ LOG_PRINT_L2("Signed key images imported to height " << height << ", "
++ << print_money(spent) << " spent, " << print_money(unspent) << " unspent");
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Error exporting key images: " << e.what());
++ setStatusError(string(tr("Failed to import key images: ")) + e.what());
++ return false;
++ }
++
++ return true;
++}
++
++
+ bool WalletImpl::exportOutputs(const string &filename, bool all)
+ {
+ if (checkBackgroundSync("cannot export outputs"))
+@@ -1312,6 +1530,40 @@ bool WalletImpl::exportOutputs(const string &filename, bool all)
+ return true;
+ }
+
++std::string WalletImpl::exportOutputsUR(size_t max_fragment_length, bool all)
++{
++
++ if (checkBackgroundSync("cannot export outputs"))
++ return "";
++ if (m_wallet->key_on_device())
++ {
++ setStatusError(string(tr("Not supported on HW wallets.")));
++ return "";
++ }
++
++ try
++ {
++ std::string data = m_wallet->export_outputs_to_str(all);
++ auto urMessage = ur::string_to_bytes(data);
++ ur::ByteVector cbor;
++ ur::CborLite::encodeBytes(cbor, urMessage);
++ ur::UR urData = ur::UR("xmr-output", cbor);
++ auto encoder = ur::UREncoder(urData, max_fragment_length);
++ std::string output;
++ for(size_t i = 0; i < encoder.seq_len(); i++) {
++ output.append("\n"+encoder.next_part());
++ }
++ return output;
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Error exporting outputs: " << e.what());
++ setStatusError(string(tr("Error exporting outputs: ")) + e.what());
++ return "";
++ }
++}
++
++
+ bool WalletImpl::importOutputs(const string &filename)
+ {
+ if (checkBackgroundSync("cannot import outputs"))
+@@ -1346,6 +1598,61 @@ bool WalletImpl::importOutputs(const string &filename)
+ return true;
+ }
+
++
++bool WalletImpl::importOutputsUR(const string &input)
++{
++ if (checkBackgroundSync("cannot import outputs"))
++ return false;
++ if (m_wallet->key_on_device())
++ {
++ setStatusError(string(tr("Not supported on HW wallets.")));
++ return false;
++ }
++
++ try
++ {
++ auto decoder = ur::URDecoder();
++
++ std::string delimiter = "\n";
++ std::string inp = input;
++ size_t pos = 0;
++ std::string token;
++ while ((pos = inp.find(delimiter)) != std::string::npos) {
++ token = inp.substr(0, pos);
++ decoder.receive_part(token);
++ inp.erase(0, pos + delimiter.length());
++ }
++ decoder.receive_part(inp);
++
++ if (decoder.is_failure()) {
++ setStatusError(decoder.result_error().what());
++ return false;
++ }
++
++ if (!decoder.is_complete()) {
++ setStatusError("file ended but ur didn't complete");
++ return false;
++ }
++
++ std::string data;
++ auto cbor = decoder.result_ur().cbor();
++ auto i = cbor.begin();
++ auto end = cbor.end();
++ ur::CborLite::decodeBytes(i, end, data);
++ size_t n_outputs = m_wallet->import_outputs_from_str(std::string(data));
++ LOG_PRINT_L2(std::to_string(n_outputs) << " outputs imported");
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Failed to import outputs: " << e.what());
++ setStatusError(string(tr("Failed to import outputs: ")) + e.what());
++ return false;
++ }
++
++ return true;
++}
++
++
+ bool WalletImpl::scanTransactions(const std::vector<std::string> &txids)
+ {
+ if (checkBackgroundSync("cannot scan transactions"))
+diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
+index ac7ce2f6a..edf8bb8ce 100644
+--- a/src/wallet/api/wallet.h
++++ b/src/wallet/api/wallet.h
+@@ -112,6 +112,7 @@ public:
+ bool setProxy(const std::string &address) override;
+ uint64_t balance(uint32_t accountIndex = 0) const override;
+ uint64_t unlockedBalance(uint32_t accountIndex = 0) const override;
++ uint64_t viewOnlyBalance(uint32_t accountIndex, const std::vector<std::string> &key_images) const override;
+ uint64_t blockChainHeight() const override;
+ uint64_t approximateBlockChainHeight() const override;
+ uint64_t estimateBlockChainHeight() const override;
+@@ -164,11 +165,18 @@ public:
+ std::set<uint32_t> subaddr_indices = {}) override;
+ virtual PendingTransaction * createSweepUnmixableTransaction() override;
+ bool submitTransaction(const std::string &fileName) override;
++ bool submitTransactionUR(const std::string &input) override;
+ virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) override;
++ virtual UnsignedTransaction * loadUnsignedTxUR(const std::string &input) override;
++ bool hasUnknownKeyImages() const override;
+ bool exportKeyImages(const std::string &filename, bool all = false) override;
++ std::string exportKeyImagesUR(size_t max_fragment_length, bool all = false) override;
+ bool importKeyImages(const std::string &filename) override;
++ bool importKeyImagesUR(const std::string &input) override;
+ bool exportOutputs(const std::string &filename, bool all = false) override;
++ std::string exportOutputsUR(size_t max_fragment_length, bool all) override;
+ bool importOutputs(const std::string &filename) override;
++ bool importOutputsUR(const std::string &filename) override;
+ bool scanTransactions(const std::vector<std::string> &txids) override;
+
+ bool setupBackgroundSync(const BackgroundSyncType background_sync_type, const std::string &wallet_password, const optional<std::string> &background_cache_password = optional<std::string>()) override;
+diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
+index e349df176..764adbfbf 100644
+--- a/src/wallet/api/wallet2_api.h
++++ b/src/wallet/api/wallet2_api.h
+@@ -91,6 +91,7 @@ struct PendingTransaction
+ virtual std::string errorString() const = 0;
+ // commit transaction or save to file if filename is provided.
+ virtual bool commit(const std::string &filename = "", bool overwrite = false) = 0;
++ virtual std::string commitUR(int max_fragment_length = 130) = 0;
+ virtual uint64_t amount() const = 0;
+ virtual uint64_t dust() const = 0;
+ virtual uint64_t fee() const = 0;
+@@ -160,7 +161,8 @@ struct UnsignedTransaction
+ * @param signedFileName
+ * return - true on success
+ */
+- virtual bool sign(const std::string &signedFileName) = 0;
++ virtual bool sign(const std::string &signedFileName) = 0;
++ virtual std::string signUR(int max_fragment_length = 130) = 0;
+ };
+
+ /**
+@@ -626,6 +628,7 @@ struct Wallet
+ result += unlockedBalance(i);
+ return result;
+ }
++ virtual uint64_t viewOnlyBalance(uint32_t accountIndex, const std::vector<std::string> &key_images = {}) const = 0;
+
+ /**
+ * @brief watchOnly - checks if wallet is watch only
+@@ -884,13 +887,15 @@ struct Wallet
+ * after object returned
+ */
+ virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0;
+-
+- /*!
++ virtual UnsignedTransaction * loadUnsignedTxUR(const std::string &input) = 0;
++
++ /*!
+ * \brief submitTransaction - submits transaction in signed tx file
+ * \return - true on success
+ */
+ virtual bool submitTransaction(const std::string &fileName) = 0;
+-
++ virtual bool submitTransactionUR(const std::string &input) = 0;
++
+
+ /*!
+ * \brief disposeTransaction - destroys transaction object
+@@ -906,6 +911,8 @@ struct Wallet
+ virtual uint64_t estimateTransactionFee(const std::vector<std::pair<std::string, uint64_t>> &destinations,
+ PendingTransaction::Priority priority) const = 0;
+
++ virtual bool hasUnknownKeyImages() const = 0;
++
+ /*!
+ * \brief exportKeyImages - exports key images to file
+ * \param filename
+@@ -913,20 +920,22 @@ struct Wallet
+ * \return - true on success
+ */
+ virtual bool exportKeyImages(const std::string &filename, bool all = false) = 0;
+-
++ virtual std::string exportKeyImagesUR(size_t max_fragment_length, bool all = false) = 0;
+ /*!
+ * \brief importKeyImages - imports key images from file
+ * \param filename
+ * \return - true on success
+ */
+ virtual bool importKeyImages(const std::string &filename) = 0;
++ virtual bool importKeyImagesUR(const std::string &input) = 0;
+
+ /*!
+- * \brief importOutputs - exports outputs to file
++ * \brief exportOutputs - exports outputs to file
+ * \param filename
+ * \return - true on success
+ */
+ virtual bool exportOutputs(const std::string &filename, bool all = false) = 0;
++ virtual std::string exportOutputsUR(size_t max_fragment_length, bool all = false) = 0;
+
+ /*!
+ * \brief importOutputs - imports outputs from file
+@@ -934,6 +943,7 @@ struct Wallet
+ * \return - true on success
+ */
+ virtual bool importOutputs(const std::string &filename) = 0;
++ virtual bool importOutputsUR(const std::string &filename) = 0;
+
+ /*!
+ * \brief scanTransactions - scan a list of transaction ids, this operation may reveal the txids to the remote node and affect your privacy
+diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
+index b827b826f..765cefb32 100644
+--- a/src/wallet/wallet2.cpp
++++ b/src/wallet/wallet2.cpp
+@@ -948,6 +948,16 @@ uint32_t get_subaddress_clamped_sum(uint32_t idx, uint32_t extra)
+ return idx + extra;
+ }
+
++bool is_preferred_input(const std::vector<crypto::key_image>& preferred_input_list, const crypto::key_image& input) {
++ if (!preferred_input_list.empty()) {
++ auto it = std::find(preferred_input_list.begin(), preferred_input_list.end(), input);
++ if (it == preferred_input_list.end()) {
++ return false;
++ }
++ }
++ return true;
++}
++
+ static void setup_shim(hw::wallet_shim * shim, tools::wallet2 * wallet)
+ {
+ shim->get_tx_pub_key_from_received_outs = std::bind(&tools::wallet2::get_tx_pub_key_from_received_outs, wallet, std::placeholders::_1);
+@@ -6997,6 +7007,25 @@ uint64_t wallet2::unlocked_balance(uint32_t index_major, bool strict, uint64_t *
+ return amount;
+ }
+ //----------------------------------------------------------------------------------------------------
++uint64_t wallet2::view_only_balance(uint32_t index_major, const std::vector<crypto::key_image>& selected_inputs)
++{
++ uint64_t amount = 0;
++ for (const auto &td : m_transfers) {
++ if (is_preferred_input(selected_inputs, td.m_key_image) &&
++ !is_spent(td, false) &&
++ !td.m_frozen &&
++ !td.m_key_image_partial &&
++ td.m_key_image_known &&
++ td.is_rct() &&
++ is_transfer_unlocked(td) &&
++ td.m_subaddr_index.major == index_major)
++ {
++ amount += td.m_amount;
++ }
++ }
++ return amount;
++}
++//----------------------------------------------------------------------------------------------------
+ std::map<uint32_t, uint64_t> wallet2::balance_per_subaddress(uint32_t index_major, bool strict) const
+ {
+ std::map<uint32_t, uint64_t> amount_per_subaddr;
+@@ -7848,9 +7877,7 @@ bool wallet2::sign_tx(unsigned_tx_set &exported_txs, std::vector<wallet2::pendin
+ crypto::key_derivation derivation;
+ std::vector<crypto::key_derivation> additional_derivations;
+
+- // compute public keys from out secret keys
+- crypto::public_key tx_pub_key;
+- crypto::secret_key_to_public_key(txs[n].tx_key, tx_pub_key);
++ crypto::public_key tx_pub_key = get_tx_pub_key_from_extra(tx);
+ std::vector<crypto::public_key> additional_tx_pub_keys;
+ for (const crypto::secret_key &skey: txs[n].additional_tx_keys)
+ {
+@@ -11246,7 +11273,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
+ MDEBUG("Ignoring output " << i << " of amount " << print_money(td.amount()) << " which is below fractional threshold " << print_money(fractional_threshold));
+ continue;
+ }
+- if (!is_spent(td, false) && !td.m_frozen && !td.m_key_image_partial && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
++ if (!is_spent(td, false) && !td.m_frozen && !td.m_key_image_partial && td.m_key_image_known && (use_rct ? true : !td.is_rct()) && is_transfer_unlocked(td) && td.m_subaddr_index.major == subaddr_account && subaddr_indices.count(td.m_subaddr_index.minor) == 1)
+ {
+ if (td.amount() > m_ignore_outputs_above || td.amount() < m_ignore_outputs_below)
+ {
+@@ -11296,9 +11323,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
+
+ LOG_PRINT_L2("Starting with " << num_nondust_outputs << " non-dust outputs and " << num_dust_outputs << " dust outputs");
+
+- if (unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty())
+- return std::vector<wallet2::pending_tx>();
++ // use tobotoht's code path on view-only wallet, otherwise default to upstream
++ bool throwOnNoEnotes = m_account.get_device().device_protocol() == hw::device::PROTOCOL_COLD || m_watch_only || m_multisig || m_is_background_wallet;
+
++ if (throwOnNoEnotes) {
++ THROW_WALLET_EXCEPTION_IF(unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty(), error::wallet_internal_error, "No enotes available to spend")
++ } else {
++ if (unused_dust_indices_per_subaddr.empty() && unused_transfers_indices_per_subaddr.empty())
++ return std::vector<wallet2::pending_tx>();
++ }
+ // if empty, put dummy entry so that the front can be referenced later in the loop
+ if (unused_dust_indices_per_subaddr.empty())
+ unused_dust_indices_per_subaddr.push_back({});
+@@ -13934,33 +13967,40 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle
+
+ bool wallet2::export_key_images(const std::string &filename, bool all) const
+ {
+- PERF_TIMER(export_key_images);
+- std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = export_key_images(all);
+- std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC));
+- const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address;
+- const uint32_t offset = ski.first;
++ std::string data = export_key_images_str(all);
++ return save_to_file(filename, data);
++}
+
+- std::string data;
+- data.reserve(4 + ski.second.size() * (sizeof(crypto::key_image) + sizeof(crypto::signature)) + 2 * sizeof(crypto::public_key));
+- data.resize(4);
+- data[0] = offset & 0xff;
+- data[1] = (offset >> 8) & 0xff;
+- data[2] = (offset >> 16) & 0xff;
+- data[3] = (offset >> 24) & 0xff;
+- data += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key));
+- data += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key));
+- for (const auto &i: ski.second)
+- {
+- data += std::string((const char *)&i.first, sizeof(crypto::key_image));
+- data += std::string((const char *)&i.second, sizeof(crypto::signature));
+- }
++std::string wallet2::export_key_images_str(bool all) const
++{
++ PERF_TIMER(export_key_images);
++ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = export_key_images(all);
++ std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC));
++ const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address;
++ const uint32_t offset = ski.first;
+
+- // encrypt data, keep magic plaintext
+- PERF_TIMER(export_key_images_encrypt);
+- std::string ciphertext = encrypt_with_view_secret_key(data);
+- return save_to_file(filename, magic + ciphertext);
++ std::string data;
++ data.reserve(4 + ski.second.size() * (sizeof(crypto::key_image) + sizeof(crypto::signature)) + 2 * sizeof(crypto::public_key));
++ data.resize(4);
++ data[0] = offset & 0xff;
++ data[1] = (offset >> 8) & 0xff;
++ data[2] = (offset >> 16) & 0xff;
++ data[3] = (offset >> 24) & 0xff;
++ data += std::string((const char *)&keys.m_spend_public_key, sizeof(crypto::public_key));
++ data += std::string((const char *)&keys.m_view_public_key, sizeof(crypto::public_key));
++ for (const auto &i: ski.second)
++ {
++ data += std::string((const char *)&i.first, sizeof(crypto::key_image));
++ data += std::string((const char *)&i.second, sizeof(crypto::signature));
++ }
++
++ // encrypt data, keep magic plaintext
++ PERF_TIMER(export_key_images_encrypt);
++ std::string ciphertext = encrypt_with_view_secret_key(data);
++ return magic + ciphertext;
+ }
+
++
+ //----------------------------------------------------------------------------------------------------
+ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> wallet2::export_key_images(bool all) const
+ {
+@@ -14015,53 +14055,60 @@ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>
+ return std::make_pair(offset, ski);
+ }
+
+-uint64_t wallet2::import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent)
++uint64_t wallet2::import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent) {
++ std::string data;
++
++ bool r = load_from_file(filename, data);
++
++ THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, std::string(tr("failed to read file ")) + filename);
++
++ return import_key_images_str(data, spent, unspent);
++}
++
++uint64_t wallet2::import_key_images_str(const std::string &data, uint64_t &spent, uint64_t &unspent)
+ {
+ PERF_TIMER(import_key_images_fsu);
+- std::string data;
+- bool r = load_from_file(filename, data);
+-
+- THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, std::string(tr("failed to read file ")) + filename);
++ std::string data_local = data;
+
+ const size_t magiclen = strlen(KEY_IMAGE_EXPORT_FILE_MAGIC);
+ if (data.size() < magiclen || memcmp(data.data(), KEY_IMAGE_EXPORT_FILE_MAGIC, magiclen))
+ {
+- THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string("Bad key image export file magic in ") + filename);
++ THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string("Bad key image export file magic"));
+ }
+
+ try
+ {
+ PERF_TIMER(import_key_images_decrypt);
+- data = decrypt_with_view_secret_key(std::string(data, magiclen));
++ data_local = decrypt_with_view_secret_key(std::string(data, magiclen));
+ }
+ catch (const std::exception &e)
+ {
+- THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string("Failed to decrypt ") + filename + ": " + e.what());
++ THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string("Failed to decrypt ") + ": " + e.what());
+ }
+
+ const size_t headerlen = 4 + 2 * sizeof(crypto::public_key);
+- THROW_WALLET_EXCEPTION_IF(data.size() < headerlen, error::wallet_internal_error, std::string("Bad data size from file ") + filename);
+- const uint32_t offset = (uint8_t)data[0] | (((uint8_t)data[1]) << 8) | (((uint8_t)data[2]) << 16) | (((uint8_t)data[3]) << 24);
+- const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data[4];
+- const crypto::public_key &public_view_key = *(const crypto::public_key*)&data[4 + sizeof(crypto::public_key)];
++ THROW_WALLET_EXCEPTION_IF(data_local.size() < headerlen, error::wallet_internal_error, std::string("Bad data size from file "));
++ const uint32_t offset = (uint8_t)data_local[0] | (((uint8_t)data_local[1]) << 8) | (((uint8_t)data_local[2]) << 16) | (((uint8_t)data_local[3]) << 24);
++ const crypto::public_key &public_spend_key = *(const crypto::public_key*)&data_local[4];
++ const crypto::public_key &public_view_key = *(const crypto::public_key*)&data_local[4 + sizeof(crypto::public_key)];
+ const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address;
+ if (public_spend_key != keys.m_spend_public_key || public_view_key != keys.m_view_public_key)
+ {
+- THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string( "Key images from ") + filename + " are for a different account");
++ THROW_WALLET_EXCEPTION(error::wallet_internal_error, std::string( "Key images from ") + " are for a different account");
+ }
+ THROW_WALLET_EXCEPTION_IF(offset > m_transfers.size(), error::wallet_internal_error, "Offset larger than known outputs");
+
+ const size_t record_size = sizeof(crypto::key_image) + sizeof(crypto::signature);
+- THROW_WALLET_EXCEPTION_IF((data.size() - headerlen) % record_size,
+- error::wallet_internal_error, std::string("Bad data size from file ") + filename);
+- size_t nki = (data.size() - headerlen) / record_size;
++ THROW_WALLET_EXCEPTION_IF((data_local.size() - headerlen) % record_size,
++ error::wallet_internal_error, std::string("Bad data size from file "));
++ size_t nki = (data_local.size() - headerlen) / record_size;
+
+ std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
+ ski.reserve(nki);
+ for (size_t n = 0; n < nki; ++n)
+ {
+- crypto::key_image key_image = *reinterpret_cast<const crypto::key_image*>(&data[headerlen + n * record_size]);
+- crypto::signature signature = *reinterpret_cast<const crypto::signature*>(&data[headerlen + n * record_size + sizeof(crypto::key_image)]);
++ crypto::key_image key_image = *reinterpret_cast<const crypto::key_image*>(&data_local[headerlen + n * record_size]);
++ crypto::signature signature = *reinterpret_cast<const crypto::signature*>(&data_local[headerlen + n * record_size + sizeof(crypto::key_image)]);
+
+ ski.push_back(std::make_pair(key_image, signature));
+ }
+diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
+index daad1e940..a752f15b9 100644
+--- a/src/wallet/wallet2.h
++++ b/src/wallet/wallet2.h
+@@ -1157,6 +1157,7 @@ private:
+ // locked & unlocked balance of given or current subaddress account
+ uint64_t balance(uint32_t subaddr_index_major, bool strict) const;
+ uint64_t unlocked_balance(uint32_t subaddr_index_major, bool strict, uint64_t *blocks_to_unlock = NULL, uint64_t *time_to_unlock = NULL);
++ uint64_t view_only_balance(uint32_t index_major, const std::vector<crypto::key_image>& selected_inputs = {});
+ // locked & unlocked balance per subaddress of given or current subaddress account
+ std::map<uint32_t, uint64_t> balance_per_subaddress(uint32_t subaddr_index_major, bool strict) const;
+ std::map<uint32_t, std::pair<uint64_t, std::pair<uint64_t, uint64_t>>> unlocked_balance_per_subaddress(uint32_t subaddr_index_major, bool strict);
+@@ -1631,9 +1632,11 @@ private:
+ std::tuple<size_t, crypto::hash, std::vector<crypto::hash>> export_blockchain() const;
+ void import_blockchain(const std::tuple<size_t, crypto::hash, std::vector<crypto::hash>> &bc);
+ bool export_key_images(const std::string &filename, bool all = false) const;
++ std::string export_key_images_str(bool all) const;
+ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> export_key_images(bool all = false) const;
+ uint64_t import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, size_t offset, uint64_t &spent, uint64_t &unspent, bool check_spent = true);
+ uint64_t import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent);
++ uint64_t import_key_images_str(const std::string &data, uint64_t &spent, uint64_t &unspent);
+ bool import_key_images(std::vector<crypto::key_image> key_images, size_t offset=0, boost::optional<std::unordered_set<size_t>> selected_transfers=boost::none);
+ bool import_key_images(signed_tx_set & signed_tx, size_t offset=0, bool only_selected_transfers=false);
+ crypto::public_key get_tx_pub_key_from_received_outs(const tools::wallet2::transfer_details &td) const;
+--
+2.48.1
+