summaryrefslogtreecommitdiff
path: root/patches/monero/0021-trezor-import-export-sign-submit-functions.patch
diff options
context:
space:
mode:
authorCzarek Nakamoto <cyjan@mrcyjanek.net>2026-07-23 09:51:20 +0200
committerCzarek Nakamoto <cyjan@mrcyjanek.net>2026-08-03 14:53:50 +0200
commitc765eca1fbbfde6165d5ed5e56276878b3496c77 (patch)
tree5b9ef6e90361595b70fb6076d92bc41b7c6b7abc /patches/monero/0021-trezor-import-export-sign-submit-functions.patch
parent5952bef2ec01b0b7e57c11613cbdc081dcd727c5 (diff)
use FCMP branch, bump simplybs, rebase patchescyjan-fcmp-v2
Diffstat (limited to 'patches/monero/0021-trezor-import-export-sign-submit-functions.patch')
-rw-r--r--patches/monero/0021-trezor-import-export-sign-submit-functions.patch1099
1 files changed, 1099 insertions, 0 deletions
diff --git a/patches/monero/0021-trezor-import-export-sign-submit-functions.patch b/patches/monero/0021-trezor-import-export-sign-submit-functions.patch
new file mode 100644
index 0000000..7488d68
--- /dev/null
+++ b/patches/monero/0021-trezor-import-export-sign-submit-functions.patch
@@ -0,0 +1,1099 @@
+From ece9b55a6c6c34a2aaf87e10e059cc4c38a0f1b8 Mon Sep 17 00:00:00 2001
+From: Czarek Nakamoto <cyjan@mrcyjanek.net>
+Date: Wed, 13 May 2026 11:35:01 -0400
+Subject: [PATCH 21/22] trezor import/export/sign/submit functions
+
+1) This PR fixes protobuf detection, by removing checks as they were
+failing on some platforms, essentially removing Protobuf_COMPILE_TEST_PASSED
+
+2) Wallet::exportTrezorTdis() returns Trezor connect-ish JSON
+Exact JSON schema is shown below, path and networkType is ommited
+
+https://connect.trezor.io/9/methods/monero/moneroKeyImageSync/
+
+```json
+{
+ tdis: [
+ {
+ out_key: '0a09ab658fca97610a38b8a5c206a0709db435341c31a9d40150df7e52440ac6',
+ tx_pub_key: 'da13cd8f4cc2c4f769d88b734d71cfdc0e43d01a20eb7bff6553fd67cb2ed37e',
+ additional_tx_pub_keys: [
+ 'aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd',
+ ],
+ internal_output_index: 1,
+ sub_addr_major: 0,
+ sub_addr_minor: 0,
+ },
+ ],
+}
+```
+
+3) Wallet::importTrezorEncryptedKeyImagesJson("{...}")
+
+Expects Trezor connect MoneroKeyImageSyncResult JSON
+
+```json
+{
+ success: true,
+ payload: {
+ key_images: [
+ {
+ iv: string, // 12-byte initialization vector/nonce
+ key_image: string, // Encrypted blob
+ },
+ // ... more key images
+ ],
+ signature: string, // 32-byte encryption key
+ }
+}
+```
+
+4) PendingTransaction::commitTrezor(0)
+New method that returns std::string with Trezor connect compatible JSON, as
+shown below and at the link
+
+https://connect.trezor.io/9/methods/monero/moneroSignTransaction/
+```json
+{
+ // REQUIRED: Hardened Monero account path (minimum 3 components)
+ path: "m/44'/128'/0'",
+
+ // REQUIRED: Network type
+ networkType: 0, // 0=MAINNET, 1=TESTNET, 2=STAGENET, 3=FAKECHAIN
+
+ // REQUIRED: Transaction data with outputs
+ tsx_data: {
+ version: 1,
+ unlock_time: 0,
+ mixin: 15,
+ fee: 10000000,
+ account: 0,
+ num_inputs: 1,
+ client_version: 3,
+ hard_fork: 16,
+ outputs: [
+ {
+ amount: 1000000000000, // 1.0 XMR in atomic units
+ addr: {
+ spend_public_key:
+ 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
+ view_public_key:
+ 'fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321',
+ },
+ is_subaddress: false,
+ original: 'address_string_here',
+ is_integrated: false,
+ },
+ ],
+ rsig_data: {
+ rsig_type: 1,
+ bp_version: 4,
+ grouping: [],
+ },
+ },
+
+ // REQUIRED: At least one input with ring members
+ inputs: [
+ {
+ outputs: [
+ // Ring member 1 (real output)
+ {
+ idx: 12345,
+ key: {
+ dest: '1111111111111111111111111111111111111111111111111111111111111111',
+ commitment:
+ '2222222222222222222222222222222222222222222222222222222222222222',
+ },
+ },
+ // Ring member 2 (decoy)
+ {
+ idx: 12346,
+ key: {
+ dest: '3333333333333333333333333333333333333333333333333333333333333333',
+ commitment:
+ '4444444444444444444444444444444444444444444444444444444444444444',
+ },
+ },
+ // ... add more ring members (typically 11 total for mixin=10)
+ ],
+ real_output: 0,
+ real_out_tx_key: 'tx_public_key_64_chars',
+ real_out_additional_tx_keys: [],
+ real_output_in_tx_index: 1,
+ amount: 1010000000,
+ rct: true,
+ mask: 'amount_mask_64_chars',
+ subaddr_minor: 0,
+ },
+ ],
+}
+```
+
+5) Transaction assembly is left as an excercises for the reader, after hex
+is assembled you can call Wallet::submitTransactionHex("CAFEBABE")
+
+That's it.
+---
+ cmake/CheckTrezor.cmake | 79 +++-----
+ src/device_trezor/trezor/protocol.cpp | 220 +++++++++++++++++++++
+ src/device_trezor/trezor/protocol.hpp | 8 +
+ src/wallet/api/pending_transaction.cpp | 60 ++++++
+ src/wallet/api/pending_transaction.h | 1 +
+ src/wallet/api/unsigned_transaction.cpp | 2 +
+ src/wallet/api/wallet.cpp | 68 +++++++
+ src/wallet/api/wallet.h | 3 +
+ src/wallet/api/wallet2_api.h | 15 ++
+ src/wallet/wallet2.cpp | 249 ++++++++++++++++++++++++
+ src/wallet/wallet2.h | 5 +-
+ 11 files changed, 661 insertions(+), 49 deletions(-)
+
+diff --git a/cmake/CheckTrezor.cmake b/cmake/CheckTrezor.cmake
+index 718760ace..08eaae58a 100644
+--- a/cmake/CheckTrezor.cmake
++++ b/cmake/CheckTrezor.cmake
+@@ -53,6 +53,14 @@ if (USE_DEVICE_TREZOR)
+ # https://github.com/protocolbuffers/protobuf/issues/14576
+ find_program(Protobuf_PROTOC_EXECUTABLE protoc REQUIRED)
+ set(Protobuf_LIBRARY protobuf::libprotobuf) # Compatibility with FindProtobuf.cmake
++ elseif(NOT Protobuf_LIBRARY AND NOT Protobuf_PROTOC_EXECUTABLE AND NOT Protobuf_INCLUDE_DIR)
++ message(STATUS "Could not find Protobuf")
++ elseif(NOT Protobuf_PROTOC_EXECUTABLE OR NOT EXISTS "${Protobuf_PROTOC_EXECUTABLE}")
++ message(STATUS "Protobuf executable not found: ${Protobuf_PROTOC_EXECUTABLE}")
++ unset(Protobuf_FOUND)
++ elseif(NOT Protobuf_INCLUDE_DIR OR NOT EXISTS "${Protobuf_INCLUDE_DIR}")
++ message(STATUS "Protobuf include dir not found: ${Protobuf_INCLUDE_DIR}")
++ unset(Protobuf_FOUND)
+ else()
+ # Look for FindProtobuf.cmake, provided by CMake
+ find_package(Protobuf)
+@@ -80,68 +88,43 @@ else()
+ message(STATUS "Trezor: support disabled by USE_DEVICE_TREZOR")
+ endif()
+
+-# Protobuf compilation test
+ if(Protobuf_FOUND AND USE_DEVICE_TREZOR)
+- execute_process(COMMAND ${Protobuf_PROTOC_EXECUTABLE} -I "${CMAKE_CURRENT_LIST_DIR}" -I "${Protobuf_INCLUDE_DIR}" "${CMAKE_CURRENT_LIST_DIR}/test-protobuf.proto" --cpp_out ${CMAKE_BINARY_DIR} RESULT_VARIABLE RET OUTPUT_VARIABLE OUT ERROR_VARIABLE ERR)
+- if(RET)
+- trezor_fatal_msg("Trezor: Protobuf test generation failed: ${OUT} ${ERR}")
++ if (NOT "$ENV{TREZOR_PYTHON}" STREQUAL "")
++ set(TREZOR_PYTHON "$ENV{TREZOR_PYTHON}" CACHE INTERNAL "Copied from environment variable TREZOR_PYTHON")
++ else()
++ find_package(Python QUIET COMPONENTS Interpreter)
++ if(Python_Interpreter_FOUND)
++ set(TREZOR_PYTHON "${Python_EXECUTABLE}")
++ endif()
+ endif()
+
+- if(ANDROID)
+- set(CMAKE_TRY_COMPILE_LINKER_FLAGS "${CMAKE_TRY_COMPILE_LINKER_FLAGS} -llog")
+- set(CMAKE_TRY_COMPILE_LINK_LIBRARIES "${CMAKE_TRY_COMPILE_LINK_LIBRARIES} log")
++ if(NOT TREZOR_PYTHON)
++ find_package(PythonInterp)
++ if(PYTHONINTERP_FOUND AND PYTHON_EXECUTABLE)
++ set(TREZOR_PYTHON "${PYTHON_EXECUTABLE}")
++ endif()
+ endif()
+
+- if(USE_DEVICE_TREZOR_PROTOBUF_TEST)
+- if(PROTOBUF_LDFLAGS)
+- set(PROTOBUF_TRYCOMPILE_LINKER "${PROTOBUF_LDFLAGS}")
+- else()
+- set(PROTOBUF_TRYCOMPILE_LINKER "${Protobuf_LIBRARY}")
+- endif()
+-
+- try_compile(Protobuf_COMPILE_TEST_PASSED
+- "${CMAKE_BINARY_DIR}"
+- SOURCES
+- "${CMAKE_BINARY_DIR}/test-protobuf.pb.cc"
+- "${CMAKE_CURRENT_LIST_DIR}/test-protobuf.cpp"
+- CMAKE_FLAGS
+- CMAKE_EXE_LINKER_FLAGS ${CMAKE_TRY_COMPILE_LINKER_FLAGS}
+- "-DINCLUDE_DIRECTORIES=${Protobuf_INCLUDE_DIR};${CMAKE_BINARY_DIR}"
+- "-DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD}"
+- LINK_LIBRARIES "${PROTOBUF_TRYCOMPILE_LINKER}" ${CMAKE_TRY_COMPILE_LINK_LIBRARIES}
+- OUTPUT_VARIABLE OUTPUT
+- )
+- if(NOT Protobuf_COMPILE_TEST_PASSED)
+- trezor_fatal_msg("Trezor: Protobuf Compilation test failed: ${OUTPUT}.")
+- endif()
+- else ()
+- message(STATUS "Trezor: Protobuf Compilation test skipped, build may fail later")
++ if(NOT TREZOR_PYTHON)
++ message(STATUS "Trezor: Python not found")
+ endif()
+ endif()
+
+ # Try to build protobuf messages
+-if(Protobuf_FOUND AND USE_DEVICE_TREZOR)
+- # .proto files to compile
+- set(_proto_files "messages.proto"
+- "messages-common.proto"
+- "messages-management.proto"
+- "messages-monero.proto")
+- if (TREZOR_DEBUG)
+- list(APPEND _proto_files "messages-debug.proto")
+- endif ()
+-
+- set(_proto_include_dir "${CMAKE_CURRENT_LIST_DIR}/../src/device_trezor/trezor/protob")
+- set(_proto_files_absolute)
+- foreach(file IN LISTS _proto_files)
+- list(APPEND _proto_files_absolute "${_proto_include_dir}/${file}")
+- endforeach ()
++if(Protobuf_FOUND AND USE_DEVICE_TREZOR AND TREZOR_PYTHON)
++ set(ENV{PROTOBUF_INCLUDE_DIRS} "${Protobuf_INCLUDE_DIR}")
++ set(ENV{PROTOBUF_PROTOC_EXECUTABLE} "${Protobuf_PROTOC_EXECUTABLE}")
++ set(TREZOR_PROTOBUF_PARAMS "")
++ if (USE_DEVICE_TREZOR_DEBUG)
++ set(TREZOR_PROTOBUF_PARAMS "--debug")
++ endif()
+
+- set(_proto_out_dir "${CMAKE_CURRENT_LIST_DIR}/../src/device_trezor/trezor/messages")
+- execute_process(COMMAND ${Protobuf_PROTOC_EXECUTABLE} --cpp_out "${_proto_out_dir}" "-I${_proto_include_dir}" ${_proto_files_absolute} RESULT_VARIABLE RET OUTPUT_VARIABLE OUT ERROR_VARIABLE ERR)
++ execute_process(COMMAND ${TREZOR_PYTHON} tools/build_protob.py ${TREZOR_PROTOBUF_PARAMS} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../src/device_trezor/trezor RESULT_VARIABLE RET OUTPUT_VARIABLE OUT ERROR_VARIABLE ERR)
+ if(RET)
+ trezor_fatal_msg("Trezor: protobuf messages could not be (re)generated (err=${RET}). OUT: ${OUT}, ERR: ${ERR}.")
+ endif()
+
++ set(_proto_out_dir "${CMAKE_CURRENT_LIST_DIR}/../src/device_trezor/trezor/messages")
+ if(FREEBSD)
+ # FreeBSD defines `minor` in usr/include/sys/types.h which conflicts with this file
+ # https://github.com/trezor/trezor-firmware/issues/4460
+diff --git a/src/device_trezor/trezor/protocol.cpp b/src/device_trezor/trezor/protocol.cpp
+index 52f02be3b..ec4e9cfe9 100644
+--- a/src/device_trezor/trezor/protocol.cpp
++++ b/src/device_trezor/trezor/protocol.cpp
+@@ -29,12 +29,17 @@
+
+ #include "version.h"
+ #include "protocol.hpp"
++#include "string_tools.h"
+ #include <unordered_map>
+ #include <set>
++#include <sstream>
+ #include <utility>
+ #include <boost/endian/conversion.hpp>
+ #include <common/apply_permutation.h>
+ #include <common/json_util.h>
++#include <rapidjson/document.h>
++#include <rapidjson/stringbuffer.h>
++#include <rapidjson/writer.h>
+ #include <crypto/hmac-keccak.h>
+ #include <ringct/rctSigs.h>
+ #include <ringct/bulletproofs.h>
+@@ -439,6 +444,10 @@ namespace tx {
+ }
+ }
+
++ void Signer::export_source_entry(MoneroTransactionSourceEntry *dst, size_t idx, bool need_ring_keys, bool need_ring_indices){
++ set_tx_input(dst, idx, need_ring_keys, need_ring_indices);
++ }
++
+ void Signer::set_tx_input(MoneroTransactionSourceEntry * dst, size_t idx, bool need_ring_keys, bool need_ring_indices){
+ const cryptonote::tx_source_entry & src = cur_tx().sources[idx];
+ const wallet2_basic::transfer_details & transfer = get_source_transfer(idx);
+@@ -1085,6 +1094,217 @@ namespace tx {
+ memwipe(plaintext.get(), keys_len);
+ }
+
++ namespace {
++
++ std::string bin_to_hex_lower(const std::string &bin)
++ {
++ return epee::string_tools::buff_to_hex_nodelimer(bin);
++ }
++
++ rapidjson::Value dest_entry_to_json(const messages::monero::MoneroTransactionDestinationEntry &e, rapidjson::Document::AllocatorType &a)
++ {
++ rapidjson::Value o(rapidjson::kObjectType);
++ if (e.has_amount())
++ o.AddMember("amount", e.amount(), a);
++ if (e.has_addr())
++ {
++ rapidjson::Value addr(rapidjson::kObjectType);
++ const auto &ad = e.addr();
++ if (ad.has_spend_public_key())
++ {
++ const std::string h = bin_to_hex_lower(ad.spend_public_key());
++ addr.AddMember("spend_public_key", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ if (ad.has_view_public_key())
++ {
++ const std::string h = bin_to_hex_lower(ad.view_public_key());
++ addr.AddMember("view_public_key", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ o.AddMember("addr", addr, a);
++ }
++ if (e.has_is_subaddress())
++ o.AddMember("is_subaddress", e.is_subaddress(), a);
++ if (e.has_original())
++ o.AddMember("original", rapidjson::Value(e.original().c_str(), static_cast<rapidjson::SizeType>(e.original().size()), a), a);
++ if (e.has_is_integrated())
++ o.AddMember("is_integrated", e.is_integrated(), a);
++ return o;
++ }
++
++ rapidjson::Value source_entry_to_json(const messages::monero::MoneroTransactionSourceEntry &e, rapidjson::Document::AllocatorType &a)
++ {
++ rapidjson::Value o(rapidjson::kObjectType);
++ rapidjson::Value ring(rapidjson::kArrayType);
++ for (int i = 0; i < e.outputs_size(); ++i)
++ {
++ const auto &out = e.outputs(i);
++ rapidjson::Value ring_m(rapidjson::kObjectType);
++ if (out.has_idx())
++ ring_m.AddMember("idx", out.idx(), a);
++ if (out.has_key())
++ {
++ rapidjson::Value key(rapidjson::kObjectType);
++ const auto &k = out.key();
++ if (k.has_dest())
++ {
++ const std::string h = bin_to_hex_lower(k.dest());
++ key.AddMember("dest", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ if (k.has_commitment())
++ {
++ const std::string h = bin_to_hex_lower(k.commitment());
++ key.AddMember("commitment", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ ring_m.AddMember("key", key, a);
++ }
++ ring.PushBack(ring_m, a);
++ }
++ o.AddMember("outputs", ring, a);
++ if (e.has_real_output())
++ o.AddMember("real_output", e.real_output(), a);
++ if (e.has_real_out_tx_key())
++ {
++ const std::string h = bin_to_hex_lower(e.real_out_tx_key());
++ o.AddMember("real_out_tx_key", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ rapidjson::Value add_keys(rapidjson::kArrayType);
++ for (int i = 0; i < e.real_out_additional_tx_keys_size(); ++i)
++ {
++ const std::string h = bin_to_hex_lower(e.real_out_additional_tx_keys(i));
++ add_keys.PushBack(rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ o.AddMember("real_out_additional_tx_keys", add_keys, a);
++ if (e.has_real_output_in_tx_index())
++ o.AddMember("real_output_in_tx_index", e.real_output_in_tx_index(), a);
++ if (e.has_amount())
++ o.AddMember("amount", e.amount(), a);
++ if (e.has_rct())
++ o.AddMember("rct", e.rct(), a);
++ if (e.has_mask())
++ {
++ const std::string h = bin_to_hex_lower(e.mask());
++ o.AddMember("mask", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), a), a);
++ }
++ if (e.has_subaddr_minor())
++ o.AddMember("subaddr_minor", e.subaddr_minor(), a);
++ return o;
++ }
++
++ std::string monero_default_bip44_path(uint32_t subaddr_account)
++ {
++ std::ostringstream oss;
++ oss << "m/44'/128'/" << subaddr_account << "'";
++ return oss.str();
++ }
++
++ } // namespace
++
++
++ std::string trezor_connect_monero_sign_transaction_to_json(
++ wallet_shim *wallet,
++ const unsigned_tx_set *utx,
++ size_t tx_idx,
++ hw::tx_aux_data *aux_data,
++ cryptonote::network_type network_type)
++ {
++ CHECK_AND_ASSERT_THROW_MES(utx && aux_data, "null argument");
++ CHECK_AND_ASSERT_THROW_MES(std::get<0>(utx->transfers) == 0, "Unsupported non zero offset");
++ CHECK_AND_ASSERT_THROW_MES(tx_idx < utx->txes.size(), "Invalid transaction index");
++
++ Signer signer(wallet, utx, tx_idx, aux_data);
++ auto init_req = signer.step_init();
++ const auto &tsx = init_req->tsx_data();
++
++ rapidjson::Document doc;
++ doc.SetObject();
++ auto &alloc = doc.GetAllocator();
++
++ const std::string path_str = monero_default_bip44_path(utx->txes[tx_idx].subaddr_account);
++ doc.AddMember("path", rapidjson::Value(path_str.c_str(), static_cast<rapidjson::SizeType>(path_str.size()), alloc), alloc);
++ doc.AddMember("networkType", static_cast<uint32_t>(network_type), alloc);
++
++ rapidjson::Value tsx_data(rapidjson::kObjectType);
++ tsx_data.AddMember("version", tsx.version(), alloc);
++ if (tsx.has_payment_id() && !tsx.payment_id().empty())
++ {
++ const std::string h = bin_to_hex_lower(tsx.payment_id());
++ tsx_data.AddMember("payment_id", rapidjson::Value(h.c_str(), static_cast<rapidjson::SizeType>(h.size()), alloc), alloc);
++ }
++ tsx_data.AddMember("unlock_time", static_cast<uint64_t>(tsx.unlock_time()), alloc);
++
++ rapidjson::Value outputs(rapidjson::kArrayType);
++ for (int i = 0; i < tsx.outputs_size(); ++i)
++ outputs.PushBack(dest_entry_to_json(tsx.outputs(i), alloc), alloc);
++ tsx_data.AddMember("outputs", outputs, alloc);
++
++ if (tsx.has_change_dts())
++ tsx_data.AddMember("change_dts", dest_entry_to_json(tsx.change_dts(), alloc), alloc);
++
++ if (tsx.has_num_inputs())
++ tsx_data.AddMember("num_inputs", tsx.num_inputs(), alloc);
++ if (tsx.has_mixin())
++ tsx_data.AddMember("mixin", tsx.mixin(), alloc);
++ if (tsx.has_fee())
++ tsx_data.AddMember("fee", tsx.fee(), alloc);
++ if (tsx.has_account())
++ tsx_data.AddMember("account", tsx.account(), alloc);
++
++ if (tsx.minor_indices_size() > 0)
++ {
++ rapidjson::Value minors(rapidjson::kArrayType);
++ for (int i = 0; i < tsx.minor_indices_size(); ++i)
++ minors.PushBack(tsx.minor_indices(i), alloc);
++ tsx_data.AddMember("minor_indices", minors, alloc);
++ }
++
++ if (tsx.has_rsig_data())
++ {
++ const auto &rd = tsx.rsig_data();
++ rapidjson::Value rj(rapidjson::kObjectType);
++ if (rd.has_rsig_type())
++ rj.AddMember("rsig_type", rd.rsig_type(), alloc);
++ if (rd.has_bp_version())
++ rj.AddMember("bp_version", rd.bp_version(), alloc);
++ rapidjson::Value grp(rapidjson::kArrayType);
++ for (int i = 0; i < rd.grouping_size(); ++i)
++ grp.PushBack(static_cast<uint64_t>(rd.grouping(i)), alloc);
++ rj.AddMember("grouping", grp, alloc);
++ tsx_data.AddMember("rsig_data", rj, alloc);
++ }
++
++ if (tsx.integrated_indices_size() > 0)
++ {
++ rapidjson::Value ii(rapidjson::kArrayType);
++ for (int i = 0; i < tsx.integrated_indices_size(); ++i)
++ ii.PushBack(tsx.integrated_indices(i), alloc);
++ tsx_data.AddMember("integrated_indices", ii, alloc);
++ }
++
++ if (tsx.has_client_version())
++ tsx_data.AddMember("client_version", tsx.client_version(), alloc);
++ if (tsx.has_hard_fork())
++ tsx_data.AddMember("hard_fork", tsx.hard_fork(), alloc);
++ if (tsx.has_monero_version())
++ tsx_data.AddMember("monero_version", rapidjson::Value(tsx.monero_version().c_str(), static_cast<rapidjson::SizeType>(tsx.monero_version().size()), alloc), alloc);
++
++ doc.AddMember("tsx_data", tsx_data, alloc);
++
++ rapidjson::Value inputs(rapidjson::kArrayType);
++ const size_t n_in = utx->txes[tx_idx].sources.size();
++ for (size_t i = 0; i < n_in; ++i)
++ {
++ messages::monero::MoneroTransactionSourceEntry src_pb;
++ signer.export_source_entry(&src_pb, i, true, true);
++ inputs.PushBack(source_entry_to_json(src_pb, alloc), alloc);
++ }
++ doc.AddMember("inputs", inputs, alloc);
++
++ rapidjson::StringBuffer buffer;
++ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
++ doc.Accept(writer);
++ return std::string(buffer.GetString(), buffer.GetSize());
++ }
++
+ }
+ }
+ }
+diff --git a/src/device_trezor/trezor/protocol.hpp b/src/device_trezor/trezor/protocol.hpp
+index 73bd0b902..2db4e8e16 100644
+--- a/src/device_trezor/trezor/protocol.hpp
++++ b/src/device_trezor/trezor/protocol.hpp
+@@ -340,8 +340,16 @@ namespace tx {
+ const TData & tdata() const {
+ return m_ct;
+ }
++ void export_source_entry(MoneroTransactionSourceEntry *dst, size_t idx, bool need_ring_keys, bool need_ring_indices);
+ };
+
++ std::string trezor_connect_monero_sign_transaction_to_json(
++ wallet_shim *wallet,
++ const unsigned_tx_set *utx,
++ size_t tx_idx,
++ hw::tx_aux_data *aux_data,
++ cryptonote::network_type network_type);
++
+ // TX Key decryption
+ void load_tx_key_data(hw::device_cold::tx_key_data_t & res, const std::string & data);
+
+diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp
+index 796e7a50b..9109c8a32 100644
+--- a/src/wallet/api/pending_transaction.cpp
++++ b/src/wallet/api/pending_transaction.cpp
+@@ -44,6 +44,11 @@
+
+ #include "bc-ur/src/bc-ur.hpp"
+
++#if defined(DEVICE_TREZOR_READY) && DEVICE_TREZOR_READY
++#include "device/device_cold.hpp"
++#include "device_trezor/trezor/protocol.hpp"
++#endif
++
+ using namespace std;
+
+ namespace Monero {
+@@ -210,6 +215,61 @@ std::string PendingTransactionImpl::commitUR(int max_fragment_length) {
+ }
+ }
+
++std::string PendingTransactionImpl::commitTrezor(uint64_t tx_index)
++{
++#if !defined(DEVICE_TREZOR_READY) || !DEVICE_TREZOR_READY
++ (void)tx_index;
++ m_errorString = tr("This build was compiled without Trezor support");
++ m_status = Status_Error;
++ return "";
++#else
++ if (tx_index >= m_pending_tx.size())
++ {
++ m_errorString = tr("Invalid transaction index");
++ m_status = Status_Error;
++ return "";
++ }
++ try
++ {
++ tools::wallet2 *w = m_wallet.m_wallet.get();
++ tools::wallet2::unsigned_tx_set utx;
++ w->construct_unsigned_tx_set_for_signing(m_pending_tx, utx);
++ if (std::get<0>(utx.transfers) != 0)
++ {
++ m_errorString = tr("Unsupported unsigned transaction transfer offset");
++ m_status = Status_Error;
++ return "";
++ }
++ hw::tx_aux_data aux_data;
++ const int bpv = w->use_fork_rules(HF_VERSION_BULLETPROOF_PLUS, -10) ? 4
++ : (w->use_fork_rules(HF_VERSION_CLSAG, -10) ? 3
++ : (w->use_fork_rules(HF_VERSION_SMALLER_BP, -10) ? 2 : 1));
++ aux_data.bp_version = bpv;
++ aux_data.hard_fork = w->get_current_hard_fork();
++ aux_data.client_version = static_cast<unsigned>(bpv >= 4 ? 4u : 3u);
++
++ hw::wallet_shim shim;
++ shim.get_tx_pub_key_from_received_outs = std::bind(&tools::wallet2::get_tx_pub_key_from_received_outs, w, std::placeholders::_1);
++
++ const std::string json = hw::trezor::protocol::tx::trezor_connect_monero_sign_transaction_to_json(
++ &shim,
++ &utx,
++ static_cast<size_t>(tx_index),
++ &aux_data,
++ w->nettype());
++ m_errorString.clear();
++ m_status = Status_Ok;
++ return json;
++ }
++ catch (const std::exception &e)
++ {
++ m_errorString = e.what();
++ m_status = Status_Error;
++ return "";
++ }
++#endif
++}
++
+
+ uint64_t PendingTransactionImpl::amount() const
+ {
+diff --git a/src/wallet/api/pending_transaction.h b/src/wallet/api/pending_transaction.h
+index eb5c75c5f..c9c68fb03 100644
+--- a/src/wallet/api/pending_transaction.h
++++ b/src/wallet/api/pending_transaction.h
+@@ -47,6 +47,7 @@ public:
+ std::string errorString() const override;
+ bool commit(const std::string &filename = "", bool overwrite = false) override;
+ std::string commitUR(int max_fragment_length = 130) override;
++ std::string commitTrezor(uint64_t tx_index = 0) 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 062df7f41..560890f2c 100644
+--- a/src/wallet/api/unsigned_transaction.cpp
++++ b/src/wallet/api/unsigned_transaction.cpp
+@@ -34,11 +34,13 @@
+
+ #include "cryptonote_basic/cryptonote_format_utils.h"
+ #include "cryptonote_basic/cryptonote_basic_impl.h"
++#include "cryptonote_config.h"
+
+ #include <memory>
+ #include <vector>
+ #include <sstream>
+ #include <boost/format.hpp>
++#include <functional>
+
+ #include "bc-ur/src/bc-ur.hpp"
+
+diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
+index 57f1203c6..c65ce8799 100644
+--- a/src/wallet/api/wallet.cpp
++++ b/src/wallet/api/wallet.cpp
+@@ -1392,6 +1392,45 @@ bool WalletImpl::submitTransactionUR(const string &input) {
+ return true;
+ }
+
++bool WalletImpl::submitTransactionHex(const string &hex) {
++ clearStatus();
++ if (checkBackgroundSync("cannot submit tx"))
++ return false;
++
++ pauseRefresh();
++ try {
++ m_wallet->relay_raw_tx(hex);
++ } catch (const tools::error::daemon_busy&) {
++ setStatusError(tr("daemon is busy. Please try again later."));
++ startRefresh();
++ return false;
++ } catch (const tools::error::no_connection_to_daemon&) {
++ setStatusError(tr("no connection to daemon."));
++ startRefresh();
++ return false;
++ } catch (const tools::error::tx_rejected& e) {
++ std::ostringstream writer;
++ writer << (boost::format(tr("transaction %s was rejected by daemon with status: ")) % get_transaction_hash(e.tx())) << e.status();
++ if (!e.reason().empty())
++ writer << tr(". Reason: ") << e.reason();
++ setStatusError(writer.str());
++ startRefresh();
++ return false;
++ } catch (const std::exception &e) {
++ setStatusError(string(tr("Unknown exception: ")) + e.what());
++ startRefresh();
++ return false;
++ } catch (...) {
++ setStatusError(tr("Unhandled exception"));
++ startRefresh();
++ return false;
++ }
++ startRefresh();
++ if (m_history)
++ m_history->refresh();
++ return true;
++}
++
+
+ bool WalletImpl::hasUnknownKeyImages() const
+ {
+@@ -3436,4 +3475,33 @@ std::string WalletImpl::serializeCacheToJson() const
+ return std::string(m_wallet->serialize_cache_to_json());
+ }
+
++std::string WalletImpl::exportTrezorTdis() const
++{
++ return m_wallet->export_trezor_tdis();
++}
++
++bool WalletImpl::importTrezorEncryptedKeyImagesJson(const string &json)
++{
++ 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
++ {
++ uint64_t spent = 0, unspent = 0;
++ uint64_t height = m_wallet->import_trezor_encrypted_key_images_json(json, spent, unspent);
++ LOG_PRINT_L2("Trezor encrypted key images imported to height " << height << ", "
++ << print_money(spent) << " spent, " << print_money(unspent) << " unspent");
++ }
++ catch (const std::exception &e)
++ {
++ LOG_ERROR("Error importing Trezor encrypted key images: " << e.what());
++ setStatusError(string(tr("Failed to import key images: ")) + e.what());
++ return false;
++ }
++ return true;
++}
++
+ } // namespace
+diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
+index 8e4846e27..ede41b46d 100644
+--- a/src/wallet/api/wallet.h
++++ b/src/wallet/api/wallet.h
+@@ -185,6 +185,7 @@ public:
+ virtual PendingTransaction * createSweepUnmixableTransaction() override;
+ bool submitTransaction(const std::string &fileName) override;
+ bool submitTransactionUR(const std::string &input) override;
++ bool submitTransactionHex(const std::string &hex) override;
+ virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) override;
+ virtual UnsignedTransaction * loadUnsignedTxUR(const std::string &input) override;
+ bool hasUnknownKeyImages() const override;
+@@ -338,6 +339,8 @@ private:
+ bool getWaitsForDeviceReceive();
+
+ virtual std::string serializeCacheToJson() const override;
++ virtual std::string exportTrezorTdis() const override;
++ bool importTrezorEncryptedKeyImagesJson(const std::string &json) override;
+ };
+
+
+diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
+index 4552c02d0..bead275bb 100644
+--- a/src/wallet/api/wallet2_api.h
++++ b/src/wallet/api/wallet2_api.h
+@@ -84,6 +84,7 @@ struct PendingTransaction
+ // 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 std::string commitTrezor(uint64_t tx_index = 0) = 0;
+ virtual uint64_t amount() const = 0;
+ virtual uint64_t dust() const = 0;
+ virtual uint64_t fee() const = 0;
+@@ -950,6 +951,7 @@ struct Wallet
+ */
+ virtual bool submitTransaction(const std::string &fileName) = 0;
+ virtual bool submitTransactionUR(const std::string &input) = 0;
++ virtual bool submitTransactionHex(const std::string &input) = 0;
+
+
+ /*!
+@@ -1216,6 +1218,19 @@ struct Wallet
+
+ //! serialize wallet cache to JSON
+ virtual std::string serializeCacheToJson() const = 0;
++
++
++ /*!
++ * \brief exportTrezorTdis — export transfer details for Trezor cold key-image sync as JSON
++ *
++ * Returns an object `{ "tdis": [ ... ] }` with hex pubkeys and indices per output.
++ */
++ virtual std::string exportTrezorTdis() const = 0;
++
++ /*!
++ * \brief importTrezorEncryptedKeyImagesJson — import key images from Trezor-style encrypted JSON
++ */
++ virtual bool importTrezorEncryptedKeyImagesJson(const std::string &json) = 0;
+ };
+
+ /**
+diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
+index 06a732296..2a71e4751 100644
+--- a/src/wallet/wallet2.cpp
++++ b/src/wallet/wallet2.cpp
+@@ -112,6 +112,7 @@ extern "C"
+ {
+ #include "crypto/keccak.h"
+ #include "crypto/crypto-ops.h"
++#include <sodium/crypto_aead_chacha20poly1305.h>
+ }
+ using namespace std;
+ using namespace crypto;
+@@ -967,6 +968,42 @@ static tools::wallet2::tx_construction_data &get_construction_data(tools::wallet
+ return std::get<tools::wallet2::tx_construction_data>(ptx.construction_data);
+ }
+
++static bool get_short_payment_id(crypto::hash8 &payment_id8, const tools::wallet2::pending_tx &ptx, hw::device &hwdev)
++{
++ std::vector<tx_extra_field> tx_extra_fields;
++ parse_tx_extra(ptx.tx.extra, tx_extra_fields);
++ cryptonote::tx_extra_nonce extra_nonce;
++ if (find_tx_extra_field_by_type(tx_extra_fields, extra_nonce))
++ {
++ if (get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id8))
++ {
++ if (ptx.dests.empty())
++ {
++ MWARNING("Encrypted payment id found, but no destinations public key, cannot decrypt");
++ return false;
++ }
++ return hwdev.decrypt_payment_id(payment_id8, ptx.dests[0].addr.m_view_public_key, ptx.tx_key);
++ }
++ }
++ return false;
++}
++
++static tools::wallet2::tx_construction_data get_construction_data_with_decrypted_short_payment_id(const tools::wallet2::pending_tx &ptx, hw::device &hwdev)
++{
++ tools::wallet2::tx_construction_data construction_data = get_construction_data(ptx);
++ crypto::hash8 payment_id = crypto::null_hash8;
++ if (get_short_payment_id(payment_id, ptx, hwdev))
++ {
++ remove_field_from_tx_extra(construction_data.extra, typeid(cryptonote::tx_extra_nonce));
++ std::string extra_nonce;
++ set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, payment_id);
++ THROW_WALLET_EXCEPTION_IF(!add_extra_nonce_to_tx_extra(construction_data.extra, extra_nonce),
++ tools::error::wallet_internal_error, "Failed to add decrypted payment id to tx extra");
++ MDEBUG("Decrypted payment ID: " << payment_id);
++ }
++ return construction_data;
++}
++
+ uint32_t get_subaddress_clamped_sum(uint32_t idx, uint32_t extra)
+ {
+ static constexpr uint32_t uint32_max = std::numeric_limits<uint32_t>::max();
+@@ -8080,6 +8117,61 @@ void wallet2::commit_tx(std::vector<pending_tx>& ptx_vector)
+ }
+ }
+ //----------------------------------------------------------------------------------------------------
++void wallet2::relay_raw_tx(const std::string &tx_as_hex)
++{
++ using namespace cryptonote;
++
++ cryptonote::blobdata tx_blob;
++ THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(tx_as_hex, tx_blob), error::wallet_internal_error, "Failed to parse hex");
++
++ transaction tx;
++ THROW_WALLET_EXCEPTION_IF(!parse_and_validate_tx_from_blob(tx_blob, tx), error::wallet_internal_error, "Failed to parse transaction");
++
++ COMMAND_RPC_SEND_RAW_TX::request req;
++ req.tx_as_hex = tx_as_hex;
++ req.do_not_relay = false;
++ req.do_sanity_checks = true;
++ COMMAND_RPC_SEND_RAW_TX::response daemon_send_resp;
++
++ {
++ const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
++ bool r = epee::net_utils::invoke_http_json("/sendrawtransaction", req, daemon_send_resp, *m_http_client, rpc_timeout);
++ THROW_ON_RPC_RESPONSE_ERROR(r, {}, daemon_send_resp, "sendrawtransaction", error::tx_rejected, tx, get_rpc_status(m_trusted_daemon, daemon_send_resp.status), get_text_reason(daemon_send_resp));
++ }
++
++ crypto::hash txid = get_transaction_hash(tx);
++ uint64_t tx_money_spent_in_ins = 0;
++ auto subaddr_account = []()->boost::optional<uint32_t> { return boost::none; }();
++ std::set<uint32_t> subaddr_indices;
++ for (const auto &in : tx.vin)
++ {
++ if (in.type() != typeid(cryptonote::txin_to_key))
++ continue;
++ const cryptonote::txin_to_key &in_to_key = boost::get<cryptonote::txin_to_key>(in);
++ const auto kit = m_key_images.find(in_to_key.k_image);
++ if (kit == m_key_images.end())
++ continue;
++ const transfer_details &td = m_transfers[kit->second];
++ const uint64_t amount = in_to_key.amount > 0 ? in_to_key.amount : td.amount();
++ tx_money_spent_in_ins += amount;
++ subaddr_account = td.m_subaddr_index.major;
++ subaddr_indices.insert(td.m_subaddr_index.minor);
++ set_spent(kit->second, 0);
++ }
++
++ if (tx_money_spent_in_ins > 0 && store_tx_info() && !m_unconfirmed_txs.count(txid))
++ {
++ THROW_WALLET_EXCEPTION_IF(!subaddr_account, error::wallet_internal_error,
++ "Relayed tx spends our outputs but subaddress account is unknown");
++ add_unconfirmed_tx(txid, tx, tx_money_spent_in_ins, {}, crypto::null_hash, 0, *subaddr_account, subaddr_indices);
++ auto utx_it = m_unconfirmed_txs.find(txid);
++ THROW_WALLET_EXCEPTION_IF(utx_it == m_unconfirmed_txs.end(), error::wallet_internal_error,
++ "unconfirmed tx wasn't found: " + string_tools::pod_to_hex(txid));
++ utx_it->second.m_amount_out = get_outgoing_amount(tx, tx_money_spent_in_ins);
++ LOG_PRINT_L1("Raw transaction relayed. <" << txid << ">");
++ }
++}
++//----------------------------------------------------------------------------------------------------
+ bool wallet2::save_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename) const
+ {
+ LOG_PRINT_L0("saving " << ptx_vector.size() << " transactions");
+@@ -8117,6 +8209,20 @@ std::string wallet2::dump_tx_to_str(const std::vector<pending_tx> &ptx_vector) c
+ return std::string(UNSIGNED_TX_PREFIX) + ciphertext;
+ }
+ //----------------------------------------------------------------------------------------------------
++void wallet2::construct_unsigned_tx_set_for_signing(const std::vector<pending_tx>& ptx_vector, unsigned_tx_set &utx) const
++{
++ utx.txes.clear();
++ utx.txes.reserve(ptx_vector.size());
++ for (const auto &tx : ptx_vector)
++ utx.txes.push_back(get_construction_data_with_decrypted_short_payment_id(tx, m_account.get_device()));
++
++ utx.new_transfers = std::make_tuple(static_cast<uint64_t>(0), static_cast<uint64_t>(0), std::vector<exported_transfer_details>());
++
++ transfer_container transfers_copy;
++ get_transfers(transfers_copy);
++ utx.transfers = std::make_tuple(static_cast<uint64_t>(0), static_cast<uint64_t>(transfers_copy.size()), std::move(transfers_copy));
++}
++//----------------------------------------------------------------------------------------------------
+ bool wallet2::load_unsigned_tx(const std::string &unsigned_filename, unsigned_tx_set &exported_txs) const
+ {
+ std::string s;
+@@ -15946,4 +16052,147 @@ std::pair<size_t, uint64_t> wallet2::estimate_tx_size_and_weight(bool use_rct, i
+ return std::make_pair(size, weight);
+ }
+ //----------------------------------------------------------------------------------------------------
++// Returns JSON: { "tdis": [ { "out_key", "tx_pub_key", optional "additional_tx_pub_keys", indices }, ... ] }
++// Hex fields are 64-char lower-case pubkeys; additional_tx_pub_keys matches protocol::ki::key_image_data
++// (subset of txn additional pubkeys for this transfer — currently 0 or 1 entry).
++std::string wallet2::export_trezor_tdis() const
++{
++ rapidjson::Document doc;
++ doc.SetObject();
++ auto &alloc = doc.GetAllocator();
++
++ rapidjson::Value tdis(rapidjson::kArrayType);
++ tdis.Reserve(static_cast<rapidjson::SizeType>(m_transfers.size()), alloc);
++
++ for (const auto &td : m_transfers)
++ {
++ const crypto::public_key out_key = td.get_public_key();
++ const crypto::public_key tx_pub_key = get_tx_pub_key_from_received_outs(td);
++ const std::vector<crypto::public_key> additional_tx_pub_keys = cryptonote::get_additional_tx_pub_keys_from_extra(td.m_tx);
++
++ std::vector<crypto::public_key> additional_for_tdi;
++ if (!additional_tx_pub_keys.empty() && additional_tx_pub_keys.size() > td.m_internal_output_index)
++ additional_for_tdi.push_back(additional_tx_pub_keys[td.m_internal_output_index]);
++
++ rapidjson::Value obj(rapidjson::kObjectType);
++ const std::string out_hex = epee::string_tools::pod_to_hex(out_key);
++ const std::string tx_pub_hex = epee::string_tools::pod_to_hex(tx_pub_key);
++ obj.AddMember("out_key", rapidjson::Value(out_hex.c_str(), static_cast<rapidjson::SizeType>(out_hex.size()), alloc), alloc);
++ obj.AddMember("tx_pub_key", rapidjson::Value(tx_pub_hex.c_str(), static_cast<rapidjson::SizeType>(tx_pub_hex.size()), alloc), alloc);
++ if (!additional_for_tdi.empty())
++ {
++ rapidjson::Value aux(rapidjson::kArrayType);
++ aux.Reserve(static_cast<rapidjson::SizeType>(additional_for_tdi.size()), alloc);
++ for (const auto &apk : additional_for_tdi)
++ {
++ const std::string ah = epee::string_tools::pod_to_hex(apk);
++ aux.PushBack(rapidjson::Value(ah.c_str(), static_cast<rapidjson::SizeType>(ah.size()), alloc), alloc);
++ }
++ obj.AddMember("additional_tx_pub_keys", aux, alloc);
++ }
++ obj.AddMember("internal_output_index", rapidjson::Value(static_cast<uint64_t>(td.m_internal_output_index)), alloc);
++ obj.AddMember("sub_addr_major", rapidjson::Value(td.m_subaddr_index.major), alloc);
++ obj.AddMember("sub_addr_minor", rapidjson::Value(td.m_subaddr_index.minor), alloc);
++ tdis.PushBack(obj, alloc);
++ }
++
++ doc.AddMember("tdis", tdis, alloc);
++
++ rapidjson::StringBuffer buffer;
++ rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
++ doc.Accept(writer);
++ return std::string(buffer.GetString(), buffer.GetSize());
++}
++//----------------------------------------------------------------------------------------------------
++namespace
++{
++ static void decrypt_trezor_exported_ki_blob(const std::string &cipher, const std::string &iv, const std::string &key32, crypto::key_image &ki, crypto::signature &sig)
++ {
++ THROW_WALLET_EXCEPTION_IF(iv.size() != crypto_aead_chacha20poly1305_ietf_NPUBBYTES,
++ error::wallet_internal_error, "Trezor KI JSON: IV must be 12 bytes");
++ THROW_WALLET_EXCEPTION_IF(key32.size() != 32,
++ error::wallet_internal_error, "Trezor KI JSON: encryption key must be 32 bytes");
++ THROW_WALLET_EXCEPTION_IF(cipher.size() < crypto_aead_chacha20poly1305_ietf_ABYTES,
++ error::wallet_internal_error, "Trezor KI JSON: key_image ciphertext too short");
++ char buf[96];
++ unsigned long long out_len = 0;
++ const int r = crypto_aead_chacha20poly1305_ietf_decrypt(
++ reinterpret_cast<unsigned char *>(buf), &out_len, nullptr,
++ reinterpret_cast<const unsigned char *>(cipher.data()), cipher.size(),
++ nullptr, 0,
++ reinterpret_cast<const unsigned char *>(iv.data()),
++ reinterpret_cast<const unsigned char *>(key32.data()));
++ THROW_WALLET_EXCEPTION_IF(r != 0,
++ error::wallet_internal_error, "Trezor KI JSON: decryption failed (wrong key or corrupt ciphertext)");
++ THROW_WALLET_EXCEPTION_IF(out_len != 96,
++ error::wallet_internal_error, "Trezor KI JSON: unexpected plaintext length");
++ memcpy(ki.data, buf, 32);
++ memcpy(sig.c.data, buf + 32, 32);
++ memcpy(sig.r.data, buf + 64, 32);
++ memwipe(buf, sizeof(buf));
++ }
++}
++
++//----------------------------------------------------------------------------------------------------
++uint64_t wallet2::import_trezor_encrypted_key_images_json(const std::string &json, uint64_t &spent, uint64_t &unspent, bool check_spent)
++{
++ rapidjson::Document doc;
++ doc.Parse(json.c_str());
++ THROW_WALLET_EXCEPTION_IF(doc.HasParseError(), error::wallet_internal_error,
++ std::string("Trezor KI JSON: parse error at offset ") + std::to_string(doc.GetErrorOffset()));
++
++ const rapidjson::Value *proot = &doc;
++ if (doc.IsObject() && doc.HasMember("payload") && doc["payload"].IsObject())
++ proot = &doc["payload"];
++
++ THROW_WALLET_EXCEPTION_IF(!proot->IsObject(), error::wallet_internal_error, "Trezor KI JSON: expected JSON object");
++ const rapidjson::Value &root = *proot;
++
++ THROW_WALLET_EXCEPTION_IF(!root.HasMember("key_images") || !root["key_images"].IsArray(),
++ error::wallet_internal_error, "Trezor KI JSON: missing or invalid key_images array");
++
++ std::string enc_key_bin;
++ if (root.HasMember("signature") && root["signature"].IsString())
++ {
++ THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(root["signature"].GetString(), enc_key_bin),
++ error::wallet_internal_error, "Trezor KI JSON: invalid hex in signature");
++ }
++ else if (root.HasMember("enc_key") && root["enc_key"].IsString())
++ {
++ THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(root["enc_key"].GetString(), enc_key_bin),
++ error::wallet_internal_error, "Trezor KI JSON: invalid hex in enc_key");
++ }
++ else
++ {
++ THROW_WALLET_EXCEPTION(error::wallet_internal_error, "Trezor KI JSON: need signature or enc_key (32-byte hex)");
++ }
++ THROW_WALLET_EXCEPTION_IF(enc_key_bin.size() != 32,
++ error::wallet_internal_error, "Trezor KI JSON: encryption key must decode to 32 bytes");
++
++ const rapidjson::Value &arr = root["key_images"];
++ std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
++ ski.reserve(arr.Size());
++
++ for (rapidjson::SizeType i = 0; i < arr.Size(); ++i)
++ {
++ const rapidjson::Value &el = arr[i];
++ THROW_WALLET_EXCEPTION_IF(!el.IsObject(), error::wallet_internal_error, "Trezor KI JSON: key_images entry must be object");
++ THROW_WALLET_EXCEPTION_IF(!el.HasMember("iv") || !el["iv"].IsString(), error::wallet_internal_error, "Trezor KI JSON: missing iv");
++ THROW_WALLET_EXCEPTION_IF(!el.HasMember("key_image") || !el["key_image"].IsString(), error::wallet_internal_error, "Trezor KI JSON: missing key_image");
++
++ std::string iv_bin, blob_bin;
++ THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(el["iv"].GetString(), iv_bin),
++ error::wallet_internal_error, "Trezor KI JSON: invalid hex in iv");
++ THROW_WALLET_EXCEPTION_IF(!epee::string_tools::parse_hexstr_to_binbuff(el["key_image"].GetString(), blob_bin),
++ error::wallet_internal_error, "Trezor KI JSON: invalid hex in key_image");
++
++ crypto::key_image ki{};
++ crypto::signature sig{};
++ decrypt_trezor_exported_ki_blob(blob_bin, iv_bin, enc_key_bin, ki, sig);
++ ski.emplace_back(std::move(ki), std::move(sig));
++ }
++
++ return import_key_images(ski, 0, spent, unspent, check_spent);
++}
++//----------------------------------------------------------------------------------------------------
+ }
+diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
+index 42e1c02d9..7332459ae 100644
+--- a/src/wallet/wallet2.h
++++ b/src/wallet/wallet2.h
+@@ -707,8 +707,10 @@ private:
+
+ void commit_tx(pending_tx& ptx_vector);
+ void commit_tx(std::vector<pending_tx>& ptx_vector);
++ void relay_raw_tx(const std::string &tx_as_hex);
+ bool save_tx(const std::vector<pending_tx>& ptx_vector, const std::string &filename) const;
+ std::string dump_tx_to_str(const std::vector<pending_tx> &ptx_vector) const;
++ void construct_unsigned_tx_set_for_signing(const std::vector<pending_tx>& ptx_vector, unsigned_tx_set &utx) const;
+ std::string save_multisig_tx(multisig_tx_set txs);
+ bool save_multisig_tx(const multisig_tx_set &txs, const std::string &filename);
+ std::string save_multisig_tx(const std::vector<pending_tx>& ptx_vector);
+@@ -1281,7 +1283,8 @@ private:
+ bool is_unattended() const { return m_unattended; }
+
+ std::pair<size_t, uint64_t> estimate_tx_size_and_weight(bool use_rct, int n_inputs, int ring_size, int n_outputs, size_t extra_size);
+-
++ std::string export_trezor_tdis() const;
++ uint64_t import_trezor_encrypted_key_images_json(const std::string &json, uint64_t &spent, uint64_t &unspent, bool check_spent = true);
+
+ /*
+ * "attributes" are a mechanism to store an arbitrary number of string values
+--
+2.54.0 (Apple Git-157)
+