1mod eip1271;
30mod eip191;
31
32use alloy::primitives::Address;
33pub use eip191::Eip191Verifier;
34pub use eip1271::Eip1271Verifier;
35use siwx::{SiwxError, SiwxMessage};
36
37pub const CHAIN_NAME: &str = "Ethereum";
40
41pub const SIG_TYPE_EIP191: &str = "eip191";
43
44pub const SIG_TYPE_EIP1271: &str = "eip1271";
46
47#[must_use]
49pub fn format_message(message: &SiwxMessage) -> String {
50 message.to_sign_string(CHAIN_NAME)
51}
52
53pub fn validate_address(address: &str) -> Result<(), SiwxError> {
60 if !address.starts_with("0x") {
61 return Err(SiwxError::InvalidAddress("must start with 0x".into()));
62 }
63 parse_address(address)?;
64 Ok(())
65}
66
67pub(crate) fn parse_address(s: &str) -> Result<Address, SiwxError> {
73 s.parse::<Address>()
74 .map_err(|e| SiwxError::InvalidAddress(e.to_string()))
75}
76
77#[derive(Debug)]
82pub struct EvmVerifier {
83 rpc_url: Option<String>,
84}
85
86impl EvmVerifier {
87 #[must_use]
89 pub const fn new() -> Self {
90 Self { rpc_url: None }
91 }
92
93 #[must_use]
95 pub fn with_rpc(url: impl Into<String>) -> Self {
96 Self {
97 rpc_url: Some(url.into()),
98 }
99 }
100}
101
102impl Default for EvmVerifier {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl siwx::Verifier for EvmVerifier {
109 async fn verify(&self, message: &SiwxMessage, signature: &[u8]) -> Result<(), SiwxError> {
110 let eip191_err = match Eip191Verifier::verify_inner(message, signature) {
111 Ok(()) => return Ok(()),
112 Err(e) => e,
113 };
114
115 let Some(rpc_url) = self.rpc_url.as_deref() else {
116 return Err(eip191_err);
117 };
118
119 Eip1271Verifier::new(rpc_url)
120 .verify_inner(message, signature)
121 .await
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn validate_address_valid() {
131 assert!(validate_address("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").is_ok());
132 assert!(validate_address("0x0000000000000000000000000000000000000000").is_ok());
133 }
134
135 #[test]
136 fn validate_address_invalid() {
137 assert!(validate_address("not-an-address").is_err());
138 assert!(validate_address("0x123").is_err());
139 assert!(validate_address("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").is_err());
140 }
141
142 #[test]
143 fn format_message_preamble() {
144 let msg = SiwxMessage::new(
145 "example.com",
146 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
147 "https://example.com",
148 "1",
149 "1",
150 )
151 .unwrap();
152 let text = format_message(&msg);
153 assert!(text.starts_with("example.com wants you to sign in with your Ethereum account:"));
154 }
155}