mod eip1271;
mod eip191;
use alloy::primitives::Address;
pub use eip191::Eip191Verifier;
pub use eip1271::Eip1271Verifier;
use siwx::{SiwxError, SiwxMessage, Verifier};
pub const CHAIN_NAME: &str = "Ethereum";
pub fn validate_address(address: &str) -> Result<(), SiwxError> {
if !address.starts_with("0x") {
return Err(SiwxError::InvalidAddress("must start with 0x".into()));
}
parse_address(address)?;
Ok(())
}
pub(crate) fn parse_address(s: &str) -> Result<Address, SiwxError> {
s.parse::<Address>()
.map_err(|e| SiwxError::InvalidAddress(e.to_string()))
}
#[derive(Debug)]
pub struct EvmVerifier {
rpc_url: Option<String>,
}
impl EvmVerifier {
#[must_use]
pub const fn new() -> Self {
Self { rpc_url: None }
}
#[must_use]
pub fn with_rpc(url: impl Into<String>) -> Self {
Self {
rpc_url: Some(url.into()),
}
}
}
impl Default for EvmVerifier {
fn default() -> Self {
Self::new()
}
}
impl Verifier for EvmVerifier {
const CHAIN_NAME: &'static str = CHAIN_NAME;
async fn verify(&self, message: &SiwxMessage, signature: &[u8]) -> Result<(), SiwxError> {
let eip191_err = match Eip191Verifier::verify_sync(message, signature) {
Ok(()) => return Ok(()),
Err(e) => e,
};
let Some(rpc_url) = self.rpc_url.as_deref() else {
return Err(eip191_err);
};
Eip1271Verifier::new(rpc_url)
.verify(message, signature)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_address_accepts_canonical_formats() {
assert!(validate_address("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").is_ok());
assert!(validate_address("0x0000000000000000000000000000000000000000").is_ok());
}
#[test]
fn validate_address_rejects_bad_formats() {
assert!(validate_address("not-an-address").is_err());
assert!(validate_address("0x123").is_err());
assert!(validate_address("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").is_err());
}
#[test]
fn format_message_uses_ethereum_preamble() {
let msg = SiwxMessage::new(
"example.com",
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"https://example.com",
"1",
"1",
)
.expect("valid");
let text = Eip191Verifier::format_message(&msg);
assert!(text.starts_with("example.com wants you to sign in with your Ethereum account:"));
}
}