From 6c49e3ceef4565063cd2a9c952f6744fa868f4f9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 17:26:11 -0600 Subject: [PATCH] feat(xmr): sign and verify messages Implement SignVerifyInterface on LibMoneroWallet so Monero wallets get the existing Sign / Verify view. cs_monero 4.0.0 already wraps MONERO_Wallet_signMessage and MONERO_Wallet_verifySignedMessage, so this only forwards them through CsMoneroInterface. Signatures are wallet2 SigV2 spend-key signatures for the chosen primary address or subaddress, compatible with monero-wallet-cli/rpc sign and verify. wallet2 returns an empty string instead of throwing when signing fails, so treat that as an error. View-only wallets cannot sign. --- .../intermediate/lib_monero_wallet.dart | 44 ++++++++++++++++++- .../interfaces/cs_monero_interface.dart | 13 ++++++ ...XMR_cs_monero_interface_impl.template.dart | 19 ++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index bf8914a836..144fa6613d 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -42,13 +42,14 @@ import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; import '../wallet.dart'; import '../wallet_mixin_interfaces/multi_address_interface.dart'; +import '../wallet_mixin_interfaces/sign_verify_interface.dart'; import '../wallet_mixin_interfaces/view_only_option_interface.dart'; import 'cryptonote_wallet.dart'; abstract class LibMoneroWallet extends CryptonoteWallet with ViewOnlyOptionInterface - implements MultiAddressInterface { + implements MultiAddressInterface, SignVerifyInterface { @override int get isarTransactionVersion => 2; @@ -175,6 +176,47 @@ abstract class LibMoneroWallet return csMonero.getTxKey(wallet!, txid); } + @override + Future signMessage(String message, {required Address address}) async { + if (isViewOnly) { + throw Exception("Cannot sign a message in a view only wallet"); + } + if (wallet == null) { + throw Exception("Wallet not open"); + } + + final signature = await csMonero.signMessage( + wallet!, + message: message, + address: address.value, + ); + + // wallet2 returns an empty string on failure instead of throwing + if (signature.isEmpty) { + throw Exception("Failed to sign message with ${address.value}"); + } + + return signature; + } + + @override + Future verifyMessage( + String message, { + required String address, + required String signature, + }) async { + if (wallet == null) { + throw Exception("Wallet not open"); + } + + return await csMonero.verifyMessage( + wallet!, + message: message, + address: address.trim(), + signature: signature.trim(), + ); + } + void _setListener() { if (wallet != null && !csMonero.hasListeners(wallet!)) { csMonero.addListener( diff --git a/lib/wl_gen/interfaces/cs_monero_interface.dart b/lib/wl_gen/interfaces/cs_monero_interface.dart index 8386cb6654..4989bf5c68 100644 --- a/lib/wl_gen/interfaces/cs_monero_interface.dart +++ b/lib/wl_gen/interfaces/cs_monero_interface.dart @@ -60,6 +60,19 @@ abstract class CsMoneroInterface { Future getTxKey(WrappedWallet wallet, String txid); + Future signMessage( + WrappedWallet wallet, { + required String message, + required String address, + }); + + Future verifyMessage( + WrappedWallet wallet, { + required String message, + required String address, + required String signature, + }); + Future save(WrappedWallet wallet); Future getPublicViewKey(WrappedWallet wallet); diff --git a/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart b/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart index e7967e0e83..ad63def12b 100644 --- a/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart +++ b/tool/wl_templates/XMR_cs_monero_interface_impl.template.dart @@ -177,6 +177,25 @@ class _CsMoneroInterfaceImpl extends CsMoneroInterface { Future getTxKey(WrappedWallet wallet, String txid) => wallet.get().getTxKey(txid); + @override + Future signMessage( + WrappedWallet wallet, { + required String message, + required String address, + }) => wallet.get().signMessage(message, address); + + @override + Future verifyMessage( + WrappedWallet wallet, { + required String message, + required String address, + required String signature, + }) => wallet.get().verifyMessage( + message, + address, + signature, + ); + @override Future save(WrappedWallet wallet) => wallet.get().save();