siwx_svm/lib.rs
1//! # siwx-svm — Solana verification for Sign-In with X
2//!
3//! Implements CAIP-122 namespace profile for Solana:
4//! - **Ed25519** signature verification (`solana:ed25519`)
5//!
6//! # Quick start
7//!
8//! ```rust,no_run
9//! use siwx::SiwxMessage;
10//! use siwx_svm::{Ed25519Verifier, CHAIN_NAME};
11//! use siwx::Verifier;
12//!
13//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
14//! let message = SiwxMessage::new(
15//! "example.com",
16//! "GwAF45zjfyGzUbd3i3hXxzGeuchzEZXwpRYHZM5912F1",
17//! "https://example.com/login",
18//! "1",
19//! "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
20//! )?;
21//! // let pubkey: [u8; 32] = ...; // Ed25519 public key
22//! // let sig_bytes: [u8; 64] = ...; // Ed25519 signature
23//! // Ed25519Verifier::new(pubkey).verify(&message, &sig_bytes).await?;
24//! # Ok(())
25//! # }
26//! ```
27
28mod ed25519;
29
30pub use ed25519::Ed25519Verifier;
31use siwx::{SiwxError, SiwxMessage};
32
33/// Human-readable chain name for the Solana namespace, used in the CAIP-122
34/// preamble line.
35pub const CHAIN_NAME: &str = "Solana";
36
37/// CAIP-122 signature type for Solana Ed25519.
38pub const SIG_TYPE: &str = "solana:ed25519";
39
40/// Convenience: format a [`SiwxMessage`] into the Solana CAIP-122 signing
41/// string.
42#[must_use]
43pub fn format_message(message: &SiwxMessage) -> String {
44 message.to_sign_string(CHAIN_NAME)
45}
46
47/// Validate that `address` is a valid base58-encoded Solana public key (32
48/// bytes decoded).
49///
50/// # Errors
51///
52/// Returns [`SiwxError::InvalidAddress`] if the format is wrong.
53pub fn validate_address(address: &str) -> Result<(), SiwxError> {
54 let bytes = bs58::decode(address)
55 .into_vec()
56 .map_err(|e| SiwxError::InvalidAddress(format!("invalid base58: {e}")))?;
57 if bytes.len() != 32 {
58 return Err(SiwxError::InvalidAddress(format!(
59 "expected 32 bytes, got {}",
60 bytes.len()
61 )));
62 }
63 Ok(())
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn validate_address_valid() {
72 assert!(validate_address("11111111111111111111111111111111").is_ok());
73 assert!(validate_address("GwAF45zjfyGzUbd3i3hXxzGeuchzEZXwpRYHZM5912F1").is_ok());
74 }
75
76 #[test]
77 fn validate_address_invalid() {
78 assert!(validate_address("not-valid").is_err());
79 assert!(validate_address("").is_err());
80 }
81
82 #[test]
83 fn format_message_preamble() {
84 let msg = SiwxMessage::new(
85 "example.com",
86 "GwAF45zjfyGzUbd3i3hXxzGeuchzEZXwpRYHZM5912F1",
87 "https://example.com",
88 "1",
89 "1",
90 )
91 .unwrap();
92 let text = format_message(&msg);
93 assert!(text.starts_with("example.com wants you to sign in with your Solana account:"));
94 }
95}