feat(monero-sys): Sign message (#450)

This commit is contained in:
Mohan 2025-07-06 14:06:40 +02:00 committed by GitHub
parent 38332ab79f
commit 56722a5780
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 153 additions and 0 deletions

View file

@ -200,6 +200,15 @@ namespace Monero
return std::make_unique<std::string>(key);
}
/**
* Sign a message with the wallet's private key
*/
inline std::unique_ptr<std::string> 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<std::string>(signature);
}
/**
* Get the seed of the wallet.
*/

View file

@ -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<UniquePtr<CxxString>>;
}
}

View file

@ -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<String> {
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<String> {
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.

View file

@ -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!");
}