diff --git a/monero-sys/src/bridge.h b/monero-sys/src/bridge.h index c3510b45..221a24e6 100644 --- a/monero-sys/src/bridge.h +++ b/monero-sys/src/bridge.h @@ -200,6 +200,15 @@ namespace Monero return std::make_unique(key); } + /** + * Sign a message with the wallet's private key + */ + inline std::unique_ptr signMessage(Wallet &wallet, const std::string &message, const std::string &address, bool sign_with_view_key) + { + auto signature = wallet.signMessage(message, address, sign_with_view_key); + return std::make_unique(signature); + } + /** * Get the seed of the wallet. */ diff --git a/monero-sys/src/bridge.rs b/monero-sys/src/bridge.rs index 9bf7a474..de136e81 100644 --- a/monero-sys/src/bridge.rs +++ b/monero-sys/src/bridge.rs @@ -270,6 +270,14 @@ pub mod ffi { self: Pin<&mut Wallet>, tx: *mut PendingTransaction, ) -> Result<()>; + + /// Sign a message with the wallet's private key. + fn signMessage( + wallet: Pin<&mut Wallet>, + message: &CxxString, + address: &CxxString, + sign_with_view_key: bool, + ) -> Result>; } } diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index cc06fadc..b6f47325 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -675,6 +675,30 @@ impl WalletHandle { // Signal success Ok(()) } + + /// Sign a message with the wallet's private key. + /// + /// # Arguments + /// * `message` - The message to sign (arbitrary byte data) + /// * `address` - The address to use for signing (uses main address if None) + /// * `sign_with_view_key` - Whether to sign with view key instead of spend key (default: false) + /// + /// # Returns + /// A proof type prefix + base58 encoded signature + pub async fn sign_message( + &self, + message: &str, + address: Option<&str>, + sign_with_view_key: bool, + ) -> anyhow::Result { + let message = message.to_string(); + let address = address.map(|s| s.to_string()); + + self.call(move |wallet| { + wallet.sign_message(&message, address.as_deref(), sign_with_view_key) + }) + .await + } } impl Wallet { @@ -1727,6 +1751,36 @@ impl FfiWallet { .expect("Shouldn't panic") .to_string() } + + /// Sign a message with the wallet's private key. + /// + /// # Arguments + /// * `message` - The message to sign (arbitrary byte data) + /// * `address` - The address to use for signing (uses main address if None) + /// * `sign_with_view_key` - Whether to sign with view key instead of spend key (default: false) + /// + /// # Returns + /// A proof type prefix + base58 encoded signature + pub fn sign_message( + &mut self, + message: &str, + address: Option<&str>, + sign_with_view_key: bool, + ) -> anyhow::Result { + let_cxx_string!(message_cxx = message); + let_cxx_string!(address_cxx = address.unwrap_or("")); + + let signature = ffi::signMessage(self.inner.pinned(), &message_cxx, &address_cxx, sign_with_view_key) + .context("Failed to sign message: FFI call failed with exception")? + .to_string(); + + if signature.is_empty() { + self.check_error().context("Failed to sign message")?; + anyhow::bail!("Failed to sign message (no signature returned)"); + } + + Ok(signature) + } } /// Safety: We check that it's never accessed outside the homethread at runtime. diff --git a/monero-sys/tests/sign_message.rs b/monero-sys/tests/sign_message.rs new file mode 100644 index 00000000..cdda778a --- /dev/null +++ b/monero-sys/tests/sign_message.rs @@ -0,0 +1,82 @@ +use monero_sys::{Daemon, WalletHandle}; + +const PLACEHOLDER_NODE: &str = "http://127.0.0.1:18081"; + +#[tokio::test] +async fn test_sign_message() { + tracing_subscriber::fmt() + .with_env_filter("info,test=debug,sign_message=trace,monero_sys=trace") + .with_test_writer() + .init(); + + let temp_dir = tempfile::tempdir().unwrap(); + let daemon = Daemon { + address: PLACEHOLDER_NODE.into(), + ssl: false, + }; + + let wallet_name = "test_signing_wallet"; + let wallet_path = temp_dir.path().join(wallet_name).display().to_string(); + + tracing::info!("Creating wallet for message signing test"); + let wallet = WalletHandle::open_or_create( + wallet_path, + daemon, + monero::Network::Stagenet, + false, // No background sync + ) + .await + .expect("Failed to create wallet"); + + let main_address = wallet.main_address().await; + tracing::info!("Wallet main address: {}", main_address); + + // Test message to sign + let test_message = "Hello, World! This is a test message for signing."; + + tracing::info!("Testing message signing with spend key (default address)"); + let signature_spend = wallet + .sign_message(test_message, None, false) + .await + .expect("Failed to sign message with spend key"); + + tracing::info!("Signature with spend key: {}", signature_spend); + assert!(!signature_spend.is_empty(), "Signature should not be empty"); + assert!(signature_spend.len() > 10, "Signature should be reasonably long"); + + tracing::info!("Testing message signing with view key (default address)"); + let signature_view = wallet + .sign_message(test_message, None, true) + .await + .expect("Failed to sign message with view key"); + + tracing::info!("Signature with view key: {}", signature_view); + assert!(!signature_view.is_empty(), "Signature should not be empty"); + assert!(signature_view.len() > 10, "Signature should be reasonably long"); + + // Signatures should be different when using different keys + assert_ne!(signature_spend, signature_view, "Spend key and view key signatures should be different"); + + tracing::info!("Testing message signing with spend key (explicit address)"); + let signature_explicit = wallet + .sign_message(test_message, Some(&main_address.to_string()), false) + .await + .expect("Failed to sign message with explicit address"); + + tracing::info!("Signature with explicit address: {}", signature_explicit); + assert!(!signature_explicit.is_empty(), "Signature should not be empty"); + + // When using the same key and same address (main address), signatures should be the same + assert_eq!(signature_spend, signature_explicit, "Signatures should be the same when using same key and address"); + + tracing::info!("Testing empty message signing"); + let signature_empty = wallet + .sign_message("", None, false) + .await + .expect("Failed to sign empty message"); + + tracing::info!("Signature for empty message: {}", signature_empty); + assert!(!signature_empty.is_empty(), "Signature should not be empty even for empty message"); + + tracing::info!("All message signing tests passed!"); +} \ No newline at end of file