diff --git a/Cargo.lock b/Cargo.lock index b060b89f..8c4df7ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4141,6 +4141,7 @@ dependencies = [ [[package]] name = "relayer-utils" version = "0.3.7" +source = "git+https://github.com/zkemail/relayer-utils.git?rev=0c19631#0c196312b6ce7ce2c282d11e6c736e958b8df77a" dependencies = [ "anyhow", "base64 0.21.7", diff --git a/packages/relayer/Cargo.toml b/packages/relayer/Cargo.toml index 30d0bb2b..4ccd237d 100644 --- a/packages/relayer/Cargo.toml +++ b/packages/relayer/Cargo.toml @@ -25,8 +25,7 @@ tiny_http = "0.12.0" lettre = { version = "0.10.4", features = ["tokio1", "tokio1-native-tls"] } ethers = { version = "2.0.10", features = ["abigen"] } # relayer-utils = { version = "0.3.7", git = "https://github.com/zkemail/relayer-utils.git" } -# relayer-utils = { rev = "94d78d6", git = "https://github.com/zkemail/relayer-utils.git" } -relayer-utils = { path = "../../../relayer-utils" } +relayer-utils = { rev = "0c19631", git = "https://github.com/zkemail/relayer-utils.git" } futures = "0.3.28" sqlx = { version = "=0.7.3", features = ["postgres", "runtime-tokio"] } regex = "1.10.2" diff --git a/packages/relayer/src/chain.rs b/packages/relayer/src/chain.rs index d3df1e62..30e362b4 100644 --- a/packages/relayer/src/chain.rs +++ b/packages/relayer/src/chain.rs @@ -10,6 +10,7 @@ const CONFIRMATIONS: usize = 1; type SignerM = SignerMiddleware, LocalWallet>; +/// Represents a client for interacting with the blockchain. #[derive(Debug, Clone)] pub struct ChainClient { pub client: Arc, @@ -25,6 +26,7 @@ impl ChainClient { let wallet: LocalWallet = PRIVATE_KEY.get().unwrap().parse()?; let provider = Provider::::try_from(CHAIN_RPC_PROVIDER.get().unwrap())?; + // Create a new SignerMiddleware with the provider and wallet let client = Arc::new(SignerMiddleware::new( provider, wallet.with_chain_id(*CHAIN_ID.get().unwrap()), @@ -58,13 +60,18 @@ impl ChainClient { let mut mutex = SHARED_MUTEX.lock().await; *mutex += 1; + // Call the contract method let call = dkim.set_dkim_public_key_hash(selector, domain_name, public_key_hash, signature); let tx = call.send().await?; + + // Wait for the transaction to be confirmed let receipt = tx .log() .confirmations(CONFIRMATIONS) .await? .ok_or(anyhow!("No receipt"))?; + + // Format the transaction hash let tx_hash = receipt.transaction_hash; let tx_hash = format!("0x{}", hex::encode(tx_hash.as_bytes())); Ok(tx_hash) @@ -87,6 +94,7 @@ impl ChainClient { public_key_hash: [u8; 32], dkim: ECDSAOwnedDKIMRegistry, ) -> Result { + // Call the contract method to check if the hash is valid let is_valid = dkim .is_dkim_public_key_hash_valid(domain_name, public_key_hash) .call() @@ -107,9 +115,16 @@ impl ChainClient { &self, controller_eth_addr: &str, ) -> Result, anyhow::Error> { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse()?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Call the dkim method to get the DKIM registry address let dkim = contract.dkim().call().await?; + + // Create and return a new ECDSAOwnedDKIMRegistry instance Ok(ECDSAOwnedDKIMRegistry::new(dkim, self.client.clone())) } @@ -126,10 +141,16 @@ impl ChainClient { &self, email_auth_addr: &str, ) -> Result, anyhow::Error> { + // Parse the email auth address let email_auth_address: H160 = email_auth_addr.parse()?; + + // Create a new EmailAuth contract instance let contract = EmailAuth::new(email_auth_address, self.client.clone()); + + // Call the dkim_registry_addr method to get the DKIM registry address let dkim = contract.dkim_registry_addr().call().await?; + // Create and return a new ECDSAOwnedDKIMRegistry instance Ok(ECDSAOwnedDKIMRegistry::new(dkim, self.client.clone())) } @@ -150,11 +171,18 @@ impl ChainClient { wallet_addr: &str, account_salt: &str, ) -> Result { + // Parse the controller and wallet Ethereum addresses let controller_eth_addr: H160 = controller_eth_addr.parse()?; let wallet_address: H160 = wallet_addr.parse()?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Decode the account salt let account_salt_bytes = hex::decode(account_salt.trim_start_matches("0x")) .map_err(|e| anyhow!("Failed to decode account_salt: {}", e))?; + + // Compute the email auth address let email_auth_addr = contract .compute_email_auth_address( wallet_address, @@ -176,7 +204,10 @@ impl ChainClient { /// /// A `Result` containing a boolean indicating if the wallet is deployed. pub async fn is_wallet_deployed(&self, wallet_addr_str: &str) -> Result { + // Parse the wallet address let wallet_addr: H160 = wallet_addr_str.parse().map_err(ChainError::HexError)?; + + // Get the bytecode at the wallet address match self.client.get_code(wallet_addr, None).await { Ok(code) => Ok(!code.is_empty()), Err(e) => { @@ -204,9 +235,14 @@ impl ChainClient { controller_eth_addr: &str, template_idx: u64, ) -> Result, ChainError> { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse().map_err(ChainError::HexError)?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Get the acceptance command templates let templates = contract .acceptance_command_templates() .call() @@ -232,9 +268,14 @@ impl ChainClient { controller_eth_addr: &str, template_idx: u64, ) -> Result, ChainError> { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse().map_err(ChainError::HexError)?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Get the recovery command templates let templates = contract .recovery_command_templates() .call() @@ -262,30 +303,30 @@ impl ChainClient { account_eth_addr: &str, complete_calldata: &str, ) -> Result { - println!("doing complete recovery"); + // Parse the controller and account Ethereum addresses let controller_eth_addr: H160 = controller_eth_addr.parse().map_err(ChainError::HexError)?; - println!("controller_eth_addr: {:?}", controller_eth_addr); + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Decode the complete calldata let decoded_calldata = hex::decode(complete_calldata.trim_start_matches("0x")).expect("Decoding failed"); - println!("decoded_calldata : {:?}", decoded_calldata); let account_eth_addr = account_eth_addr .parse::() .map_err(ChainError::HexError)?; - println!("account_eth_addr : {:?}", account_eth_addr); + // Call the complete_recovery method let call = contract.complete_recovery(account_eth_addr, Bytes::from(decoded_calldata)); - println!("call: {:?}", call); let tx = call .send() .await .map_err(|e| ChainError::contract_error("Failed to call complete_recovery", e))?; - println!("tx: {:?}", tx); - // If the transaction is successful, the function will return true and false otherwise. + + // Wait for the transaction to be confirmed let receipt = tx .log() .confirmations(CONFIRMATIONS) @@ -297,8 +338,8 @@ impl ChainClient { ) })? .ok_or(anyhow!("No receipt"))?; - println!("receipt : {:?}", receipt); + // Check if the transaction was successful Ok(receipt .status .map(|status| status == U64::from(1)) @@ -322,14 +363,20 @@ impl ChainClient { email_auth_msg: EmailAuthMsg, template_idx: u64, ) -> std::result::Result { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse()?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Call the handle_acceptance method let call = contract.handle_acceptance(email_auth_msg, template_idx.into()); let tx = call .send() .await .map_err(|e| ChainError::contract_error("Failed to call handle_acceptance", e))?; - // If the transaction is successful, the function will return true and false otherwise. + + // Wait for the transaction to be confirmed let receipt = tx .log() .confirmations(CONFIRMATIONS) @@ -341,6 +388,8 @@ impl ChainClient { ) })? .ok_or(anyhow!("No receipt"))?; + + // Check if the transaction was successful Ok(receipt .status .map(|status| status == U64::from(1)) @@ -364,14 +413,20 @@ impl ChainClient { email_auth_msg: EmailAuthMsg, template_idx: u64, ) -> std::result::Result { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse()?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Call the handle_recovery method let call = contract.handle_recovery(email_auth_msg, template_idx.into()); let tx = call .send() .await .map_err(|e| ChainError::contract_error("Failed to call handle_recovery", e))?; - // If the transaction is successful, the function will return true and false otherwise. + + // Wait for the transaction to be confirmed let receipt = tx .log() .confirmations(CONFIRMATIONS) @@ -380,6 +435,8 @@ impl ChainClient { ChainError::provider_error("Failed to get receipt after calling handle_recovery", e) })? .ok_or(anyhow!("No receipt"))?; + + // Check if the transaction was successful Ok(receipt .status .map(|status| status == U64::from(1)) @@ -396,7 +453,10 @@ impl ChainClient { /// /// A `Result` containing the bytecode as Bytes. pub async fn get_bytecode(&self, wallet_addr: &str) -> std::result::Result { + // Parse the wallet address let wallet_address: H160 = wallet_addr.parse().map_err(ChainError::HexError)?; + + // Get the bytecode at the wallet address let client_code = self .client .get_code(wallet_address, None) @@ -420,7 +480,10 @@ impl ChainClient { wallet_addr: &str, slot: u64, ) -> Result { + // Parse the wallet address let wallet_address: H160 = wallet_addr.parse()?; + + // Get the storage at the specified slot Ok(self .client .get_storage_at(wallet_address, u64_to_u8_array_32(slot).into(), None) @@ -444,16 +507,23 @@ impl ChainClient { command_params: Vec, template_idx: u64, ) -> Result { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse().map_err(ChainError::HexError)?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Encode the command parameters let command_params_bytes = command_params - .iter() // Change here: use iter() instead of map() directly on Vec + .iter() .map(|s| { - s.abi_encode(None) // Assuming decimal_size is not needed or can be None + s.abi_encode(None) .unwrap_or_else(|_| Bytes::from("Error encoding".as_bytes().to_vec())) - }) // Error handling + }) .collect::>(); + + // Call the extract_recovered_account_from_acceptance_command method let recovered_account = contract .extract_recovered_account_from_acceptance_command( command_params_bytes, @@ -487,17 +557,24 @@ impl ChainClient { command_params: Vec, template_idx: u64, ) -> Result { + // Parse the controller Ethereum address let controller_eth_addr: H160 = controller_eth_addr.parse().map_err(ChainError::HexError)?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Encode the command parameters let command_params_bytes = command_params - .iter() // Change here: use iter() instead of map() directly on Vec + .iter() .map(|s| { s.abi_encode(None).map_err(|_| { ChainError::Validation("Error encoding subject parameters".to_string()) }) }) .collect::, ChainError>>()?; + + // Call the extract_recovered_account_from_recovery_command method let recovered_account = contract .extract_recovered_account_from_recovery_command( command_params_bytes, @@ -529,10 +606,15 @@ impl ChainClient { controller_eth_addr: &str, account_eth_addr: &str, ) -> Result { + // Parse the controller and account Ethereum addresses let controller_eth_addr: H160 = controller_eth_addr.parse().map_err(ChainError::HexError)?; let account_eth_addr: H160 = account_eth_addr.parse().map_err(ChainError::HexError)?; + + // Create a new EmailAccountRecovery contract instance let contract = EmailAccountRecovery::new(controller_eth_addr, self.client.clone()); + + // Call the is_activated method let is_activated = contract .is_activated(account_eth_addr) .call() diff --git a/packages/relayer/src/config.rs b/packages/relayer/src/config.rs index bed30ff4..2e5a9095 100644 --- a/packages/relayer/src/config.rs +++ b/packages/relayer/src/config.rs @@ -4,6 +4,10 @@ use std::{env, path::PathBuf}; use dotenv::dotenv; +/// Configuration struct for the Relayer service. +/// +/// This struct holds various configuration parameters needed for the Relayer service, +/// including SMTP settings, database path, web server address, and blockchain-related information. #[derive(Clone)] pub struct RelayerConfig { pub smtp_server: String, @@ -21,9 +25,19 @@ pub struct RelayerConfig { } impl RelayerConfig { + /// Creates a new instance of RelayerConfig. + /// + /// This function loads environment variables using dotenv and populates + /// the RelayerConfig struct with the values. + /// + /// # Returns + /// + /// A new instance of RelayerConfig. pub fn new() -> Self { + // Load environment variables from .env file dotenv().ok(); + // Construct and return the RelayerConfig instance Self { smtp_server: env::var(SMTP_SERVER_KEY).unwrap(), relayer_email_addr: env::var(RELAYER_EMAIL_ADDR_KEY).unwrap(), @@ -45,6 +59,13 @@ impl RelayerConfig { } impl Default for RelayerConfig { + /// Provides a default instance of RelayerConfig. + /// + /// This implementation simply calls the `new()` method to create a default instance. + /// + /// # Returns + /// + /// A default instance of RelayerConfig. fn default() -> Self { Self::new() } diff --git a/packages/relayer/src/core.rs b/packages/relayer/src/core.rs index 43986c63..f9c10165 100644 --- a/packages/relayer/src/core.rs +++ b/packages/relayer/src/core.rs @@ -104,6 +104,16 @@ pub async fn handle_email(email: String) -> Result { handle_email_request(params, invitation_code).await } +/// Handles the email request based on the presence of an invitation code and whether it's for recovery. +/// +/// # Arguments +/// +/// * `params` - The `EmailRequestContext` containing request details. +/// * `invitation_code` - An optional invitation code. +/// +/// # Returns +/// +/// A `Result` containing an `EmailAuthEvent` or an `EmailError`. async fn handle_email_request( params: EmailRequestContext, invitation_code: Option, @@ -141,6 +151,16 @@ async fn handle_email_request( } } +/// Handles the acceptance of an email authentication request. +/// +/// # Arguments +/// +/// * `params` - The `EmailRequestContext` containing request details. +/// * `invitation_code` - The invitation code from the email. +/// +/// # Returns +/// +/// A `Result` containing an `EmailAuthEvent` or an `EmailError`. async fn accept( params: EmailRequestContext, invitation_code: String, @@ -150,6 +170,7 @@ async fn accept( info!(LOG, "Email Auth Msg: {:?}", email_auth_msg); info!(LOG, "Request: {:?}", params.request); + // Handle the acceptance with the client let is_accepted = CLIENT .handle_acceptance( ¶ms.request.controller_eth_addr, @@ -194,12 +215,22 @@ async fn accept( } } +/// Handles the recovery process for an email authentication request. +/// +/// # Arguments +/// +/// * `params` - The `EmailRequestContext` containing request details. +/// +/// # Returns +/// +/// A `Result` containing an `EmailAuthEvent` or an `EmailError`. async fn recover(params: EmailRequestContext) -> Result { let (email_auth_msg, email_proof, account_salt) = get_email_auth_msg(¶ms).await?; info!(LOG, "Email Auth Msg: {:?}", email_auth_msg); info!(LOG, "Request: {:?}", params.request); + // Handle the recovery with the client let is_success = CLIENT .handle_recovery( ¶ms.request.controller_eth_addr, @@ -236,6 +267,16 @@ async fn recover(params: EmailRequestContext) -> Result, start_idx: usize) -> Result { // Gather signals from start_idx to start_idx + COMMAND_FIELDS let command_bytes: Vec = public_signals @@ -253,6 +294,18 @@ fn get_masked_command(public_signals: Vec, start_idx: usize) -> Result Result<(EmailProof, [u8; 32]), EmailError> { @@ -314,6 +376,15 @@ async fn generate_email_proof( Ok((email_proof, account_salt)) } +/// Generates the template ID for the email authentication request. +/// +/// # Arguments +/// +/// * `params` - The `EmailRequestContext` containing request details. +/// +/// # Returns +/// +/// A 32-byte array representing the template ID. fn get_template_id(params: &EmailRequestContext) -> [u8; 32] { let action = if params.request.is_for_recovery { "RECOVERY".to_string() @@ -331,6 +402,15 @@ fn get_template_id(params: &EmailRequestContext) -> [u8; 32] { keccak256(encode(&tokens)) } +/// Retrieves and encodes the command parameters for the email authentication request. +/// +/// # Arguments +/// +/// * `params` - The `EmailRequestContext` containing request details. +/// +/// # Returns +/// +/// A `Result` containing a vector of encoded command parameters or an `EmailError`. async fn get_encoded_command_params( params: &EmailRequestContext, ) -> Result, EmailError> { @@ -365,6 +445,15 @@ async fn get_encoded_command_params( Ok(command_params_encoded) } +/// Generates the email authentication message. +/// +/// # Arguments +/// +/// * `params` - The `EmailRequestContext` containing request details. +/// +/// # Returns +/// +/// A `Result` containing the `EmailAuthMsg`, `EmailProof`, and account salt, or an `EmailError`. async fn get_email_auth_msg( params: &EmailRequestContext, ) -> Result<(EmailAuthMsg, EmailProof, [u8; 32]), EmailError> { @@ -380,11 +469,17 @@ async fn get_email_auth_msg( Ok((email_auth_msg, email_proof, account_salt)) } +/// Represents the context for an email authentication request. #[derive(Debug, Clone)] struct EmailRequestContext { + /// The request details. request: Request, + /// The body of the email. email_body: String, + /// The account code as a string. account_code_str: String, + /// The full raw email. email: String, + /// The parsed email. parsed_email: ParsedEmail, } diff --git a/packages/relayer/src/database.rs b/packages/relayer/src/database.rs index 93de8ee1..29729903 100644 --- a/packages/relayer/src/database.rs +++ b/packages/relayer/src/database.rs @@ -3,28 +3,45 @@ use crate::*; use relayer_utils::LOG; use sqlx::{postgres::PgPool, Row}; +/// Represents the credentials for a user account. #[derive(Debug, Clone)] pub struct Credentials { + /// The unique code associated with the account. pub account_code: String, + /// The Ethereum address of the account. pub account_eth_addr: String, + /// The email address of the guardian. pub guardian_email_addr: String, + /// Indicates whether the credentials are set. pub is_set: bool, } +/// Represents a request in the system. #[derive(Debug, Clone)] pub struct Request { + /// The unique identifier for the request. pub request_id: u32, + /// The Ethereum address of the account. pub account_eth_addr: String, + /// The Ethereum address of the controller. pub controller_eth_addr: String, + /// The email address of the guardian. pub guardian_email_addr: String, + /// Indicates whether the request is for recovery. pub is_for_recovery: bool, + /// The index of the template used for the request. pub template_idx: u64, + /// Indicates whether the request has been processed. pub is_processed: bool, + /// Indicates the success status of the request, if available. pub is_success: Option, + /// The nullifier for the email, if available. pub email_nullifier: Option, + /// The salt for the account, if available. pub account_salt: Option, } +/// Represents the database connection and operations. pub struct Database { db: PgPool, } @@ -57,6 +74,7 @@ impl Database { /// /// A `Result` indicating success or failure of the setup process. pub async fn setup_database(&self) -> Result<()> { + // Create credentials table sqlx::query( "CREATE TABLE IF NOT EXISTS credentials ( account_code TEXT PRIMARY KEY, @@ -68,6 +86,7 @@ impl Database { .execute(&self.db) .await?; + // Create requests table sqlx::query( "CREATE TABLE IF NOT EXISTS requests ( request_id BIGINT PRIMARY KEY, @@ -85,6 +104,7 @@ impl Database { .execute(&self.db) .await?; + // Create expected_replies table sqlx::query( "CREATE TABLE IF NOT EXISTS expected_replies ( message_id VARCHAR(255) PRIMARY KEY, @@ -98,6 +118,11 @@ impl Database { Ok(()) } + /// Tests the database connection by attempting to execute a simple query. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the connection test. pub(crate) async fn test_db_connection(&self) -> Result<()> { // Try up to 3 times for i in 1..4 { @@ -120,6 +145,15 @@ impl Database { )) } + /// Retrieves credentials for a given account code. + /// + /// # Arguments + /// + /// * `account_code` - The unique code associated with the account. + /// + /// # Returns + /// + /// A `Result` containing an `Option` if successful, or an error if the query fails. pub(crate) async fn get_credentials(&self, account_code: &str) -> Result> { let row = sqlx::query("SELECT * FROM credentials WHERE account_code = $1") .bind(account_code) @@ -128,6 +162,7 @@ impl Database { match row { Some(row) => { + // Extract values from the row let account_code: String = row.get("account_code"); let account_eth_addr: String = row.get("account_eth_addr"); let guardian_email_addr: String = row.get("guardian_email_addr"); @@ -145,6 +180,16 @@ impl Database { } } + /// Checks if a wallet and email combination is registered in the database. + /// + /// # Arguments + /// + /// * `account_eth_addr` - The Ethereum address of the account. + /// * `email_addr` - The email address to check. + /// + /// # Returns + /// + /// A `Result` containing a boolean indicating if the combination is registered. pub(crate) async fn is_wallet_and_email_registered( &self, account_eth_addr: &str, @@ -159,17 +204,23 @@ impl Database { .await .map_err(|e| DatabaseError::new("Failed to check if wallet and email are registered", e))?; - match row { - Some(_) => Ok(true), - None => Ok(false), - } + Ok(row.is_some()) } + /// Updates the credentials for a given account code. + /// + /// # Arguments + /// + /// * `row` - The `Credentials` struct containing the updated information. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the update operation. pub(crate) async fn update_credentials_of_account_code( &self, row: &Credentials, ) -> std::result::Result<(), DatabaseError> { - let res = sqlx::query("UPDATE credentials SET account_eth_addr = $1, guardian_email_addr = $2, is_set = $3 WHERE account_code = $4") + sqlx::query("UPDATE credentials SET account_eth_addr = $1, guardian_email_addr = $2, is_set = $3 WHERE account_code = $4") .bind(&row.account_eth_addr) .bind(&row.guardian_email_addr) .bind(row.is_set) @@ -182,11 +233,20 @@ impl Database { Ok(()) } + /// Updates the credentials for a given wallet and email combination. + /// + /// # Arguments + /// + /// * `row` - The `Credentials` struct containing the updated information. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the update operation. pub(crate) async fn update_credentials_of_wallet_and_email( &self, row: &Credentials, ) -> std::result::Result<(), DatabaseError> { - let res = sqlx::query("UPDATE credentials SET account_code = $1, is_set = $2 WHERE account_eth_addr = $3 AND guardian_email_addr = $4") + sqlx::query("UPDATE credentials SET account_code = $1, is_set = $2 WHERE account_eth_addr = $3 AND guardian_email_addr = $4") .bind(&row.account_code) .bind(row.is_set) .bind(&row.account_eth_addr) @@ -199,12 +259,22 @@ impl Database { Ok(()) } + /// Updates the credentials of an inactive guardian. + /// + /// # Arguments + /// + /// * `is_set` - The new value for the `is_set` field. + /// * `account_eth_addr` - The Ethereum address of the account. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the update operation. pub(crate) async fn update_credentials_of_inactive_guardian( &self, is_set: bool, account_eth_addr: &str, ) -> std::result::Result<(), DatabaseError> { - let res = sqlx::query( + sqlx::query( "UPDATE credentials SET is_set = $1 WHERE account_eth_addr = $2 AND is_set = true", ) .bind(is_set) @@ -215,11 +285,20 @@ impl Database { Ok(()) } + /// Inserts new credentials into the database. + /// + /// # Arguments + /// + /// * `row` - The `Credentials` struct containing the new information. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the insert operation. pub(crate) async fn insert_credentials( &self, row: &Credentials, ) -> std::result::Result<(), DatabaseError> { - let row = sqlx::query( + sqlx::query( "INSERT INTO credentials (account_code, account_eth_addr, guardian_email_addr, is_set) VALUES ($1, $2, $3, $4) RETURNING *", ) .bind(&row.account_code) @@ -229,7 +308,7 @@ impl Database { .fetch_one(&self.db) .await .map_err(|e| DatabaseError::new("Failed to insert credentials", e))?; - info!(LOG, "Credentials inserted",); + info!(LOG, "Credentials inserted"); Ok(()) } @@ -255,12 +334,18 @@ impl Database { .await .map_err(|e| DatabaseError::new("Failed to check if guardian is set", e))?; - match row { - Some(_) => Ok(true), - None => Ok(false), - } + Ok(row.is_some()) } + /// Retrieves a request from the database based on the request ID. + /// + /// # Arguments + /// + /// * `request_id` - The unique identifier of the request. + /// + /// # Returns + /// + /// A `Result` containing an `Option` if successful, or an error if the query fails. pub(crate) async fn get_request( &self, request_id: u32, @@ -273,6 +358,7 @@ impl Database { match row { Some(row) => { + // Extract values from the row let request_id: i64 = row.get("request_id"); let account_eth_addr: String = row.get("account_eth_addr"); let controller_eth_addr: String = row.get("controller_eth_addr"); @@ -302,11 +388,20 @@ impl Database { } } + /// Updates an existing request in the database. + /// + /// # Arguments + /// + /// * `row` - The `Request` struct containing the updated information. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the update operation. pub(crate) async fn update_request( &self, row: &Request, ) -> std::result::Result<(), DatabaseError> { - let res = sqlx::query("UPDATE requests SET account_eth_addr = $1, controller_eth_addr = $2, guardian_email_addr = $3, is_for_recovery = $4, template_idx = $5, is_processed = $6, is_success = $7, email_nullifier = $8, account_salt = $9 WHERE request_id = $10") + sqlx::query("UPDATE requests SET account_eth_addr = $1, controller_eth_addr = $2, guardian_email_addr = $3, is_for_recovery = $4, template_idx = $5, is_processed = $6, is_success = $7, email_nullifier = $8, account_salt = $9 WHERE request_id = $10") .bind(&row.account_eth_addr) .bind(&row.controller_eth_addr) .bind(&row.guardian_email_addr) @@ -323,6 +418,16 @@ impl Database { Ok(()) } + /// Retrieves the account code for a given wallet and email combination. + /// + /// # Arguments + /// + /// * `account_eth_addr` - The Ethereum address of the account. + /// * `email_addr` - The email address associated with the account. + /// + /// # Returns + /// + /// A `Result` containing an `Option` with the account code if found, or an error if the query fails. pub(crate) async fn get_account_code_from_wallet_and_email( &self, account_eth_addr: &str, @@ -346,6 +451,16 @@ impl Database { } } + /// Retrieves the credentials for a given wallet and email combination. + /// + /// # Arguments + /// + /// * `account_eth_addr` - The Ethereum address of the account. + /// * `email_addr` - The email address associated with the account. + /// + /// # Returns + /// + /// A `Result` containing an `Option` if found, or an error if the query fails. pub(crate) async fn get_credentials_from_wallet_and_email( &self, account_eth_addr: &str, @@ -362,6 +477,7 @@ impl Database { match row { Some(row) => { + // Extract values from the row let account_code: String = row.get("account_code"); let account_eth_addr: String = row.get("account_eth_addr"); let guardian_email_addr: String = row.get("guardian_email_addr"); @@ -379,12 +495,21 @@ impl Database { } } + /// Inserts a new request into the database. + /// + /// # Arguments + /// + /// * `row` - The `Request` struct containing the new request information. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the insert operation. pub(crate) async fn insert_request( &self, row: &Request, ) -> std::result::Result<(), DatabaseError> { let request_id = row.request_id; - let row = sqlx::query( + sqlx::query( "INSERT INTO requests (request_id, account_eth_addr, controller_eth_addr, guardian_email_addr, is_for_recovery, template_idx, is_processed, is_success, email_nullifier, account_salt) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *", ) .bind(row.request_id as i64) @@ -404,6 +529,16 @@ impl Database { Ok(()) } + /// Adds an expected reply to the database. + /// + /// # Arguments + /// + /// * `message_id` - The unique identifier of the message. + /// * `request_id` - An optional request ID associated with the reply. + /// + /// # Returns + /// + /// A `Result` indicating success or failure of the insert operation. pub(crate) async fn add_expected_reply( &self, message_id: &str, @@ -422,10 +557,17 @@ impl Database { Ok(()) } - // Checks if the given message_id corresponds to a valid reply. - // This function updates the `has_reply` field to true if the message_id exists and hasn't been replied to yet. - // Returns true if the update was successful (i.e., a valid reply was recorded), false otherwise, - // also if no record exists to be updated. + /// Checks if the given message_id corresponds to a valid reply. + /// + /// This function updates the `has_reply` field to true if the message_id exists and hasn't been replied to yet. + /// + /// # Arguments + /// + /// * `message_id` - The unique identifier of the message. + /// + /// # Returns + /// + /// A `Result` containing a boolean indicating if the reply is valid (true if the update was successful). pub(crate) async fn is_valid_reply(&self, message_id: &str) -> Result { let query = " UPDATE expected_replies diff --git a/packages/relayer/src/lib.rs b/packages/relayer/src/lib.rs index 42348138..c4d1adb6 100644 --- a/packages/relayer/src/lib.rs +++ b/packages/relayer/src/lib.rs @@ -46,9 +46,19 @@ pub static SMTP_SERVER: OnceLock = OnceLock::new(); static DB_CELL: OnceCell> = OnceCell::const_new(); +/// Wrapper struct for database access struct DBWrapper; impl DBWrapper { + /// Retrieves the database instance. + /// + /// # Returns + /// + /// A reference to the `Arc`. + /// + /// # Panics + /// + /// Panics if the database is not initialized. fn get() -> &'static Arc { DB_CELL.get().expect("Database not initialized") } @@ -58,13 +68,14 @@ impl std::ops::Deref for DBWrapper { type Target = Database; fn deref(&self) -> &Self::Target { - &**Self::get() + Self::get() } } static DB: DBWrapper = DBWrapper; lazy_static! { + /// Shared instance of the `ChainClient`. pub static ref CLIENT: Arc = { dotenv().ok(); let client = tokio::task::block_in_place(|| { @@ -75,12 +86,23 @@ lazy_static! { .unwrap(); Arc::new(client) }; + /// Shared mutex for synchronization. pub static ref SHARED_MUTEX: Arc> = Arc::new(Mutex::new(0)); } +/// Runs the relayer with the given configuration. +/// +/// # Arguments +/// +/// * `config` - The configuration for the relayer. +/// +/// # Returns +/// +/// A `Result` indicating success or failure. pub async fn run(config: RelayerConfig) -> Result<()> { info!(LOG, "Starting relayer"); + // Initialize global configuration CIRCUITS_DIR_PATH.set(config.circuits_dir_path).unwrap(); WEB_SERVER_ADDRESS.set(config.web_server_address).unwrap(); PROVER_ADDRESS.set(config.prover_address).unwrap(); @@ -97,6 +119,7 @@ pub async fn run(config: RelayerConfig) -> Result<()> { .unwrap(); SMTP_SERVER.set(config.smtp_server).unwrap(); + // Spawn the API server task let api_server_task = tokio::task::spawn(async move { loop { match run_server().await { @@ -106,13 +129,14 @@ pub async fn run(config: RelayerConfig) -> Result<()> { } Err(err) => { error!(LOG, "Error api server: {}", err); - // Optionally, add a delay before restarting + // Add a delay before restarting to prevent rapid restart loops tokio::time::sleep(Duration::from_secs(5)).await; } } } }); + // Wait for the API server task to complete let _ = tokio::join!(api_server_task); Ok(()) diff --git a/packages/relayer/src/modules/dkim.rs b/packages/relayer/src/modules/dkim.rs index 21ae7d30..fe91ee7c 100644 --- a/packages/relayer/src/modules/dkim.rs +++ b/packages/relayer/src/modules/dkim.rs @@ -15,17 +15,25 @@ use ic_utils::canister::*; use serde::Deserialize; +/// Represents a client for interacting with the DKIM Oracle. #[derive(Debug, Clone)] pub struct DkimOracleClient<'a> { + /// The canister used for communication pub canister: Canister<'a>, } +/// Represents a signed DKIM public key. #[derive(Default, CandidType, Deserialize, Debug, Clone)] pub struct SignedDkimPublicKey { + /// The selector for the DKIM key pub selector: String, + /// The domain for the DKIM key pub domain: String, + /// The signature of the DKIM key pub signature: String, + /// The public key pub public_key: String, + /// The hash of the public key pub public_key_hash: String, } @@ -41,8 +49,13 @@ impl<'a> DkimOracleClient<'a> { /// /// An `anyhow::Result`. pub fn gen_agent(pem_path: &str, replica_url: &str) -> anyhow::Result { + // Create identity from PEM file let identity = Secp256k1Identity::from_pem_file(pem_path)?; + + // Create transport using the replica URL let transport = ReqwestTransport::create(replica_url)?; + + // Build and return the agent let agent = AgentBuilder::default() .with_identity(identity) .with_transport(transport) @@ -61,6 +74,7 @@ impl<'a> DkimOracleClient<'a> { /// /// An `anyhow::Result`. pub fn new(canister_id: &str, agent: &'a Agent) -> anyhow::Result { + // Build the canister using the provided ID and agent let canister = CanisterBuilder::new() .with_canister_id(canister_id) .with_agent(agent) @@ -83,11 +97,14 @@ impl<'a> DkimOracleClient<'a> { selector: &str, domain: &str, ) -> anyhow::Result { + // Build the request to sign the DKIM public key let request = self .canister .update("sign_dkim_public_key") .with_args((selector, domain)) .build::<(Result,)>(); + + // Call the canister and wait for the response let response = request .call_and_wait_one::>() .await? @@ -117,25 +134,36 @@ pub async fn check_and_update_dkim( wallet_addr: &str, account_salt: &str, ) -> Result<()> { + // Generate public key hash let mut public_key_n = parsed_email.public_key.clone(); public_key_n.reverse(); let public_key_hash = public_key_hash(&public_key_n)?; info!(LOG, "public_key_hash {:?}", public_key_hash); + + // Get email domain let domain = parsed_email.get_email_domain()?; info!(LOG, "domain {:?}", domain); + + // Check if wallet is deployed if CLIENT.get_bytecode(wallet_addr).await? == Bytes::from_static(&[0u8; 20]) { info!(LOG, "wallet not deployed"); return Ok(()); } + + // Get email auth address let email_auth_addr = CLIENT .get_email_auth_addr_from_wallet(controller_eth_addr, wallet_addr, account_salt) .await?; let email_auth_addr = format!("0x{:x}", email_auth_addr); + + // Get DKIM from wallet or email auth let mut dkim = CLIENT.get_dkim_from_wallet(controller_eth_addr).await?; if CLIENT.get_bytecode(&email_auth_addr).await? != Bytes::new() { dkim = CLIENT.get_dkim_from_email_auth(&email_auth_addr).await?; } info!(LOG, "dkim {:?}", dkim); + + // Check if DKIM public key hash is valid if CLIENT .check_if_dkim_public_key_hash_valid( domain.clone(), @@ -147,6 +175,8 @@ pub async fn check_and_update_dkim( info!(LOG, "public key registered"); return Ok(()); } + + // Get selector let selector_def_path = env::var(SELECTOR_DEF_PATH_KEY) .map_err(|_| anyhow!("ENV var {} not set", SELECTOR_DEF_PATH_KEY))?; let selector_def_contents = fs::read_to_string(&selector_def_path) @@ -158,17 +188,25 @@ pub async fn check_and_update_dkim( parsed_email.canonicalized_header[idxes.0..idxes.1].to_string() }; info!(LOG, "selector {}", selector); + + // Generate IC agent and create oracle client let ic_agent = DkimOracleClient::gen_agent( &env::var(PEM_PATH_KEY).unwrap(), &env::var(IC_REPLICA_URL_KEY).unwrap(), )?; let oracle_client = DkimOracleClient::new(&env::var(CANISTER_ID_KEY).unwrap(), &ic_agent)?; + + // Request signature from oracle let oracle_result = oracle_client.request_signature(&selector, &domain).await?; info!(LOG, "DKIM oracle result {:?}", oracle_result); + + // Process oracle response let public_key_hash = hex::decode(&oracle_result.public_key_hash[2..])?; info!(LOG, "public_key_hash from oracle {:?}", public_key_hash); let signature = Bytes::from_hex(&oracle_result.signature[2..])?; info!(LOG, "signature {:?}", signature); + + // Set DKIM public key hash let tx_hash = CLIENT .set_dkim_public_key_hash( selector, diff --git a/packages/relayer/src/modules/mail.rs b/packages/relayer/src/modules/mail.rs index d0fd85f7..5fbc5d9a 100644 --- a/packages/relayer/src/modules/mail.rs +++ b/packages/relayer/src/modules/mail.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::fs::read_to_string; +/// Represents different types of email authentication events. #[derive(Debug, Clone)] pub enum EmailAuthEvent { AcceptanceRequest { @@ -62,6 +63,7 @@ pub enum EmailAuthEvent { NoOp, } +/// Represents an email message to be sent. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailMessage { pub to: String, @@ -73,6 +75,7 @@ pub struct EmailMessage { pub body_attachments: Option>, } +/// Represents an attachment in an email message. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailAttachment { pub inline_id: String, @@ -98,8 +101,10 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> command, account_code, } => { + // Prepare the command with the account code let command = format!("{} Code {}", command, account_code); + // Create the plain text body let body_plain = format!( "You have received an guardian request from the wallet address {}. \ {} Code {}. \ @@ -111,6 +116,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> let subject = "Email Recovery: Acceptance Request".to_string(); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "userEmailAddr": guardian_email_addr, "walletAddress": account_eth_addr, @@ -119,6 +125,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> }); let body_html = render_html("acceptance_request.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject, @@ -145,12 +152,14 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> error ); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "error": error, "userEmailAddr": email_addr, }); let body_html = render_html("error.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: email_addr, subject, @@ -174,12 +183,14 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> guardian_email_addr, account_eth_addr ); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "walletAddress": account_eth_addr, "userEmailAddr": guardian_email_addr, }); let body_html = render_html("guardian_already_exists.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject: subject.to_string(), @@ -208,6 +219,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> let subject = "Email Recovery: Recovery Request".to_string(); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "userEmailAddr": guardian_email_addr, "walletAddress": account_eth_addr, @@ -216,6 +228,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> }); let body_html = render_html("recovery_request.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject, @@ -242,6 +255,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> account_eth_addr, request_id ); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "walletAddress": account_eth_addr, "userEmailAddr": guardian_email_addr, @@ -249,6 +263,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> }); let body_html = render_html("acceptance_success.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject: subject.to_string(), @@ -275,6 +290,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> account_eth_addr, request_id ); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "walletAddress": account_eth_addr, "userEmailAddr": guardian_email_addr, @@ -282,6 +298,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> }); let body_html = render_html("recovery_success.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject: subject.to_string(), @@ -301,12 +318,14 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> let subject = "Guardian Not Set"; let body_plain = format!("Guardian not set for wallet address {}", account_eth_addr); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "walletAddress": account_eth_addr, "userEmailAddr": guardian_email_addr, }); let body_html = render_html("guardian_not_set.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject: subject.to_string(), @@ -335,6 +354,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> account_eth_addr, request_id ); + // Prepare data for HTML rendering let render_data = serde_json::json!({ "userEmailAddr": guardian_email_addr, "walletAddress": account_eth_addr, @@ -345,6 +365,7 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> let subject = "Guardian Not Registered".to_string(); let body_html = render_html("credential_not_present.html", render_data).await?; + // Create and send the email let email = EmailMessage { to: guardian_email_addr, subject, @@ -367,9 +388,11 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> "Hi {}!\nYour email with the command {} is received.", email_addr, command ); + // Prepare data for HTML rendering let render_data = serde_json::json!({"userEmailAddr": email_addr, "request": command}); let body_html = render_html("acknowledgement.html", render_data).await?; let subject = format!("Re: {}", original_subject); + // Create and send the email let email = EmailMessage { to: email_addr, subject, @@ -397,10 +420,13 @@ pub async fn handle_email_event(event: EmailAuthEvent) -> Result<(), EmailError> /// # Returns /// /// A `Result` containing the rendered HTML string or an `EmailError`. -pub async fn render_html(template_name: &str, render_data: Value) -> Result { +async fn render_html(template_name: &str, render_data: Value) -> Result { + // Construct the full path to the email template let email_template_filename = PathBuf::new() .join(EMAIL_TEMPLATES.get().unwrap()) .join(template_name); + + // Read the email template file let email_template = read_to_string(&email_template_filename) .await .map_err(|e| { @@ -410,8 +436,10 @@ pub async fn render_html(template_name: &str, render_data: Value) -> Result Result` with the parsed error message. -pub fn parse_error(error: String) -> Result> { +fn parse_error(error: String) -> Result> { let mut error = error; if error.contains("Contract call reverted with data: ") { + // Extract and decode the revert data let revert_data = error .replace("Contract call reverted with data: ", "") .split_at(10) @@ -441,6 +470,7 @@ pub fn parse_error(error: String) -> Result> { error = String::from_utf8(revert_bytes).unwrap().trim().to_string(); } + // Match known error messages and provide user-friendly responses match error.as_str() { "Account is already created" => Ok(Some(error)), "insufficient balance" => Ok(Some("You don't have sufficient balance".to_string())), @@ -453,11 +483,12 @@ pub fn parse_error(error: String) -> Result> { /// # Arguments /// /// * `email` - The `EmailMessage` to be sent. +/// * `expects_reply` - An optional `ExpectsReply` struct indicating if a reply is expected. /// /// # Returns /// /// A `Result` indicating success or an `EmailError`. -pub async fn send_email( +async fn send_email( email: EmailMessage, expects_reply: Option, ) -> Result<(), EmailError> { @@ -472,6 +503,7 @@ pub async fn send_email( .await .map_err(|e| EmailError::Send(format!("Failed to send email: {}", e)))?; + // Check if the email was sent successfully if !response.status().is_success() { return Err(EmailError::Send(format!( "Failed to send email: {}", @@ -479,6 +511,7 @@ pub async fn send_email( ))); } + // Handle expected reply if necessary if let Some(expects_reply) = expects_reply { let response_body: EmailResponse = response .json() @@ -493,23 +526,31 @@ pub async fn send_email( Ok(()) } +/// Represents the response from the email server after sending an email. #[derive(Debug, Clone, Serialize, Deserialize)] struct EmailResponse { status: String, message_id: String, } +/// Represents an expectation of a reply to an email. pub struct ExpectsReply { request_id: Option, } impl ExpectsReply { + /// Creates a new `ExpectsReply` instance with a request ID. + /// + /// # Arguments + /// + /// * `request_id` - The ID of the request expecting a reply. fn new(request_id: u32) -> Self { Self { request_id: Some(request_id.to_string()), } } + /// Creates a new `ExpectsReply` instance without a request ID. fn new_no_request_id() -> Self { Self { request_id: None } } @@ -518,17 +559,27 @@ impl ExpectsReply { /// Checks if the email is a reply to a command that expects a reply. /// Will return false for duplicate replies. /// Will return true if the email is not a reply. +/// +/// # Arguments +/// +/// * `email` - The `ParsedEmail` to be checked. +/// +/// # Returns +/// +/// A `Result` containing a boolean indicating if the request is valid. pub async fn check_is_valid_request(email: &ParsedEmail) -> Result { + // Check if the email is a reply by looking for the "In-Reply-To" header let reply_message_id = match email .headers .get_header("In-Reply-To") .and_then(|v| v.first().cloned()) { Some(id) => id, - // Email is not a reply + // Email is not a reply, so it's valid None => return Ok(true), }; + // Check if the reply is valid (not a duplicate) using the database let is_valid = DB.is_valid_reply(&reply_message_id).await?; Ok(is_valid) } diff --git a/packages/relayer/src/modules/mod.rs b/packages/relayer/src/modules/mod.rs index 5ace1b92..eaf1adc5 100644 --- a/packages/relayer/src/modules/mod.rs +++ b/packages/relayer/src/modules/mod.rs @@ -1,3 +1,5 @@ +//! This module contains the dkim, mail and web_server modules. + pub mod dkim; pub mod mail; pub mod web_server; diff --git a/packages/relayer/src/modules/web_server/relayer_errors.rs b/packages/relayer/src/modules/web_server/relayer_errors.rs index 10959c21..8b49462a 100644 --- a/packages/relayer/src/modules/web_server/relayer_errors.rs +++ b/packages/relayer/src/modules/web_server/relayer_errors.rs @@ -10,6 +10,7 @@ use rustc_hex::FromHexError; use serde_json::json; use thiserror::Error; +/// Custom error type for API-related errors #[derive(Error, Debug)] pub enum ApiError { #[error("Database error: {0}")] @@ -30,6 +31,7 @@ pub enum ApiError { Email(#[from] EmailError), } +/// Custom error type for email-related errors #[derive(Error, Debug)] pub enum EmailError { #[error("Email body error: {0}")] @@ -65,6 +67,7 @@ pub enum EmailError { Anyhow(#[from] anyhow::Error), } +/// Custom error type for blockchain-related errors #[derive(Error, Debug)] pub enum ChainError { #[error("Contract error: {0}")] @@ -98,6 +101,7 @@ impl ChainError { } } +/// Custom error type for database-related errors #[derive(Debug, thiserror::Error)] #[error("{msg}: {source}")] pub struct DatabaseError { @@ -115,6 +119,7 @@ impl DatabaseError { } } +/// Wrapper for contract-related errors #[derive(Debug)] pub struct ContractErrorWrapper { msg: String, @@ -136,6 +141,7 @@ impl ContractErrorWrapper { } } +/// Wrapper for signer middleware-related errors #[derive(Debug)] pub struct SignerMiddlewareErrorWrapper { msg: String, @@ -160,6 +166,7 @@ impl SignerMiddlewareErrorWrapper { } } +/// Wrapper for provider-related errors #[derive(Debug)] pub struct ProviderErrorWrapper { msg: String, diff --git a/packages/relayer/src/modules/web_server/rest_api.rs b/packages/relayer/src/modules/web_server/rest_api.rs index add61e88..8b0093d0 100644 --- a/packages/relayer/src/modules/web_server/rest_api.rs +++ b/packages/relayer/src/modules/web_server/rest_api.rs @@ -20,6 +20,8 @@ pub async fn request_status_api( Json(payload): Json, ) -> Result, ApiError> { let row = DB.get_request(payload.request_id).await?; + + // Determine the status based on the retrieved row let status = if let Some(ref row) = row { if row.is_processed { RequestStatus::Processed @@ -29,6 +31,7 @@ pub async fn request_status_api( } else { RequestStatus::NotExist }; + Ok(Json(RequestStatusResponse { request_id: payload.request_id, status, @@ -56,9 +59,11 @@ pub async fn handle_acceptance_request( .get_acceptance_command_templates(&payload.controller_eth_addr, payload.template_idx) .await?; + // Extract and validate command parameters let command_params = extract_template_vals(&payload.command, command_template) .map_err(|_| ApiError::Validation("Invalid command".to_string()))?; + // Recover the account address let account_eth_addr = CLIENT .get_recovered_account_from_acceptance_command( &payload.controller_eth_addr, @@ -69,66 +74,19 @@ pub async fn handle_acceptance_request( let account_eth_addr = format!("0x{:x}", account_eth_addr); + // Check if the wallet is deployed if !CLIENT.is_wallet_deployed(&account_eth_addr).await? { return Err(ApiError::Validation("Wallet not deployed".to_string())); } - // Check if hash of bytecode of proxy contract is equal or not - let bytecode = CLIENT.get_bytecode(&account_eth_addr).await?; - let bytecode_hash = format!("0x{}", hex::encode(keccak256(bytecode.as_ref()))); - - // let permitted_wallets: Vec = - // serde_json::from_str(include_str!("../../permitted_wallets.json")).unwrap(); - // let permitted_wallet = permitted_wallets - // .iter() - // .find(|w| w.hash_of_bytecode_of_proxy == bytecode_hash); - - // if let Some(permitted_wallet) = permitted_wallet { - // let slot_location = permitted_wallet.slot_location.parse::().unwrap(); - // let impl_contract_from_proxy = { - // let raw_hex = hex::encode( - // CLIENT - // .get_storage_at(&account_eth_addr, slot_location) - // .await - // .unwrap(), - // ); - // format!("0x{}", &raw_hex[24..]) - // }; - - // if !permitted_wallet - // .impl_contract_address - // .eq_ignore_ascii_case(&impl_contract_from_proxy) - // { - // return Response::builder() - // .status(StatusCode::BAD_REQUEST) - // .body(Body::from( - // "Invalid bytecode, impl contract address mismatch", - // )) - // .unwrap(); - // } - - // if !permitted_wallet - // .controller_eth_addr - // .eq_ignore_ascii_case(&payload.controller_eth_addr) - // { - // return Response::builder() - // .status(StatusCode::BAD_REQUEST) - // .body(Body::from("Invalid controller eth addr")) - // .unwrap(); - // } - // } else { - // return Response::builder() - // .status(StatusCode::BAD_REQUEST) - // .body(Body::from("Wallet not permitted")) - // .unwrap(); - // } - + // Check if the account code is already used if let Ok(Some(creds)) = DB.get_credentials(&payload.account_code).await { return Err(ApiError::Validation( "Account code already used".to_string(), )); } + // Generate a unique request ID let mut request_id = rand::thread_rng().gen::(); while let Ok(Some(request)) = DB.get_request(request_id).await { request_id = rand::thread_rng().gen::(); @@ -150,6 +108,7 @@ pub async fn handle_acceptance_request( }) .await?; + // Handle different scenarios based on guardian status if DB .is_guardian_set(&account_eth_addr, &payload.guardian_email_addr) .await? @@ -165,8 +124,7 @@ pub async fn handle_acceptance_request( .is_wallet_and_email_registered(&account_eth_addr, &payload.guardian_email_addr) .await? { - // In this case, the relayer sent a request email to the same guardian before, but it has not been replied yet. - // Therefore, the relayer will send an email to the guardian again with a fresh account code. + // Update credentials and send acceptance request email DB.update_credentials_of_wallet_and_email(&Credentials { account_code: payload.account_code.clone(), account_eth_addr: account_eth_addr.clone(), @@ -184,6 +142,7 @@ pub async fn handle_acceptance_request( }) .await?; } else { + // Insert new credentials and send acceptance request email DB.insert_credentials(&Credentials { account_code: payload.account_code.clone(), account_eth_addr: account_eth_addr.clone(), @@ -220,13 +179,16 @@ pub async fn handle_acceptance_request( pub async fn handle_recovery_request( Json(payload): Json, ) -> Result, ApiError> { + // Fetch the command template let command_template = CLIENT .get_recovery_command_templates(&payload.controller_eth_addr, payload.template_idx) .await?; + // Extract and validate command parameters let command_params = extract_template_vals(&payload.command, command_template) .map_err(|_| ApiError::Validation("Invalid command".to_string()))?; + // Recover the account address let account_eth_addr = CLIENT .get_recovered_account_from_recovery_command( &payload.controller_eth_addr, @@ -237,65 +199,18 @@ pub async fn handle_recovery_request( let account_eth_addr = format!("0x{:x}", account_eth_addr); + // Check if the wallet is deployed if !CLIENT.is_wallet_deployed(&account_eth_addr).await? { return Err(ApiError::Validation("Wallet not deployed".to_string())); } - // Check if hash of bytecode of proxy contract is equal or not - let bytecode = CLIENT.get_bytecode(&account_eth_addr).await?; - let bytecode_hash = format!("0x{}", hex::encode(keccak256(bytecode.as_ref()))); - - // let permitted_wallets: Vec = - // serde_json::from_str(include_str!("../../permitted_wallets.json")).unwrap(); - // let permitted_wallet = permitted_wallets - // .iter() - // .find(|w| w.hash_of_bytecode_of_proxy == bytecode_hash); - - // if let Some(permitted_wallet) = permitted_wallet { - // let slot_location = permitted_wallet.slot_location.parse::().unwrap(); - // let impl_contract_from_proxy = { - // let raw_hex = hex::encode( - // CLIENT - // .get_storage_at(&account_eth_addr, slot_location) - // .await - // .unwrap(), - // ); - // format!("0x{}", &raw_hex[24..]) - // }; - - // if !permitted_wallet - // .impl_contract_address - // .eq_ignore_ascii_case(&impl_contract_from_proxy) - // { - // return Response::builder() - // .status(StatusCode::BAD_REQUEST) - // .body(Body::from( - // "Invalid bytecode, impl contract address mismatch", - // )) - // .unwrap(); - // } - - // if !permitted_wallet - // .controller_eth_addr - // .eq_ignore_ascii_case(&payload.controller_eth_addr) - // { - // return Response::builder() - // .status(StatusCode::BAD_REQUEST) - // .body(Body::from("Invalid controller eth addr")) - // .unwrap(); - // } - // } else { - // return Response::builder() - // .status(StatusCode::BAD_REQUEST) - // .body(Body::from("Wallet not permitted")) - // .unwrap(); - // } - + // Generate a unique request ID let mut request_id = rand::thread_rng().gen::(); while let Ok(Some(request)) = DB.get_request(request_id).await { request_id = rand::thread_rng().gen::(); } + // Fetch account details and calculate account salt let account = DB .get_credentials_from_wallet_and_email(&account_eth_addr, &payload.guardian_email_addr) .await?; @@ -306,6 +221,7 @@ pub async fn handle_recovery_request( return Err(ApiError::Validation("Wallet not deployed".to_string())); }; + // Handle the case when wallet and email are not registered if !DB .is_wallet_and_email_registered(&account_eth_addr, &payload.guardian_email_addr) .await? @@ -338,6 +254,7 @@ pub async fn handle_recovery_request( })); } + // Insert the recovery request DB.insert_request(&Request { request_id, account_eth_addr: account_eth_addr.clone(), @@ -352,6 +269,7 @@ pub async fn handle_recovery_request( }) .await?; + // Handle different scenarios based on guardian status if DB .is_guardian_set(&account_eth_addr, &payload.guardian_email_addr) .await? @@ -393,10 +311,12 @@ pub async fn handle_recovery_request( pub async fn handle_complete_recovery_request( Json(payload): Json, ) -> Result { + // Check if the wallet is deployed if !CLIENT.is_wallet_deployed(&payload.account_eth_addr).await? { return Err(ApiError::Validation("Wallet not deployed".to_string())); } + // Attempt to complete the recovery match CLIENT .complete_recovery( &payload.controller_eth_addr, @@ -455,6 +375,7 @@ pub async fn get_account_salt( pub async fn inactive_guardian( Json(payload): Json, ) -> Result { + // Check if the wallet is activated let is_activated = CLIENT .get_is_activated(&payload.controller_eth_addr, &payload.account_eth_addr) .await?; @@ -464,18 +385,31 @@ pub async fn inactive_guardian( } trace!(LOG, "Inactive guardian"; "is_activated" => is_activated); + + // Parse and format the account Ethereum address let account_eth_addr: Address = payload .account_eth_addr .parse() .map_err(|e| ApiError::Validation(format!("Failed to parse account_eth_addr: {}", e)))?; let account_eth_addr = format!("0x{:x}", &account_eth_addr); trace!(LOG, "Inactive guardian"; "account_eth_addr" => &account_eth_addr); + + // Update the credentials of the inactive guardian DB.update_credentials_of_inactive_guardian(false, &account_eth_addr) .await?; Ok("Guardian inactivated".to_string()) } +/// Parses an error message from contract call data. +/// +/// # Arguments +/// +/// * `error_data` - The error data as a `String`. +/// +/// # Returns +/// +/// A `String` containing the parsed error message or a default error message. fn parse_error_message(error_data: String) -> String { // Attempt to extract and decode the error message if let Some(hex_error) = error_data.split(" ").last() { @@ -509,6 +443,7 @@ pub async fn receive_email_api_fn(email: String) -> Result<(), ApiError> { return; } + // Send acknowledgment email match handle_email_event(EmailAuthEvent::Ack { email_addr: from_addr.clone(), command: parsed_email.get_command(false).unwrap_or_default(), @@ -524,6 +459,8 @@ pub async fn receive_email_api_fn(email: String) -> Result<(), ApiError> { error!(LOG, "Error handling email event: {:?}", e); } } + + // Process the email match handle_email(email.clone()).await { Ok(event) => match handle_email_event(event).await { Ok(_) => {} @@ -555,80 +492,125 @@ pub async fn receive_email_api_fn(email: String) -> Result<(), ApiError> { Ok(()) } +/// Request status request structure. #[derive(Serialize, Deserialize)] pub struct RequestStatusRequest { + /// The unique identifier for the request. pub request_id: u32, } +/// Enum representing the possible statuses of a request. #[derive(Serialize, Deserialize)] pub enum RequestStatus { + /// The request does not exist. NotExist = 0, + /// The request is pending processing. Pending = 1, + /// The request has been processed. Processed = 2, } +/// Response structure for a request status query. #[derive(Serialize, Deserialize)] pub struct RequestStatusResponse { + /// The unique identifier for the request. pub request_id: u32, + /// The current status of the request. pub status: RequestStatus, + /// Indicates whether the request was successful. pub is_success: bool, + /// The email nullifier, if available. pub email_nullifier: Option, + /// The account salt, if available. pub account_salt: Option, } +/// Request structure for an acceptance request. #[derive(Serialize, Deserialize)] pub struct AcceptanceRequest { + /// The Ethereum address of the controller. pub controller_eth_addr: String, + /// The email address of the guardian. pub guardian_email_addr: String, + /// The unique account code. pub account_code: String, + /// The index of the template to use. pub template_idx: u64, + /// The command to execute. pub command: String, } +/// Response structure for an acceptance request. #[derive(Serialize, Deserialize)] pub struct AcceptanceResponse { + /// The unique identifier for the request. pub request_id: u32, + /// The parameters extracted from the command. pub command_params: Vec, } +/// Request structure for a recovery request. #[derive(Serialize, Deserialize, Debug)] pub struct RecoveryRequest { + /// The Ethereum address of the controller. pub controller_eth_addr: String, + /// The email address of the guardian. pub guardian_email_addr: String, + /// The index of the template to use. pub template_idx: u64, + /// The command to execute. pub command: String, } +/// Response structure for a recovery request. #[derive(Serialize, Deserialize)] pub struct RecoveryResponse { + /// The unique identifier for the request. pub request_id: u32, + /// The parameters extracted from the command. pub command_params: Vec, } +/// Request structure for completing a recovery. #[derive(Serialize, Deserialize)] pub struct CompleteRecoveryRequest { + /// The Ethereum address of the account to recover. pub account_eth_addr: String, + /// The Ethereum address of the controller. pub controller_eth_addr: String, + /// The calldata to complete the recovery. pub complete_calldata: String, } +/// Request structure for retrieving an account salt. #[derive(Serialize, Deserialize)] pub struct GetAccountSaltRequest { + /// The unique account code. pub account_code: String, + /// The email address associated with the account. pub email_addr: String, } +/// Structure representing a permitted wallet. #[derive(Deserialize)] struct PermittedWallet { + /// The name of the wallet. wallet_name: String, + /// The Ethereum address of the controller. controller_eth_addr: String, + /// The hash of the bytecode of the proxy contract. hash_of_bytecode_of_proxy: String, + /// The address of the implementation contract. impl_contract_address: String, + /// The slot location in storage. slot_location: String, } +/// Request structure for marking a guardian as inactive. #[derive(Serialize, Deserialize)] pub struct InactiveGuardianRequest { + /// The Ethereum address of the account. pub account_eth_addr: String, + /// The Ethereum address of the controller. pub controller_eth_addr: String, } diff --git a/packages/relayer/src/modules/web_server/server.rs b/packages/relayer/src/modules/web_server/server.rs index 594ee4c5..b8f371c3 100644 --- a/packages/relayer/src/modules/web_server/server.rs +++ b/packages/relayer/src/modules/web_server/server.rs @@ -11,6 +11,7 @@ use tower_http::cors::{AllowHeaders, AllowMethods, Any, CorsLayer}; pub async fn run_server() -> Result<()> { let addr = WEB_SERVER_ADDRESS.get().unwrap(); + // Initialize the global DB ref before starting the server DB_CELL .get_or_init(|| async { dotenv::dotenv().ok(); @@ -28,6 +29,7 @@ pub async fn run_server() -> Result<()> { }; info!(LOG, "Testing connection to database successfull"); + // Initialize the API routes let mut app = Router::new() .route( "/api/echo", @@ -51,6 +53,7 @@ pub async fn run_server() -> Result<()> { .allow_origin(Any), ); + // Start the server trace!(LOG, "Listening API at {}", addr); axum::Server::bind(&addr.parse()?) .serve(app.into_make_service())