diff --git a/tee-worker/omni-executor/rpc-server/src/auth_utils.rs b/tee-worker/omni-executor/rpc-server/src/auth_utils.rs deleted file mode 100644 index 018a635a1e..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/auth_utils.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::utils::user_op::convert_to_packed_user_op; -use crate::{error_code::AUTH_VERIFICATION_FAILED_CODE, ErrorCode}; -use aa_contracts_client::calculate_user_operation_hash; -use alloy::primitives::Address; -use executor_core::types::SerializablePackedUserOperation; -use executor_crypto::ecdsa; -use executor_primitives::{ - signature::{EthereumSignature, HeimaMultiSignature}, - utils::hex::decode_hex, - ChainId, -}; -use executor_storage::{Storage, WildmetaTimestampStorage}; -use heima_primitives::{Address20, Identity}; -use jsonrpsee::types::ErrorObject; -use std::sync::Arc; -use tracing::error; - -/// Verify WildMeta signature -pub fn verify_wildmeta_signature( - agent_address: &str, - business_json: &str, - signature: &str, -) -> Result<(), ErrorObject<'static>> { - let message = business_json.as_bytes(); - - let signature_bytes = decode_hex(signature).map_err(|e| { - error!("Failed to decode signature: {:?}", e); - ErrorObject::from(ErrorCode::ParseError) - })?; - let ethereum_signature = - EthereumSignature::try_from(signature_bytes.as_slice()).map_err(|e| { - error!("Failed to convert signature to EthereumSignature: {:?}", e); - ErrorObject::from(ErrorCode::ParseError) - })?; - let heima_sig = HeimaMultiSignature::Ethereum(ethereum_signature); - - let agent_address_bytes = decode_hex(agent_address).map_err(|e| { - error!("Failed to decode signature: {:?}", e); - ErrorObject::from(ErrorCode::ParseError) - })?; - let agent_address = Address20::try_from(agent_address_bytes.as_slice()).map_err(|_| { - error!("Failed to parse agent address"); - ErrorObject::from(ErrorCode::ParseError) - })?; - - let agent_identity = Identity::Evm(agent_address); - - if !heima_sig.verify(message, &agent_identity) { - error!("Signature verification failed"); - return Err(ErrorObject::from(ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE))); - } - - Ok(()) -} - -/// Verify WildmetaBackend signature against user operation hash -pub fn verify_wildmeta_backend_signature( - signature: &str, - user_operations: &[SerializablePackedUserOperation], - chain_id: ChainId, - entry_point_address: Address, - expected_pubkey: &[u8; 33], -) -> Result<(), ErrorObject<'static>> { - if user_operations.is_empty() { - error!("No user operations provided for signature verification"); - return Err(ErrorObject::from(ErrorCode::ParseError)); - } - - // Decode signature from hex - let signature_bytes = decode_hex(signature).map_err(|e| { - error!("Failed to decode signature: {:?}", e); - ErrorObject::from(ErrorCode::ParseError) - })?; - - if signature_bytes.len() != 65 { - error!("Invalid signature length: expected 65 bytes, got {}", signature_bytes.len()); - return Err(ErrorObject::from(ErrorCode::ParseError)); - } - - let signature_array: [u8; 65] = signature_bytes - .try_into() - .map_err(|_| ErrorObject::from(ErrorCode::ParseError))?; - - // Convert all user operations to PackedUserOperations and calculate their combined hash - let mut combined_hash_data = Vec::new(); - - for user_op in user_operations { - let packed_user_op = convert_to_packed_user_op(user_op.clone()).map_err(|e| { - error!("Failed to convert user operation: {}", e); - ErrorObject::from(ErrorCode::ParseError) - })?; - - let user_op_hash = - calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); - combined_hash_data.extend_from_slice(&user_op_hash.0); - } - - // Hash the combined data using keccak256 to create a single 32-byte hash - use alloy::primitives::keccak256; - let user_op_hash = keccak256(&combined_hash_data); - - // Convert user op hash to 32-byte array - let user_op_hash_array: [u8; 32] = user_op_hash.0; - - // Convert expected public key to ecdsa::Public - let public_key = ecdsa::Public::from_raw(*expected_pubkey); - - let signature = ecdsa::Signature::from_raw(signature_array); - - // Verify signature directly using verify_prehashed - if !ecdsa::Pair::verify_prehashed(&signature, &user_op_hash_array, &public_key) { - error!("Signature verification failed"); - return Err(ErrorObject::from(ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE))); - } - - Ok(()) -} - -/// Verify and update payload timestamp to prevent replay attacks -pub fn verify_payload_timestamp( - storage: &Arc, - main_address: &str, - new_timestamp: u64, -) -> Result<(), ErrorObject<'static>> { - let last_timestamp = storage - .get(&main_address.to_string()) - .map_err(|_| { - error!("Failed to get last timestamp"); - ErrorObject::from(ErrorCode::InternalError) - })? - .unwrap_or(0); - - if new_timestamp <= last_timestamp { - error!("Invalid payload timestamp: {} <= {}", new_timestamp, last_timestamp); - return Err(ErrorObject::from(ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE))); - } - - storage.insert(&main_address.to_string(), new_timestamp).map_err(|_| { - error!("Failed to store timestamp"); - ErrorObject::from(ErrorCode::InternalError) - })?; - - Ok(()) -} diff --git a/tee-worker/omni-executor/rpc-server/src/detailed_error.rs b/tee-worker/omni-executor/rpc-server/src/detailed_error.rs index 545d4825f5..806bb0c954 100644 --- a/tee-worker/omni-executor/rpc-server/src/detailed_error.rs +++ b/tee-worker/omni-executor/rpc-server/src/detailed_error.rs @@ -1,11 +1,11 @@ use crate::error_code::{ - ACCOUNT_PARSE_ERROR_CODE, EMAIL_SERVICE_ERROR_CODE, GAS_ESTIMATION_FAILED_CODE, - INVALID_ADDRESS_FORMAT_CODE, INVALID_AMOUNT_CODE, INVALID_CHAIN_ID_CODE, - INVALID_HEX_FORMAT_CODE, INVALID_USER_OPERATION_CODE, INVALID_WALLET_INDEX_CODE, - SIGNATURE_SERVICE_UNAVAILABLE_CODE, SIGNER_SERVICE_ERROR_CODE, STORAGE_SERVICE_ERROR_CODE, - UNEXPECTED_RESPONSE_TYPE_CODE, + EMAIL_SERVICE_ERROR_CODE, GAS_ESTIMATION_FAILED_CODE, INVALID_BACKEND_RESPONSE_CODE, + INVALID_USEROP_CODE, PUMPX_SERVICE_ERROR_CODE, SIGNER_SERVICE_ERROR_CODE, + STORAGE_SERVICE_ERROR_CODE, WILDMETA_SERVICE_ERROR_CODE, }; -use jsonrpsee::types::ErrorObject; +use jsonrpsee::types::{ErrorCode, ErrorObject, ErrorObjectOwned}; +use parity_scale_codec::Codec; +use pumpx::methods::common::ApiResponse; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,6 +17,8 @@ pub struct DetailedError { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ErrorDetails { + #[serde(skip_serializing_if = "Option::is_none")] + pub backend_response: Option, #[serde(skip_serializing_if = "Option::is_none")] pub field: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -29,12 +31,19 @@ pub struct ErrorDetails { pub suggestion: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BackendResponse { + pub code: i32, + pub message: String, +} + impl DetailedError { pub fn new(code: i32, message: impl Into) -> Self { Self { code, message: message.into(), details: ErrorDetails { + backend_response: None, field: None, expected: None, received: None, @@ -69,76 +78,49 @@ impl DetailedError { self } - pub fn to_error_object(&self) -> ErrorObject<'static> { + pub fn with_backend_response(mut self, code: i32, message: impl Into) -> Self { + self.details.backend_response = Some(BackendResponse { code, message: message.into() }); + self + } + + pub fn to_rpc_error(&self) -> ErrorObjectOwned { ErrorObject::owned(self.code, self.message.clone(), Some(self.details.clone())) } } impl DetailedError { - pub fn invalid_chain_id(chain_id: u64, supported_chains: &[u64]) -> Self { - Self::new(INVALID_CHAIN_ID_CODE, "Invalid or unsupported chain ID") - .with_field("chain_id") - .with_received(chain_id.to_string()) - .with_expected(format!("One of: {:?}", supported_chains)) - .with_suggestion("Use a supported chain ID from the list") + pub fn internal_error(reason: &str) -> Self { + Self::new(ErrorCode::InternalError.code(), ErrorCode::InternalError.message()) + .with_reason(reason) } - pub fn invalid_wallet_index(index: u32, max_index: u32) -> Self { - Self::new(INVALID_WALLET_INDEX_CODE, "Wallet index out of bounds") - .with_field("wallet_index") - .with_received(index.to_string()) - .with_expected(format!("0 to {}", max_index)) - .with_suggestion(format!("Use a wallet index between 0 and {}", max_index)) + pub fn parse_error(reason: &str) -> Self { + Self::new(ErrorCode::ParseError.code(), ErrorCode::ParseError.message()).with_reason(reason) } - pub fn invalid_address_format(field: &str, address: &str, expected_format: &str) -> Self { - Self::new(INVALID_ADDRESS_FORMAT_CODE, "Invalid address format") + pub fn invalid_params(field: &str, reason: &str) -> Self { + Self::new(ErrorCode::InvalidParams.code(), ErrorCode::InvalidParams.message()) .with_field(field) - .with_received(address.to_string()) - .with_expected(expected_format) - .with_suggestion("Ensure the address follows the correct format") + .with_reason(reason) } - pub fn invalid_amount(field: &str, amount: &str, reason: &str) -> Self { - Self::new(INVALID_AMOUNT_CODE, "Invalid amount value") - .with_field(field) - .with_received(amount.to_string()) - .with_expected("Positive number within valid range") - .with_suggestion(reason) - } - - pub fn invalid_hex_format(field: &str, value: &str, expected_length: Option) -> Self { - let expected = if let Some(len) = expected_length { - format!("0x-prefixed hex string of {} bytes", len) - } else { - "Valid hexadecimal string".to_string() - }; - - Self::new(INVALID_HEX_FORMAT_CODE, "Invalid hexadecimal format") - .with_field(field) - .with_received(value.to_string()) - .with_expected(expected) - .with_suggestion("Check that the value is properly hex-encoded") + pub fn invalid_backend_response(response: &ApiResponse, op: &str) -> Self { + Self::new(INVALID_BACKEND_RESPONSE_CODE, "Invalid backend response") + .with_field(op) + .with_backend_response(response.code as i32, response.message.to_owned()) } - pub fn account_parse_error(account: &str, error: &str) -> Self { - Self::new(ACCOUNT_PARSE_ERROR_CODE, "Failed to parse account identifier") - .with_field("omni_account") - .with_received(account.to_string()) - .with_expected("Valid 32-byte account identifier") - .with_suggestion(format!("Error: {}", error)) + pub fn invalid_chain_id(chain_id: u64) -> Self { + let supported: Vec = + crate::config::SUPPORTED_EVM_CHAINS.iter().map(|&c| c as u64).collect(); + Self::invalid_params( + "chain_id", + &format!("invalid chain_id: {}, expected one of {:?}", chain_id, supported), + ) } - pub fn unexpected_response_type(expected: &str, received: &str) -> Self { - Self::new(UNEXPECTED_RESPONSE_TYPE_CODE, "Unexpected response type from service") - .with_expected(expected) - .with_received(received) - .with_suggestion("This is likely an internal error. Please contact support.") - } - - pub fn signer_service_error(operation: &str, error: &str) -> Self { - Self::new(SIGNER_SERVICE_ERROR_CODE, format!("Signer service error during {}", operation)) - .with_suggestion(format!("Signer error: {}", error)) + pub fn signer_service_error() -> Self { + Self::new(SIGNER_SERVICE_ERROR_CODE, "Signer service error") } pub fn email_service_error(email: &str) -> Self { @@ -148,35 +130,30 @@ impl DetailedError { .with_suggestion("Failed to send verification email. Please try again later.") } - pub fn storage_error(operation: &str) -> Self { - Self::new(STORAGE_SERVICE_ERROR_CODE, format!("Storage service error during {}", operation)) - .with_suggestion("Storage operation failed. Please try again later.") + pub fn storage_service_error(op: &str) -> Self { + Self::new(STORAGE_SERVICE_ERROR_CODE, format!("Storage service error in {}", op)) } - // Native task error factory methods - pub fn chain_not_supported(chain_id: u64) -> Self { - // Get supported chains from config - use crate::config::SUPPORTED_EVM_CHAINS; - let supported: Vec = SUPPORTED_EVM_CHAINS.iter().map(|&c| c as u64).collect(); + pub fn pumpx_service_error(op: &str, reason: impl Into) -> Self { + Self::new(PUMPX_SERVICE_ERROR_CODE, format!("Pumpx service error in {}", op)) + .with_reason(reason) + } - Self::new(INVALID_CHAIN_ID_CODE, "Chain not supported") - .with_field("chain_id") - .with_received(chain_id.to_string()) - .with_expected(format!("One of: {:?}", supported)) - .with_suggestion("Please use a supported chain ID") + pub fn wildmeta_service_error(op: &str) -> Self { + Self::new(WILDMETA_SERVICE_ERROR_CODE, format!("Wildmeta service error in {}", op)) } - pub fn invalid_user_operation_error(description: &str) -> Self { - Self::new(INVALID_USER_OPERATION_CODE, description) + pub fn invalid_user_op(reason: &str) -> Self { + Self::new(INVALID_USEROP_CODE, "Invalid UserOp").with_received(reason) } pub fn gas_estimation_failed() -> Self { - Self::new(GAS_ESTIMATION_FAILED_CODE, "Unable to estimate gas for operation") - .with_suggestion("Please check the user operation parameters and try again") + Self::new(GAS_ESTIMATION_FAILED_CODE, "Gas estimaton failed") } +} - pub fn signature_service_unavailable() -> Self { - Self::new(SIGNATURE_SERVICE_UNAVAILABLE_CODE, "Signature service temporarily unavailable") - .with_suggestion("Please try again in a few moments") +impl From for ErrorObjectOwned { + fn from(error: DetailedError) -> Self { + error.to_rpc_error() } } diff --git a/tee-worker/omni-executor/rpc-server/src/error_code.rs b/tee-worker/omni-executor/rpc-server/src/error_code.rs index a6f36c1e8a..be54a8280d 100644 --- a/tee-worker/omni-executor/rpc-server/src/error_code.rs +++ b/tee-worker/omni-executor/rpc-server/src/error_code.rs @@ -1,73 +1,17 @@ // we should use -32000 to -32099 for implementation defined error codes, // see https://www.jsonrpc.org/specification#error_object -// Standard JSON-RPC error codes -pub const PARSE_ERROR_CODE: i32 = -32700; -pub const INVALID_PARAMS_CODE: i32 = -32602; -pub const INTERNAL_ERROR_CODE: i32 = -32603; - // Server-defined error codes (-32000 to -32099) -#[allow(dead_code)] -pub const INVALID_RAW_REQUEST_CODE: i32 = -32000; -pub const DECRYPT_REQUEST_FAILED_CODE: i32 = -32001; -#[allow(dead_code)] -pub const DECODE_REQUEST_FAILED_CODE: i32 = -32002; +pub const INVALID_BACKEND_RESPONSE_CODE: i32 = -32000; +pub const INVALID_USEROP_CODE: i32 = -32001; +pub const INVALID_RPC_EXTENSION: i32 = -32002; pub const AUTH_VERIFICATION_FAILED_CODE: i32 = -32003; -#[allow(dead_code)] -pub const REQUIRE_ENCRYPTED_REQUEST_CODE: i32 = -32004; -pub const AES_KEY_CONVERT_FAILED_CODE: i32 = -32005; - -// Native task error codes -#[allow(dead_code)] -pub const UNAUTHORIZED_SENDER_CODE: i32 = -32006; -#[allow(dead_code)] -pub const AUTH_TOKEN_CREATION_FAILED_CODE: i32 = -32007; -#[allow(dead_code)] -pub const INVALID_MEMBER_IDENTITY_CODE: i32 = -32008; -#[allow(dead_code)] -pub const VALIDATION_DATA_VERIFICATION_FAILED_CODE: i32 = -32009; -#[allow(dead_code)] -pub const UNSUPPORTED_IDENTITY_TYPE_CODE: i32 = -32010; -#[allow(dead_code)] -pub const REQUIRE_AUTHENTICATION_CODE: i32 = -32012; - -pub const PUMPX_API_GOOGLE_CODE_VERIFICATION_FAILED_CODE: i32 = -32031; -pub const PUMPX_API_ADD_WALLET_FAILED_CODE: i32 = -32034; -pub const PUMPX_API_CREATE_TRANSFER_TX_FAILED_CODE: i32 = -32038; -pub const PUMPX_API_GET_ACCOUNT_USER_ID_FAILED_CODE: i32 = -32039; -pub const POST_HEIMA_LOGIN_FAILED_CODE: i32 = -32041; - -#[allow(dead_code)] -pub const PUMPX_SIGNER_REQUEST_SIGNATURE_FAILED_CODE: i32 = -32050; -pub const PUMPX_SIGNER_REQUEST_WALLET_FAILED_CODE: i32 = -32051; -pub const PUMPX_SIGNER_PUBKEY_TO_ADDRESS_FAILED_CODE: i32 = -32052; - -#[allow(dead_code)] -pub const INTENT_NONCE_MISMATCH_ERROR_CODE: i32 = -32060; +pub const GAS_ESTIMATION_FAILED_CODE: i32 = -32005; -// Input Validation Error Codes (-32100 to -32119) -pub const INVALID_CHAIN_ID_CODE: i32 = -32100; -pub const INVALID_WALLET_INDEX_CODE: i32 = -32101; -pub const INVALID_ADDRESS_FORMAT_CODE: i32 = -32102; -pub const INVALID_AMOUNT_CODE: i32 = -32103; -pub const INVALID_TOKEN_ADDRESS_CODE: i32 = -32104; -pub const MISSING_REQUIRED_FIELD_CODE: i32 = -32105; -pub const INVALID_HEX_FORMAT_CODE: i32 = -32106; -pub const INVALID_EMAIL_FORMAT_CODE: i32 = -32107; - -// Account/Identity Error Codes (-32120 to -32139) -pub const ACCOUNT_PARSE_ERROR_CODE: i32 = -32121; - -// External Service Error Codes (-32160 to -32179) +// Error when calling external services (-32160 to -32179) pub const SIGNER_SERVICE_ERROR_CODE: i32 = -32160; +pub const PUMPX_SERVICE_ERROR_CODE: i32 = -32161; pub const EMAIL_SERVICE_ERROR_CODE: i32 = -32162; pub const STORAGE_SERVICE_ERROR_CODE: i32 = -32163; pub const EXTERNAL_API_ERROR_CODE: i32 = -32164; - -// Response Processing Error Codes (-32180 to -32199) -pub const UNEXPECTED_RESPONSE_TYPE_CODE: i32 = -32180; - -// Native Task Error Codes (-32200 to -32219) -pub const INVALID_USER_OPERATION_CODE: i32 = -32201; -pub const GAS_ESTIMATION_FAILED_CODE: i32 = -32202; -pub const SIGNATURE_SERVICE_UNAVAILABLE_CODE: i32 = -32203; +pub const WILDMETA_SERVICE_ERROR_CODE: i32 = -32165; diff --git a/tee-worker/omni-executor/rpc-server/src/lib.rs b/tee-worker/omni-executor/rpc-server/src/lib.rs index eafb7438fe..e79e7da4df 100644 --- a/tee-worker/omni-executor/rpc-server/src/lib.rs +++ b/tee-worker/omni-executor/rpc-server/src/lib.rs @@ -1,24 +1,28 @@ #![allow(clippy::result_large_err)] mod auth_token_key_store; -mod auth_utils; +pub use auth_token_key_store::AuthTokenKeyStore; + mod config; mod detailed_error; mod error_code; mod mailer_factory; + mod methods; +pub use methods::*; + mod middlewares; mod oauth2_factory; + mod server; +pub use server::{start_server, RpcContext}; + pub mod utils; -mod validation_helpers; mod verify_auth; -pub use auth_token_key_store::AuthTokenKeyStore; pub use executor_crypto::shielding_key::ShieldingKey; -pub use server::start_server; -// Removed unused hex imports - they may be used in other modules +use jsonrpsee::core::RpcResult; use jsonrpsee::types::ErrorCode; use parity_scale_codec::Decode; use serde::{Deserialize, Serialize}; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/mod.rs b/tee-worker/omni-executor/rpc-server/src/methods/mod.rs index 044a1c4a94..8134f72ae6 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/mod.rs @@ -5,8 +5,6 @@ use jsonrpsee::RpcModule; mod omni; use omni::*; -pub use omni::PumpxRpcError; - pub const PROTECTED_METHODS: [&str; 8] = [ "omni_testProtectedMethod", "omni_addWallet", diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/add_wallet.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/add_wallet.rs index e918ad01df..2bd3335d11 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/add_wallet.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/add_wallet.rs @@ -1,10 +1,6 @@ -use super::common::check_omni_api_response; use crate::{ - detailed_error::DetailedError, - error_code::*, - methods::omni::{common::check_auth, PumpxRpcError}, - server::RpcContext, - utils::omni::to_omni_account, + detailed_error::DetailedError, methods::omni::check_backend_response, server::RpcContext, + utils::omni::extract_omni_account, }; use executor_core::intent_executor::IntentExecutor; use executor_storage::{HeimaJwtStorage, Storage}; @@ -24,52 +20,26 @@ pub fn register_add_wallet().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_closePositionTest, params: {:?}", params); - let omni_account = to_omni_account(¶ms.omni_account).map_err(|_| { - error!("Failed to parse omni account"); - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; + let omni_account = to_omni_account(¶ms.omni_account)?; let smart_wallet = ¶ms.sender; - let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INVALID_CHAIN_ID_CODE, "Chain not supported").with_reason(e), - ) + let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|_| { + DetailedError::internal_error("HyperCore client error").to_rpc_error() })?; hypercore_client.print_account_state(smart_wallet, "Before Close Position").await; // Get current positions let perp_state = - hypercore_client.get_perp_clearinghouse_state(smart_wallet).await.map_err(|e| { - error!("Failed to get perp state: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to query perp state: {}", e)), - ) - })?; + hypercore_client.get_perp_clearinghouse_state(smart_wallet).await.map_err_internal("Failed to get perp state")?; // Filter positions based on ticker parameter let positions_to_close: Vec<_> = perp_state @@ -108,41 +88,15 @@ pub fn register_close_position_test< for pos in positions_to_close { let ticker = &pos.position.coin; - let position_size = pos.position.szi.parse::().map_err(|e| { - error!("Failed to parse position size for {}: {}", ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse position size: {}", e)), - ) - })?; + let position_size: f64 = parse_as(&pos.position.szi, "position_size")?; info!("Closing position for {}: size={}", ticker, position_size); // Get perp market data let (perp_meta, perp_mark_price, perp_mid_price) = - hypercore_client.get_perp_market_prices(ticker).await.map_err(|e| { - error!("Failed to get perp market prices for {}: {}", ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to get perp market prices: {}", e)), - ) - })?; - - let perp_asset_id = get_perp_asset_id(ticker, &perp_meta).map_err(|e| { - error!("Failed to get perp asset ID: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), - ) - })?; - - let perp_asset = perp_meta.universe.get(perp_asset_id as usize).ok_or_else(|| { - error!("Perp asset {} not found in meta", perp_asset_id); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Perp asset {} not found", perp_asset_id)), - ) - })?; - + hypercore_client.get_perp_market_prices(ticker).await.map_err_internal("Failed to get perp market prices")?; + let perp_asset_id = get_perp_asset_id(ticker, &perp_meta).map_err_internal("Failed to get perp asset id")?; + let perp_asset = perp_meta.universe.get(perp_asset_id as usize).ok_or_internal("Perp asset not found in meta")?; let perp_sz_decimals = perp_asset.sz_decimals; // For closing long position (selling), we want to sell at the highest buy price (bid) @@ -158,20 +112,8 @@ pub fn register_close_position_test< ticker, perp_mark_price, perp_mid_price, perp_bid_price, PERP_CLOSE_PRICE_RATIO, target_close_price, clamped_close_price ); - let clamped_close_size_f64 = clamped_close_size.parse::().map_err(|e| { - error!("Failed to parse clamped close size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped close size: {}", e)), - ) - })?; - let clamped_close_price_f64 = clamped_close_price.parse::().map_err(|e| { - error!("Failed to parse clamped close price: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped close price: {}", e)), - ) - })?; + let clamped_close_size_f64: f64 = parse_as(&clamped_close_size, "clamped_close_size")?; + let clamped_close_price_f64: f64 = parse_as(&clamped_close_price, "clamped_close_price")?; let close_size_units = to_price_units(clamped_close_size_f64); let close_price_units = to_price_units(clamped_close_price_f64); @@ -190,7 +132,7 @@ pub fn register_close_position_test< encode_send_raw_action(close_action), ); - let close_tx_hash = submit_corewriter_userop( + let close_tx_hash = submit_corewriter_user_ops( ctx.clone(), &omni_account, &generate_userop(smart_wallet, current_nonce), @@ -214,21 +156,13 @@ pub fn register_close_position_test< 30, OrderWaitCondition::Filled, ) - .await - .map_err(|e| { - error!("Close order did not complete for {}: {}", ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Close order failed: {}", e)), - ) - })?; + .await.map_err_internal("Close order did not complete")?; if !order_filled { - error!("Close order was rejected or canceled for {}", ticker); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Close order was rejected or canceled for {}", ticker)), - )); + let msg = format!("Close order was rejected or canceled for {}", ticker); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error() + ); } info!("Position closed successfully for {}", ticker); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs deleted file mode 100644 index a5c5aa4a4e..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs +++ /dev/null @@ -1,127 +0,0 @@ -use http::Extensions; -use jsonrpsee::types::{ErrorCode, ErrorObjectOwned}; -use parity_scale_codec::Codec; -use pumpx::methods::common::ApiResponse; -use serde::Serialize; - -use crate::{ - detailed_error::DetailedError, error_code::INTERNAL_ERROR_CODE, middlewares::RpcExtensions, -}; -use tracing::error; - -#[derive(Serialize, Debug)] -pub struct PumpxRpcError { - pub code: i32, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -#[derive(Serialize, Debug)] -pub struct PumpxRpcErrorData { - #[serde(skip_serializing_if = "Option::is_none")] - pub backend_response: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub field: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expected: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub received: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub suggestion: Option, -} - -#[derive(Serialize, Debug)] -pub struct PumpxRpcErrorBackendResponse { - pub code: i32, - pub message: String, -} - -impl PumpxRpcError { - pub fn from_code_and_message(code: i32, message: String) -> Self { - Self { code, message, data: None } - } - - pub fn from_error_code(error_code: ErrorCode) -> Self { - Self { code: error_code.code(), message: error_code.message().to_string(), data: None } - } - - pub fn from_api_response(api_response: ApiResponse) -> Self - where - T: Codec, - { - Self { - code: INTERNAL_ERROR_CODE, - message: ErrorCode::InternalError.message().to_string(), - data: Some(PumpxRpcErrorData { - backend_response: Some(PumpxRpcErrorBackendResponse { - code: api_response.code as i32, - message: api_response.message, - }), - field: None, - expected: None, - received: None, - reason: None, - suggestion: None, - }), - } - } -} - -impl From for ErrorObjectOwned { - fn from(error: PumpxRpcError) -> Self { - ErrorObjectOwned::owned(error.code, error.message, error.data) - } -} - -impl From for PumpxRpcError { - fn from(error: DetailedError) -> Self { - Self { - code: error.code, - message: error.message, - data: Some(PumpxRpcErrorData { - backend_response: None, - field: error.details.field, - expected: error.details.expected, - received: error.details.received, - reason: error.details.reason, - suggestion: error.details.suggestion, - }), - } - } -} - -impl From> for PumpxRpcError { - fn from(error: Box) -> Self { - Self::from(*error) - } -} - -// Removed: handle_omni_native_task - all RPC methods now call handlers directly - -pub fn check_omni_api_response( - response: ApiResponse, - name: String, -) -> Result<(), PumpxRpcError> -where - T: Codec, -{ - if response.code != 10000 { - error!("{} failed: code={}, message={}", name, response.code, response.message); - return Err(PumpxRpcError::from_api_response(response)); - } - Ok(()) -} - -/// This is used to verify that the request is authenticated. -/// If the RpcExtensions is not found, it indicates that the request is not authenticated. -/// If the RpcExtensions is found, it contains the sender's omni account extracted from the JWT. -/// Check rpc_middleware.rs -pub fn check_auth(ext: &Extensions) -> Result { - if let Some(rpc_extensions) = ext.get::() { - return Ok(rpc_extensions.sender.clone()); - } - Err(()) -} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/estimate_user_op_gas.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/estimate_user_op_gas.rs index 0c2f3794ac..7ced893840 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/estimate_user_op_gas.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/estimate_user_op_gas.rs @@ -14,14 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use super::common::PumpxRpcError; use crate::detailed_error::DetailedError; -use crate::error_code::PARSE_ERROR_CODE; use crate::server::RpcContext; use crate::utils::gas_estimation::estimate_user_op_gas; use crate::utils::omni::to_omni_account; use crate::utils::user_op::convert_to_packed_user_op; -use crate::validation_helpers::validate_ethereum_address; +use crate::utils::validation::{parse_rpc_params, validate_evm_address}; use alloy::primitives::utils::format_units; use executor_core::intent_executor::IntentExecutor; use executor_core::types::SerializablePackedUserOperation; @@ -94,28 +92,12 @@ pub fn register_estimate_user_op_gas< ) { module .register_async_method("omni_estimateUserOpGas", |params, ctx, _ext| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - crate::error_code::MISSING_REQUIRED_FIELD_CODE, - "Failed to parse request parameters", - ) - .with_reason(format!("Parse error: {}", e)), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_estimateUserOpGas, params: {:?}", params); - let omni_account = to_omni_account(¶ms.omni_account).map_err(|_| { - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; - - validate_ethereum_address(¶ms.user_operation.sender, "user_operation.sender") - .map_err(PumpxRpcError::from)?; + let omni_account = to_omni_account(¶ms.omni_account)?; + validate_evm_address(¶ms.user_operation.sender, "user_operation.sender")?; // Inlined handler logic from handle_estimate_user_op_gas info!( @@ -127,16 +109,14 @@ pub fn register_estimate_user_op_gas< let entry_point_client = ctx.entry_point_clients.get(¶ms.chain_id).ok_or_else(|| { error!("No EntryPoint client configured for chain_id: {}", params.chain_id); - PumpxRpcError::from(DetailedError::chain_not_supported(params.chain_id)) + DetailedError::invalid_chain_id(params.chain_id).to_rpc_error() })?; // Convert SerializablePackedUserOperation to PackedUserOperation let packed_user_op = convert_to_packed_user_op(params.user_operation.clone()).map_err(|e| { error!("Failed to convert UserOperation: {}", e); - PumpxRpcError::from(DetailedError::invalid_user_operation_error( - "Invalid user operation format", - )) + DetailedError::invalid_user_op(&e).to_rpc_error() })?; // Perform gas estimation @@ -183,7 +163,7 @@ pub fn register_estimate_user_op_gas< }, Err(e) => { error!("Gas estimation failed: {}", e); - Err(PumpxRpcError::from(DetailedError::gas_estimation_failed())) + Err(DetailedError::gas_estimation_failed().to_rpc_error()) }, } }) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/export_bundler_private_key.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/export_bundler_private_key.rs index 02af75b172..c92f009eab 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/export_bundler_private_key.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/export_bundler_private_key.rs @@ -1,12 +1,6 @@ use crate::{ - detailed_error::DetailedError, - error_code::{ - AES_KEY_CONVERT_FAILED_CODE, AUTH_VERIFICATION_FAILED_CODE, DECRYPT_REQUEST_FAILED_CODE, - PARSE_ERROR_CODE, - }, - methods::omni::PumpxRpcError, - server::RpcContext, - Deserialize, + detailed_error::DetailedError, error_code::AUTH_VERIFICATION_FAILED_CODE, server::RpcContext, + utils::types::RpcResultExt, utils::validation::parse_rpc_params, Deserialize, RpcResult, }; use alloy::primitives::keccak256; use executor_core::intent_executor::IntentExecutor; @@ -16,7 +10,7 @@ use executor_crypto::{ }; use executor_primitives::utils::hex::decode_hex; use executor_storage::{Storage, WildmetaTimestampStorage}; -use jsonrpsee::RpcModule; +use jsonrpsee::{types::ErrorObjectOwned, RpcModule}; use rsa::Oaep; use sha2::Sha256; use std::sync::Arc; @@ -39,94 +33,73 @@ fn verify_signature( signature: &str, expected_pubkey: &[u8; 33], storage: &Arc, -) -> Result<(), PumpxRpcError> { +) -> RpcResult<()> { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|e| { error!("Failed to get current time: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_reason("System time error"), - ) + DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") + .with_reason("System time error") + .to_rpc_error() })? .as_millis() as u64; if timestamp < current_time.saturating_sub(TIMESTAMP_VALIDITY_WINDOW_MS) { error!("Timestamp too old: {} vs current {}", timestamp, current_time); - return Err(PumpxRpcError::from( - DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") - .with_field("timestamp") - .with_reason(format!( - "Timestamp is too old (must be within last {} minutes)", - TIMESTAMP_VALIDITY_WINDOW_MS / 60000 - )) - .with_suggestion("Use a recent timestamp"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", + ) + .with_field("timestamp") + .with_reason(format!( + "Timestamp is too old (must be within last {} minutes)", + TIMESTAMP_VALIDITY_WINDOW_MS / 60000 + )) + .with_suggestion("Use a recent timestamp") + .to_rpc_error()); } if timestamp > current_time + TIMESTAMP_FUTURE_TOLERANCE_MS { error!("Timestamp too far in future: {} vs current {}", timestamp, current_time); - return Err(PumpxRpcError::from( - DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") - .with_field("timestamp") - .with_reason("Timestamp is too far in the future") - .with_suggestion("Ensure system clock is synchronized"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", + ) + .with_field("timestamp") + .with_reason("Timestamp is too far in the future") + .with_suggestion("Ensure system clock is synchronized") + .to_rpc_error()); } let last_timestamp = storage .get(&BUNDLER_KEY_EXPORT_STORAGE_KEY.to_string()) .map_err(|e| { error!("Failed to get last timestamp from storage: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_reason("Failed to retrieve last timestamp from storage"), - ) + DetailedError::storage_service_error("get timestamp").to_rpc_error() })? .unwrap_or(0); if timestamp <= last_timestamp { error!("Timestamp not greater than last used: {} <= {}", timestamp, last_timestamp); - return Err(PumpxRpcError::from( - DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") - .with_field("timestamp") - .with_reason("Timestamp must be greater than previously used timestamp") - .with_suggestion("This may be a replay attack. Use a fresh timestamp."), - )); - } - let signature_bytes = decode_hex(signature).map_err(|e| { - error!("Failed to decode signature: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_field("signature") - .with_reason("The signature could not be decoded from hex"), + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", ) - })?; - + .with_field("timestamp") + .with_reason("Timestamp must be greater than previously used timestamp") + .with_suggestion("This may be a replay attack. Use a fresh timestamp.") + .to_rpc_error()); + } + let signature_bytes = decode_hex(signature).map_err_parse("Failed to decode signature")?; if signature_bytes.len() != 65 { - error!("Invalid signature length: expected 65 bytes, got {}", signature_bytes.len()); - return Err(PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_field("signature") - .with_reason(format!( - "Invalid signature length: expected 65 bytes, got {}", - signature_bytes.len() - )), - )); + let msg = format!("Invalid signature length, expected 65, got {}", signature_bytes.len()); + error!(msg); + return Err(DetailedError::parse_error(&msg).to_rpc_error()); } - let signature_array: [u8; 65] = signature_bytes.try_into().map_err(|_| { - error!("Failed to convert signature bytes to array"); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error").with_field("signature"), - ) - })?; + let signature_array: [u8; 65] = signature_bytes + .try_into() + .map_err_parse("Failed to convert signature bytes to array")?; let timestamp_bytes = timestamp.to_string(); let challenge_hash = keccak256(timestamp_bytes.as_bytes()); @@ -137,25 +110,21 @@ fn verify_signature( if !ecdsa::Pair::verify_prehashed(&signature, &challenge_hash_array, &public_key) { error!("Signature verification failed for bundler key export"); - return Err(PumpxRpcError::from( - DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") - .with_field("signature") - .with_reason("The signature does not match the challenge") - .with_suggestion("Ensure you are using the correct private key"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", + ) + .with_field("signature") + .with_reason("The signature does not match the challenge") + .with_suggestion("Ensure you are using the correct private key") + .to_rpc_error()); } storage .insert(&BUNDLER_KEY_EXPORT_STORAGE_KEY.to_string(), timestamp) .map_err(|e| { error!("Failed to store new timestamp: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_reason("Failed to store timestamp in storage"), - ) + DetailedError::storage_service_error("insert timestamp").to_rpc_error() })?; debug!("Timestamp {} validated and stored successfully", timestamp); @@ -170,54 +139,23 @@ pub fn register_export_bundler_private_key< ) { module .register_method("omni_exportBundlerPrivateKey", |params, ctx, _ext| { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!( "Received omni_exportBundlerPrivateKey request with timestamp: {}", params.timestamp ); - let key_bytes = decode_hex(¶ms.key).map_err(|e| { - error!("Failed to decode key hex: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_field("key") - .with_reason("The key could not be decoded from hex"), - ) - })?; + let key_bytes = decode_hex(¶ms.key).map_err_parse("Failed to decode key hex")?; let aes_key = ctx .shielding_key .private_key() .decrypt(Oaep::new::(), &key_bytes) - .map_err(|e| { - error!("Failed to decrypt shielded value: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - DECRYPT_REQUEST_FAILED_CODE, - "Shielded value decryption failed", - ) - .with_field("key") - .with_reason("The provided RSA-encrypted AES key could not be decrypted") - .with_suggestion("Ensure the RSA public key matches the encryption key"), - ) - })?; - - let aes_key: Aes256Key = aes_key.try_into().map_err(|_| { - error!("Failed to convert AesKey"); - PumpxRpcError::from( - DetailedError::new(AES_KEY_CONVERT_FAILED_CODE, "AesKey convert failed") - .with_field("key") - .with_reason("The decrypted key is not a valid 256-bit AES key") - .with_suggestion("Ensure the AES key is exactly 32 bytes (256 bits)"), - ) - })?; + .map_err_internal("Failed to decrypt shielded value")?; + + let aes_key: Aes256Key = + aes_key.try_into().map_err_internal("Failed to convert AesKey")?; verify_signature( params.timestamp, @@ -230,7 +168,7 @@ pub fn register_export_bundler_private_key< let encrypted_key: SerdeAesOutput = aes_encrypt_default(&aes_key, &ctx.bundler_private_key).into(); - Ok::(encrypted_key) + Ok::(encrypted_key) }) .expect("Failed to register omni_exportBundlerPrivateKey method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/export_wallet.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/export_wallet.rs index b777ae16bb..9e774f1995 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/export_wallet.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/export_wallet.rs @@ -1,10 +1,6 @@ use crate::{ - detailed_error::DetailedError, - error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE, *}, - methods::omni::{common::check_auth, PumpxRpcError}, - server::RpcContext, - utils::omni::to_omni_account, - Deserialize, + detailed_error::DetailedError, server::RpcContext, utils::omni::extract_omni_account, + utils::types::RpcResultExt, utils::validation::parse_rpc_params, Deserialize, }; use ::pumpx::signer_client::PumpxChainId as _; use ethers::types::Bytes; @@ -34,64 +30,33 @@ pub fn register_export_wallet().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Parse error" - ).with_reason("Invalid JSON format or missing required fields")) - })?; - - debug!("Received omni_exportWallet, chain_id: {}, wallet_index: {}, expected_wallet_address: {}", params.chain_id, params.wallet_index, params.wallet_address); - - let omni_account = to_omni_account(&oa_str).map_err(|_| { - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; + let params = parse_rpc_params::(params)?; + let omni_account = extract_omni_account(&ext)?; let aes_key = ctx .shielding_key .private_key() .decrypt(Oaep::new::(), ¶ms.key) - .map_err(|e| { - error!("Failed to decrypt shielded value: {:?}", e); - PumpxRpcError::from(DetailedError::new( - DECRYPT_REQUEST_FAILED_CODE, - "Shielded value decryption failed" - ).with_field("key").with_reason("The provided RSA-encrypted AES key could not be decrypted").with_suggestion("Ensure the RSA public key matches the encryption key")) - })?; - let aes_key: Aes256Key = aes_key.try_into().map_err(|_| { - error!("Failed to convert AesKey"); - PumpxRpcError::from(DetailedError::new( - AES_KEY_CONVERT_FAILED_CODE, - "AesKey convert failed" - ).with_field("key").with_reason("The decrypted key is not a valid 256-bit AES key").with_suggestion("Ensure the AES key is exactly 32 bytes (256 bits)")) - })?; + .map_err_internal("Failed to decrypt shielded value")?; + + let aes_key: Aes256Key = + aes_key.try_into().map_err_internal("Failed to convert AesKey")?; // Inlined handler logic from handle_pumpx_export_wallet let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); - let Ok(Some(access_token)) = storage.get(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE)) + let Ok(Some(access_token)) = + storage.get(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE)) else { - error!("Failed to get pumpx_{}_jwt_token", AUTH_TOKEN_ACCESS_TYPE); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to get access token"), - )); + error!("Failed to get {}_jwt_token", AUTH_TOKEN_ACCESS_TYPE); + return Err(DetailedError::storage_service_error("get access token").to_rpc_error()); }; // Inline verify_google_code logic debug!("Calling pumpx verify_google_code, code: {}", params.google_code); - let verify_result = ctx.pumpx_api.verify_google_code(&access_token, params.google_code, None).await; + let verify_result = + ctx.pumpx_api.verify_google_code(&access_token, params.google_code, None).await; let verify_success = verify_result.map_or_else( |e| { error!("Google code verification request failed: {:?}", e); @@ -108,25 +73,17 @@ pub fn register_export_wallet().map_err(|e| { - error!("Failed to parse params: {:?}", e); - DetailedError::new(PARSE_ERROR_CODE, "Failed to parse request parameters") - .with_reason(format!("Invalid JSON structure: {}", e)) - .to_error_object() - })?; - debug!("Received omni_getHyperliquidSignatureData, params: {:?}", params); + let params = parse_rpc_params::(params)?; // Make sure `user_id` is non-evm type if matches!(params.user_id, UserId::Evm(_)) { - error!("Invalid user_id type, expected non-Evm"); - return Err(DetailedError::new(INVALID_PARAMS_CODE, "Invalid user ID type") - .with_field("user_id") - .with_expected("Non-EVM user ID (Email, Twitter, Discord, etc.)") - .with_received("EVM type") - .with_suggestion("Use a non-EVM user ID type for this operation") - .to_error_object()); + error!("Invalid user_id type, expected non-evm"); + return Err( + DetailedError::invalid_params("user_id", "expect non-evm").to_rpc_error() + ); } // Unified authentication logic let main_address = if let Some(user_auth) = ¶ms.user_auth { // User authentication provided - let auth = - to_omni_auth(user_auth, ¶ms.user_id, ¶ms.client_id).map_err(|e| { - error!("Failed to convert to OmniAuth: {:?}", e); - DetailedError::new(PARSE_ERROR_CODE, "Failed to convert authentication data") - .with_field("user_auth") - .with_reason(format!("OmniAuth conversion error: {:?}", e)) - .to_error_object() - })?; + let auth = to_omni_auth(user_auth, ¶ms.user_id, ¶ms.client_id) + .map_err_parse("Failed to convert to OmniAuth")?; verify_auth(ctx.clone(), &auth).await.map_err(|e| { error!("Failed to verify user authentication: {:?}", e); - DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication verification failed") - .with_field("user_auth") - .with_reason(format!("Verification error: {:?}", e)) - .with_suggestion("Please check your authentication credentials") - .to_error_object() + DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", + ) + .with_field("user_auth") + .with_reason(format!("Verification error: {:?}", e)) + .with_suggestion("Please check your authentication credentials") + .to_rpc_error() })?; // Get main address from derived wallet - let identity = Identity::try_from(params.user_id.clone()).map_err(|e| { - error!("Failed to convert user ID to identity: {}", e); - DetailedError::new(PARSE_ERROR_CODE, "Failed to parse user identity") - .with_field("user_id") - .with_reason(format!("Identity conversion error: {}", e)) - .to_error_object() - })?; + let identity = Identity::try_from(params.user_id.clone()) + .map_err_parse("Failed to convert user ID to identity")?; let omni_account = identity.to_omni_account(¶ms.client_id); - let derived_pubkey = ctx - .signer_client + ctx.signer_client .request_wallet(ChainType::Evm, 0, *omni_account.as_ref()) .await .map_err(|_| { error!("Failed to derive EVM address"); - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to derive wallet address") - .with_reason("Signer service failed to derive EVM address") - .with_suggestion("Please try again later") - .to_error_object() - })?; - pubkey_to_address(ChainType::Evm, &derived_pubkey).map_err(|_| { - error!("Failed to convert derived pubkey to address"); - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to convert public key") - .with_reason("Public key to address conversion failed") - .to_error_object() - })? + DetailedError::signer_service_error().to_rpc_error() + }) + .and_then(|pk| { + pubkey_to_address(ChainType::Evm, &pk) + .map_err_internal("Failed to convert pubkey to address") + })? } else if let Some(client_auth) = ¶ms.client_auth { // Client authentication provided (WildMeta) match client_auth { @@ -194,24 +173,15 @@ pub fn register_get_hyperliquid_signature_data< verify_wildmeta_signature(agent_address, business_json, signature)?; let business_data: serde_json::Value = serde_json::from_str(business_json) - .map_err(|e| { - error!("Failed to parse business_json: {:?}", e); - DetailedError::new(PARSE_ERROR_CODE, "Failed to parse business JSON") - .with_field("business_json") - .with_reason(format!("JSON parse error: {:?}", e)) - .to_error_object() - })?; + .map_err_parse("Failed to parse business_json")?; let timestamp = business_data .get("timestamp") .and_then(|v| v.as_u64()) .ok_or_else(|| { error!("Missing timestamp in business_json"); - DetailedError::new(MISSING_REQUIRED_FIELD_CODE, "Missing required field in business JSON") - .with_field("timestamp") - .with_expected("Unix timestamp as number") - .with_reason("Business JSON must contain a 'timestamp' field") - .to_error_object() + DetailedError::invalid_params("business_json", "missing timestamp") + .to_rpc_error() })?; verify_payload_timestamp( @@ -225,50 +195,41 @@ pub fn register_get_hyperliquid_signature_data< .verify_hyperliquid_link(agent_address, main_address, *login_type) .await .map_err(|_| { - error!("Failed to verify hyperliquid link"); - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to verify account linkage") - .with_reason("Hyperliquid link verification service error") - .with_suggestion("Please try again later") - .to_error_object() + let msg = "Failed to verify hyperliquid link"; + error!(msg); + DetailedError::wildmeta_service_error("verify_hyperliquid_link") + .to_rpc_error() })?; if !linked { - error!("Agent and main addresses are not linked"); - return Err(DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Account linkage verification failed") - .with_field("agent_address") - .with_expected("Linked to main address") - .with_received("Not linked") - .with_suggestion("Ensure the agent address is linked to your main address in Hyperliquid") - .to_error_object()); + let msg = "Agent and main addresses are not linked"; + error!(msg); + return Err(DetailedError::internal_error(msg).to_rpc_error()); } main_address.clone() }, _ => { error!("Invalid client auth type"); - return Err(DetailedError::new(INVALID_PARAMS_CODE, "Invalid client authentication type") - .with_field("client_auth") - .with_expected("WildmetaHl") - .with_suggestion("Use a supported authentication method") - .to_error_object()); + return Err(DetailedError::invalid_params( + "client_auth", + "expect wildmeta_hl", + ) + .to_rpc_error()); }, } } else { error!("Either user_auth or client_auth must be provided"); - return Err(DetailedError::new(MISSING_REQUIRED_FIELD_CODE, "Missing authentication data") - .with_expected("Either user_auth or client_auth") - .with_suggestion("Provide either user_auth or client_auth for authentication") - .to_error_object()); + return Err(DetailedError::invalid_params( + "auth", + "expect either user_auth or client_auth", + ) + .to_rpc_error()); }; // Derive omni_account for signing (works for both auth methods) - let identity = Identity::try_from(params.user_id.clone()).map_err(|e| { - error!("Failed to convert user ID to identity: {}", e); - DetailedError::new(PARSE_ERROR_CODE, "Failed to parse user identity") - .with_field("user_id") - .with_reason(format!("Identity conversion error: {}", e)) - .to_error_object() - })?; + let identity = Identity::try_from(params.user_id.clone()) + .map_err_parse("Failed to convert user_id to identity")?; let omni_account = identity.to_omni_account(¶ms.client_id); let nonce = Utc::now().timestamp_millis() as u64; @@ -284,8 +245,7 @@ pub fn register_get_hyperliquid_signature_data< let action = ApproveAgent { signature_chain_id: params.chain_id, hyperliquid_chain, - agent_address: validate_ethereum_address(&agent_address, "agent_address") - .map_err(|e| e.to_error_object())?, + agent_address: validate_evm_address(&agent_address, "agent_address")?, agent_name, nonce, }; @@ -294,8 +254,7 @@ pub fn register_get_hyperliquid_signature_data< (HyperliquidAction::ApproveAgent(action), signature) }, HyperliquidActionType::Withdraw3 { amount, destination } => { - let _ = validate_ethereum_address(&destination, "destination") - .map_err(|e| e.to_error_object())?; + let _ = validate_evm_address(&destination, "destination")?; let action = Withdraw3 { signature_chain_id: params.chain_id, hyperliquid_chain, @@ -312,8 +271,7 @@ pub fn register_get_hyperliquid_signature_data< signature_chain_id: params.chain_id, hyperliquid_chain, max_fee_rate, - builder: validate_ethereum_address(&builder, "builder") - .map_err(|e| e.to_error_object())?, + builder: validate_evm_address(&builder, "builder")?, nonce, }; let signature = @@ -328,12 +286,10 @@ pub fn register_get_hyperliquid_signature_data< amount, from_sub_account, } => { - let _ = validate_ethereum_address(&destination, "destination") - .map_err(|e| e.to_error_object())?; + let _ = validate_evm_address(&destination, "destination")?; // Validate from_sub_account if it's not empty if !from_sub_account.is_empty() { - let _ = validate_ethereum_address(&from_sub_account, "from_sub_account") - .map_err(|e| e.to_error_object())?; + let _ = validate_evm_address(&from_sub_account, "from_sub_account")?; } let action = SendAsset { signature_chain_id: params.chain_id, @@ -354,8 +310,7 @@ pub fn register_get_hyperliquid_signature_data< let action = UserDexAbstraction { signature_chain_id: params.chain_id, hyperliquid_chain, - user: validate_ethereum_address(&user, "user") - .map_err(|e| e.to_error_object())?, + user: validate_evm_address(&user, "user")?, enabled, nonce, }; @@ -380,7 +335,7 @@ async fn generate_eip712_signature< ctx: &RpcContext, action: &T, omni_account: &[u8; 32], -) -> Result> { +) -> RpcResult { let message_hash = action.eip712_signing_hash(); let signature_bytes = ctx @@ -388,11 +343,9 @@ async fn generate_eip712_signature< .request_signature(ChainType::Evm, 0, *omni_account, message_hash.to_vec()) .await .map_err(|_| { - error!("Failed to sign message"); - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to generate signature") - .with_reason("Signer service failed to sign EIP-712 message") - .with_suggestion("Please try again later") - .to_error_object() + let msg = "Failed to sign message"; + error!(msg); + DetailedError::signer_service_error().to_rpc_error() })?; Ok(hex_encode(&signature_bytes)) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_next_intent_id.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_next_intent_id.rs index d34385e9f3..8cf150e2db 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_next_intent_id.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_next_intent_id.rs @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . +use crate::detailed_error::DetailedError; use crate::server::RpcContext; +use crate::utils::validation::parse_rpc_params; use crate::ErrorCode; use executor_core::intent_executor::IntentExecutor; use executor_primitives::AccountId; @@ -37,7 +39,7 @@ pub fn register_get_next_intent_id< ) { module .register_async_method("omni_getNextIntentId", |params, ctx, _| async move { - let params = params.parse::()?; + let params = parse_rpc_params::(params)?; let account = AccountId::from_str(¶ms.omni_account).map_err(|e| { error!("Could not parse AccountId: {:?}", e); >::into(ErrorCode::InvalidParams) @@ -48,7 +50,7 @@ pub fn register_get_next_intent_id< .get(&account) .map_err(|e| { error!("Could not get IntentId from store: {:?}", e); - >::into(ErrorCode::InternalError) + DetailedError::storage_service_error("get intent ID").to_rpc_error() })? .unwrap_or_default(); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs index a6fd889bc7..bf0977ca3c 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_oauth2_authorization_data.rs @@ -59,7 +59,7 @@ pub fn register_get_oauth2_authorization_data< .with_field("provider") .with_received(¶ms.provider) .with_expected("google, apple") - .to_error_object()); + .to_rpc_error()); }, }; @@ -76,7 +76,7 @@ pub fn register_get_oauth2_authorization_data< .with_field("client_id") .with_received(¶ms.client_id) .with_reason(format!("Error: {}", e)) - .to_error_object() + .to_rpc_error() })?; let provider_config = oauth2_common::OAuth2ProviderConfig::from_provider(provider); @@ -99,7 +99,11 @@ pub fn register_get_oauth2_authorization_data< nonce: authorize_data.nonce.clone(), }; - storage.insert(&key, verification_data).map_err(|_| ErrorCode::InternalError)?; + storage.insert(&key, verification_data).map_err(|e| { + error!("Failed to store OAuth2 verification data: {:?}", e); + DetailedError::storage_service_error("insert OAuth2 verification data") + .to_rpc_error() + })?; let response_mode = provider_config.use_response_mode.then(|| "form_post".to_string()); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_omni_account.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_omni_account.rs index d6965d8832..89671accfe 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_omni_account.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_omni_account.rs @@ -14,15 +14,13 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use crate::detailed_error::DetailedError; -use crate::error_code::PARSE_ERROR_CODE; -use crate::{methods::omni::common::PumpxRpcError, server::RpcContext}; +use crate::server::RpcContext; +use crate::utils::validation::parse_rpc_params; use executor_core::intent_executor::IntentExecutor; use executor_primitives::{utils::hex::hex_encode, Web2IdentityType}; use heima_primitives::Identity; use jsonrpsee::{types::ErrorObject, RpcModule}; use serde::{Deserialize, Serialize}; -use tracing::error; #[derive(Debug, Deserialize, Serialize)] pub struct GetOmniAccountParams { @@ -38,13 +36,7 @@ pub fn register_get_omni_account< ) { module .register_async_method("omni_getOmniAccount", |params, _, _| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; let account = Identity::from_web2_account(params.user_email.as_str(), Web2IdentityType::Email) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_smart_wallet_root_signer.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_smart_wallet_root_signer.rs index 2b1b575889..f3defc2936 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_smart_wallet_root_signer.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_smart_wallet_root_signer.rs @@ -1,16 +1,13 @@ use crate::{ - detailed_error::DetailedError, - error_code::{PARSE_ERROR_CODE, *}, - methods::omni::PumpxRpcError, - server::RpcContext, - utils::omni::to_omni_account, + detailed_error::DetailedError, server::RpcContext, utils::omni::to_omni_account, + utils::types::RpcResultExt, utils::validation::parse_rpc_params, }; use executor_core::intent_executor::IntentExecutor; -use jsonrpsee::RpcModule; +use jsonrpsee::{types::ErrorObjectOwned, RpcModule}; use pumpx::pubkey_to_address; use serde::Deserialize; use signer_client::ChainType; -use tracing::{debug, error}; +use tracing::debug; // used in rpc with backend only #[derive(Debug, Copy, Deserialize, Clone, PartialEq)] @@ -45,52 +42,23 @@ pub fn register_get_smart_wallet_root_signer< ) { module .register_async_method("omni_getSmartWalletRootSigner", |params, ctx, _| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_getSmartWalletRootSigner, params: {:?}", params); - let omni_account = to_omni_account(¶ms.omni_account).map_err(|_| { - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; + let omni_account = to_omni_account(¶ms.omni_account)?; - let pubkey = ctx + let address = ctx .signer_client .request_wallet(params.chain_type.into(), params.wallet_index, omni_account.into()) .await - .map_err(|_| { - error!("Failed to request wallet from signer client"); - PumpxRpcError::from( - DetailedError::new( - PUMPX_SIGNER_REQUEST_WALLET_FAILED_CODE, - "Failed to request wallet from signer service", - ) - .with_reason("Signer service is temporarily unavailable") - .with_suggestion("Please try again later"), - ) + .map_err(|_| DetailedError::signer_service_error().to_rpc_error()) + .and_then(|pk| { + pubkey_to_address(params.chain_type.into(), &pk) + .map_err_internal("Failed to convert pubkey to address") })?; - let address = pubkey_to_address(params.chain_type.into(), &pubkey).map_err(|_| { - error!("Failed to convert pubkey to address"); - PumpxRpcError::from( - DetailedError::new( - PUMPX_SIGNER_PUBKEY_TO_ADDRESS_FAILED_CODE, - "Failed to convert public key to address", - ) - .with_reason("Public key conversion error") - .with_suggestion("Please check your chain type and try again"), - ) - })?; - - Ok::(address) + Ok::(address) }) .expect("Failed to register omni_addWallet method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_web3_sign_in_message.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_web3_sign_in_message.rs index 33dff8784e..82be379d9f 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_web3_sign_in_message.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_web3_sign_in_message.rs @@ -1,4 +1,6 @@ +use crate::detailed_error::DetailedError; use crate::server::RpcContext; +use crate::utils::validation::parse_rpc_params; use crate::ErrorCode; use executor_core::intent_executor::IntentExecutor; use executor_primitives::utils::hex::hex_encode; @@ -24,7 +26,7 @@ pub fn register_get_web3_sign_in_message< ) { module .register_async_method("omni_getWeb3SignInMessage", |params, ctx, _| async move { - let params = params.parse::()?; + let params = parse_rpc_params::(params)?; let omni_account = AccountId::from_str(¶ms.omni_account).map_err(|_| { error!("Could not parse AccountId: {:?}", params.omni_account); ErrorCode::InvalidParams @@ -35,12 +37,20 @@ pub fn register_get_web3_sign_in_message< Ok(Some(message_code)) => message_code, Ok(None) => { let message_code = generate_otp(8); - verification_code_storage - .insert(&storage_key, message_code.clone()) - .map_err(|_| ErrorCode::InternalError)?; + verification_code_storage.insert(&storage_key, message_code.clone()).map_err( + |e| { + error!("Failed to store verification code: {:?}", e); + DetailedError::storage_service_error("insert verification code") + .to_rpc_error() + }, + )?; message_code }, - Err(_) => return Err(ErrorCode::InternalError.into()), + Err(e) => { + error!("Failed to get verification code from storage: {:?}", e); + return Err(DetailedError::storage_service_error("get verification code") + .to_rpc_error()); + }, }; Ok::(HeimaMessagePayload { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs index b938e4ae45..c3d1dd3ef5 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/login_with_oauth2.rs @@ -1,9 +1,10 @@ use crate::{ detailed_error::DetailedError, - error_code::{AUTH_VERIFICATION_FAILED_CODE, INTERNAL_ERROR_CODE, PARSE_ERROR_CODE}, + error_code::AUTH_VERIFICATION_FAILED_CODE, server::RpcContext, + utils::{types::RpcResultExt, validation::parse_rpc_params}, verify_auth::verify_oauth2_authentication, - Deserialize, ErrorCode, Serialize, + Deserialize, Serialize, }; use chrono::{Days, Utc}; use executor_core::intent_executor::IntentExecutor; @@ -14,7 +15,7 @@ use heima_authentication::{ constants::{AUTH_TOKEN_EXPIRATION_DAYS, AUTH_TOKEN_ID_TYPE}, }; use heima_primitives::Identity; -use jsonrpsee::{types::ErrorObject, RpcModule}; +use jsonrpsee::{core::RpcResult, types::ErrorObject, RpcModule}; use tracing::error; #[derive(Debug, Deserialize, Clone)] @@ -34,13 +35,14 @@ pub struct LoginWithOAuth2Response { pub access_token: String, } -fn parse_oauth2_provider(provider: &str) -> Result { +fn parse_oauth2_provider(provider: &str) -> RpcResult { match provider.to_lowercase().as_str() { "google" => Ok(OAuth2Provider::Google), "apple" => Ok(OAuth2Provider::Apple), _ => { - error!("Unsupported OAuth2 provider: {}", provider); - Err(ErrorCode::InvalidParams) + let msg = format!("Unsupported OAuth2 provider: {}", provider); + error!(msg); + Err(DetailedError::parse_error(&msg).to_rpc_error()) }, } } @@ -75,21 +77,9 @@ pub fn register_login_with_oauth2< ) { module .register_async_method("omni_loginWithOAuth2", |params, ctx, _| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields") - .to_error_object() - })?; + let params = parse_rpc_params::(params)?; - let provider = parse_oauth2_provider(¶ms.provider).map_err(|_e| { - error!("Invalid OAuth2 provider: {}", params.provider); - DetailedError::new(PARSE_ERROR_CODE, "Invalid provider") - .with_field("provider") - .with_received(¶ms.provider) - .with_expected("google, apple") - .to_error_object() - })?; + let provider = parse_oauth2_provider(¶ms.provider)?; let oauth2_data = OAuth2Data { provider, @@ -111,7 +101,7 @@ pub fn register_login_with_oauth2< ) .with_reason(format!("{}", e)) .with_suggestion("Please check your OAuth2 credentials and try again") - .to_error_object() + .to_rpc_error() })?; let access_token = create_jwt_for_user( @@ -120,32 +110,19 @@ pub fn register_login_with_oauth2< ¶ms.client_id, &ctx.jwt_rsa_private_key, ) - .map_err(|_| { - error!("Failed to create access token for user"); - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to create access token") - .with_reason("Internal error while generating JWT token") - .to_error_object() - })?; + .map_err_internal("Failed to create access token for user")?; let user_id = match &verified_identity { Identity::Google(identity_string) | Identity::Apple(identity_string) => { std::str::from_utf8(identity_string.inner_ref()) - .map_err(|_| { - error!("Failed to convert identity to string"); - DetailedError::new(INTERNAL_ERROR_CODE, "Failed to parse identity") - .with_reason("Invalid UTF-8 in identity string") - .to_error_object() - })? + .map_err_internal("Failed to convert identity to string")? .to_string() }, _ => { - error!("Unexpected identity type for OAuth2: {:?}", verified_identity); - return Err(DetailedError::new( - INTERNAL_ERROR_CODE, - "Failed to extract user identity", - ) - .with_reason("OAuth2 provider returned unsupported identity type") - .to_error_object()); + let msg = + format!("Unexpected identity type for OAuth2: {:?}", verified_identity); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); }, }; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs index bf20de58a1..0132f4e08b 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/mod.rs @@ -1,8 +1,10 @@ +use crate::detailed_error::DetailedError; use crate::server::RpcContext; +use crate::RpcResult; use jsonrpsee::RpcModule; - -mod common; -pub use common::PumpxRpcError; +use parity_scale_codec::Codec; +use pumpx::methods::common::ApiResponse; +use tracing::error; mod get_health; use get_health::*; @@ -53,9 +55,6 @@ use get_omni_account::*; mod get_smart_wallet_root_signer; use get_smart_wallet_root_signer::*; -mod submit_user_op; -use submit_user_op::*; - mod estimate_user_op_gas; use estimate_user_op_gas::*; @@ -131,7 +130,6 @@ pub fn register_omni(response: &ApiResponse, op: &str) -> RpcResult<()> { + if response.code != 10000 { + error!("{} failed: code={}, message={}", op, response.code, response.message); + return Err(DetailedError::invalid_backend_response(response, op).to_rpc_error()); + } + Ok(()) +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/notify_limit_order_result.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/notify_limit_order_result.rs index 1ab3b57988..020e026f3d 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/notify_limit_order_result.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/notify_limit_order_result.rs @@ -1,9 +1,6 @@ -use crate::methods::omni::{common::check_auth, PumpxRpcError}; use crate::{ - detailed_error::DetailedError, - error_code::{PARSE_ERROR_CODE, *}, - server::RpcContext, - Deserialize, + detailed_error::DetailedError, server::RpcContext, utils::omni::extract_omni_account, + utils::validation::parse_rpc_params, Deserialize, }; use executor_core::intent_executor::IntentExecutor; use jsonrpsee::RpcModule; @@ -23,37 +20,17 @@ pub fn register_notify_limit_order_result< ) { module .register_async_method("omni_notifyLimitOrderResult", |params, _ctx, ext| async move { - let _user = check_auth(&ext).map_err(|e| { - error!("Authentication check failed: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_suggestion("Please check your authentication credentials"), - ) - })?; + debug!("Received omni_notifyLimitOrderResult, params: {:?}", params); - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; - - debug!( - "Received omni_notifyLimitOrderResult, intent_id: {}, result: {}, message: {:?}", - params.intent_id, params.result, params.message - ); + let params = parse_rpc_params::(params)?; + let _ = extract_omni_account(&ext)?; // Inline handle_pumpx_notify_limit_order_result logic if params.result != "ok" && params.result != "nok" { error!("Invalid result value: {}. Must be 'ok' or 'nok'", params.result); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid input") - .with_reason("Result must be 'ok' or 'nok'"), - )); + return Err( + DetailedError::invalid_params("result", "must be ok or nok").to_rpc_error() + ); } if let Some(msg) = ¶ms.message { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/open_position_test.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/open_position_test.rs index f69be1a101..d978e2419c 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/open_position_test.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/open_position_test.rs @@ -1,16 +1,16 @@ use crate::detailed_error::DetailedError; -use crate::error_code::{INTERNAL_ERROR_CODE, INVALID_CHAIN_ID_CODE, PARSE_ERROR_CODE}; -use crate::methods::omni::PumpxRpcError; use crate::server::RpcContext; use crate::utils::omni::to_omni_account; -use crate::utils::user_op::submit_corewriter_userop; +use crate::utils::types::{RpcOptionExt, RpcResultExt}; +use crate::utils::user_op::submit_corewriter_user_ops; +use crate::utils::validation::{parse_as, parse_rpc_params}; use executor_core::intent_executor::IntentExecutor; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::ChainId; use hyperliquid::*; use jsonrpsee::RpcModule; use serde::{Deserialize, Serialize}; -use tracing::{debug, error, info}; +use tracing::{debug, info}; #[derive(Debug, Deserialize)] pub struct OpenPositionTestParams { @@ -36,78 +36,44 @@ pub fn register_open_position_test< ) { module .register_async_method("omni_openPositionTest", |params, ctx, _ext| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_openPositionTest, params: {:?}", params); - let omni_account = to_omni_account(¶ms.omni_account).map_err(|_| { - error!("Failed to parse omni account"); - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; + let omni_account = to_omni_account(¶ms.omni_account)?; let smart_wallet = ¶ms.sender; let ticker = ¶ms.ticker; - let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INVALID_CHAIN_ID_CODE, "Chain not supported").with_reason(e), - ) + let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|_| { + DetailedError::internal_error("HyperCore client error").to_rpc_error() })?; - let (perp_meta, perp_mark_price, perp_mid_price) = - hypercore_client.get_perp_market_prices(ticker).await.map_err(|e| { - error!("Failed to get perp market prices for {}: {}", ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to get perp market prices: {}", e)), - ) - })?; + let (perp_meta, perp_mark_price, perp_mid_price) = hypercore_client + .get_perp_market_prices(ticker) + .await + .map_err_internal("Failed to get perp market prices")?; - let perp_asset_id = get_perp_asset_id(ticker, &perp_meta).map_err(|e| { - error!("Failed to get perp asset ID: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), - ) - })?; + let perp_asset_id = get_perp_asset_id(ticker, &perp_meta) + .map_err_internal("Failed to get perp asset id")?; - let perp_asset = perp_meta.universe.get(perp_asset_id as usize).ok_or_else(|| { - error!("Perp asset {} not found in meta", perp_asset_id); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Perp asset {} not found", perp_asset_id)), - ) - })?; + let perp_asset = perp_meta + .universe + .get(perp_asset_id as usize) + .ok_or_internal("Perp asset not found in meta")?; let perp_sz_decimals = perp_asset.sz_decimals; let (_, perp_ask_price) = get_bid_ask_prices(perp_mark_price, perp_mid_price); let target_hedge_price = perp_ask_price * PERP_ENTRY_PRICE_RATIO; - let hedge_size = params.position_size.parse::().unwrap(); + let hedge_size: f64 = parse_as(¶ms.position_size, "position_size")?; let clamped_hedge_size = clamp_size(hedge_size, perp_sz_decimals); let clamped_hedge_price = clamp_price(target_hedge_price, perp_sz_decimals, false); - let clamped_hedge_size_f64 = clamped_hedge_size.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped hedge size: {}", e)), - ) - })?; - let clamped_hedge_price_f64 = clamped_hedge_price.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped hedge price: {}", e)), - ) - })?; + let clamped_hedge_size_f64: f64 = parse_as(&clamped_hedge_size, "clamped_hedge_size")?; + let clamped_hedge_price_f64: f64 = + parse_as(&clamped_hedge_price, "clamped_hedge_price")?; let cloid = generate_cloid(); @@ -118,7 +84,7 @@ pub fn register_open_position_test< cloid, ); - let hedge_open_tx_hash = submit_corewriter_userop( + let hedge_open_tx_hash = submit_corewriter_user_ops( ctx.clone(), &omni_account, &generate_userop(smart_wallet, params.nonce), @@ -140,17 +106,13 @@ pub fn register_open_position_test< .wait_for_order(smart_wallet, &cloid.to_string(), 20, OrderWaitCondition::Opened) .await .map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Hedge order failed to open: {}", e)), - ) + DetailedError::internal_error(&format!("Hedge order failed to open: {}", e)) + .to_rpc_error() })?; if !order_opened { - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Hedge order was rejected or canceled"), - )); + return Err(DetailedError::internal_error("Hedge order was rejected or canceled") + .to_rpc_error()); } hypercore_client.print_account_state(smart_wallet, "After Open Position").await; diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/payback_loan_test.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/payback_loan_test.rs index e8b31fc82f..9680cf8fe8 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/payback_loan_test.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/payback_loan_test.rs @@ -1,9 +1,10 @@ use crate::detailed_error::DetailedError; -use crate::error_code::{INTERNAL_ERROR_CODE, INVALID_CHAIN_ID_CODE, PARSE_ERROR_CODE}; -use crate::methods::omni::PumpxRpcError; use crate::server::RpcContext; use crate::utils::omni::to_omni_account; -use crate::utils::user_op::submit_corewriter_userop; +use crate::utils::types::{RpcOptionExt, RpcResultExt}; +use crate::utils::user_op::submit_corewriter_user_ops; +use crate::utils::validation::{parse_as, parse_rpc_params}; +use crate::RpcResult; use executor_core::intent_executor::IntentExecutor; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::AccountId; @@ -81,13 +82,7 @@ pub fn register_payback_loan_test< ) { module .register_async_method("omni_paybackLoanTest", |params, ctx, _ext| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_paybackLoanTest, params: {:?}", params); @@ -101,12 +96,8 @@ pub fn register_payback_loan_test< nonce: loan_nonce, }; - let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|e| { - error!("Failed to create HyperCore client: {}", e); - PumpxRpcError::from( - DetailedError::new(INVALID_CHAIN_ID_CODE, "Chain not supported") - .with_reason(format!("Chain ID {} is not supported", params.chain_id)), - ) + let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|_| { + DetailedError::internal_error("HyperCore client error").to_rpc_error() })?; hypercore_client.print_account_state(smart_wallet, "Before Payback").await; @@ -115,42 +106,15 @@ pub fn register_payback_loan_test< let loan_record = ctx .loan_record_storage .get(&storage_key) - .map_err(|_| { - error!("Failed to retrieve loan record from storage"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to retrieve loan record from storage"), - ) - })? - .ok_or_else(|| { - error!("Loan record not found for nonce {}", loan_nonce); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Loan record not found") - .with_reason(format!("No loan record found for nonce {}", loan_nonce)), - ) - })?; + .map_err_internal("Failed to retrieve loan record from storage")? + .ok_or_internal("Loan record not found")?; info!("Retrieved loan record: {:?}", loan_record); let collateral_ticker = loan_record.collateral_ticker.to_uppercase(); - let collateral_size = loan_record.collateral_size.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason(format!("Invalid collateral_size: {}", e)), - ) - })?; - let usdc_sold = loan_record.usdc_sold.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason(format!("Invalid usdc_sold: {}", e)), - ) - })?; - let usdc_loaned = loan_record.usdc_loaned.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason(format!("Invalid usdc_loaned: {}", e)), - ) - })?; + let collateral_size: f64 = parse_as(&loan_record.collateral_size, "collateral_size")?; + let usdc_sold: f64 = parse_as(&loan_record.usdc_sold, "usdc_sold")?; + let usdc_loaned: f64 = parse_as(&loan_record.usdc_loaned, "usdc_loaned")?; let exec_ctx = ExecutionContext { ctx: ctx.clone(), @@ -168,24 +132,13 @@ pub fn register_payback_loan_test< LoanState::HedgeOpened => { info!("Starting payback from HedgeOpened state"); - let hedge_open_cloid_str = loan_record + let hedge_open_cloid = loan_record .cloids .iter() .find(|(name, _)| name == "hedge_open") .map(|(_, cloid)| cloid.clone()) - .ok_or_else(|| { - error!("hedge_open cloid not found in loan record"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason("hedge_open cloid not found in loan_record"), - ) - })?; - let hedge_open_cloid = hedge_open_cloid_str.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason(format!("Invalid hedge_open_cloid: {}", e)), - ) - })?; + .ok_or_internal("hedge_open_cloid not found in loan record") + .and_then(|s| parse_as(&s, "hedge_open_cloid"))?; let close_ctx = precheck_close_hedge( &ctx, @@ -287,30 +240,21 @@ pub fn register_payback_loan_test< }) }, LoanState::SpotBought => { - error!("Payback already completed"); - Err(PumpxRpcError::from(DetailedError::new( - INTERNAL_ERROR_CODE, - "Payback already completed", - ))) + let msg = "Payback already completed".to_string(); + error!(msg); + Err(DetailedError::internal_error(&msg).to_rpc_error()) }, } }) .expect("Failed to register omni_paybackLoanTest method"); } -fn precheck_params(params: &PaybackLoanTestParams) -> Result<(AccountId, u64, f64), PumpxRpcError> { - let omni_account = to_omni_account(¶ms.omni_account).map_err(|_| { - PumpxRpcError::from(DetailedError::new(PARSE_ERROR_CODE, "Invalid omni account")) - })?; - - let min_expected_account_value_f64 = - params.min_expected_account_value.parse::().map_err(|e| { - error!("Failed to parse min_expected_account_value: {}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Invalid parameter") - .with_reason(format!("Invalid min_expected_account_value value: {}", e)), - ) - })?; +fn precheck_params(params: &PaybackLoanTestParams) -> RpcResult<(AccountId, u64, f64)> { + let omni_account = to_omni_account(¶ms.omni_account) + .map_err(|_| DetailedError::internal_error("Invalid omni account").to_rpc_error())?; + + let min_expected_account_value_f64: f64 = + parse_as(¶ms.min_expected_account_value, "min_expected_account_value")?; info!("Minimum expected account value threshold: {} USDC", min_expected_account_value_f64); @@ -336,99 +280,37 @@ async fn verify_hedge_position( hypercore_client: &HyperCoreClient, smart_wallet_address: &str, collateral_ticker: &str, -) -> Result<(f64, f64, f64, f64, f64, f64, f64), PumpxRpcError> { +) -> RpcResult<(f64, f64, f64, f64, f64, f64, f64)> { let perp_state = hypercore_client .get_perp_clearinghouse_state(smart_wallet_address) .await - .map_err(|e| { - error!("Failed to get perp state: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to query perp state: {}", e)), - ) - })?; + .map_err_internal("Failed to get perp state")?; let hedge_position = perp_state .asset_positions .iter() .find(|pos| pos.position.coin.eq_ignore_ascii_case(collateral_ticker)) - .ok_or_else(|| { - error!("Hedge position for {} not found", collateral_ticker); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Position not found").with_reason(format!( - "Position for {} not found or liquidated", - collateral_ticker - )), - ) - })?; + .ok_or_internal("Hedge position not found")?; - let position_size = hedge_position.position.szi.parse::().map_err(|e| { - error!("Failed to parse position size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid position size: {}", e)), - ) - })?; + let position_size: f64 = parse_as(&hedge_position.position.szi, "position_size")?; if position_size <= 0.0 { - error!("Position liquidated (size: {})", position_size); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Position liquidated") - .with_reason(format!("Position for {} has been liquidated", collateral_ticker)), - )); + let msg = format!("Position liquidated (size: {})", position_size); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); } // Get account value from crossMarginSummary - let account_value = - perp_state.cross_margin_summary.account_value.parse::().map_err(|e| { - error!("Failed to parse account value: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid account value: {}", e)), - ) - })?; + let account_value: f64 = + parse_as(&perp_state.cross_margin_summary.account_value, "account_value")?; // Parse additional position data - let unrealized_pnl = hedge_position.position.unrealized_pnl.parse::().map_err(|e| { - error!("Failed to parse unrealized PnL: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid unrealized PnL: {}", e)), - ) - })?; - - let cum_funding_all_time = - hedge_position.position.cum_funding.all_time.parse::().map_err(|e| { - error!("Failed to parse cumulative funding: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid cumulative funding: {}", e)), - ) - })?; - - let margin_used = hedge_position.position.margin_used.parse::().map_err(|e| { - error!("Failed to parse margin used: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid margin used: {}", e)), - ) - })?; - - let position_value = hedge_position.position.position_value.parse::().map_err(|e| { - error!("Failed to parse position value: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid position value: {}", e)), - ) - })?; - - let withdrawable = perp_state.withdrawable.parse::().map_err(|e| { - error!("Failed to parse withdrawable: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid withdrawable: {}", e)), - ) - })?; + let unrealized_pnl: f64 = parse_as(&hedge_position.position.unrealized_pnl, "unrealized_pnl")?; + let cum_funding_all_time: f64 = + parse_as(&hedge_position.position.cum_funding.all_time, "cum_funding_all_time")?; + let margin_used: f64 = parse_as(&hedge_position.position.margin_used, "margin_used")?; + let position_value: f64 = parse_as(&hedge_position.position.position_value, "position_value")?; + let withdrawable: f64 = parse_as(&perp_state.withdrawable, "withdrawable")?; info!( "Position data for {}: size={}, account_value={}, unrealized_pnl={}, cum_funding_all_time={}, margin_used={}, position_value={}, withdrawable={}", @@ -454,88 +336,53 @@ async fn precheck_close_hedge Result { +) -> RpcResult { // Validate loan state is HedgeOpened if loan_record.state != LoanState::HedgeOpened { - error!("Invalid loan state for payback: expected HedgeOpened, got {:?}", loan_record.state); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid loan state").with_reason(format!( - "Loan must be in HedgeOpened state for payback, current state: {:?}", - loan_record.state - )), - )); + let msg = format!( + "Invalid loan state for payback: expected HedgeOpened, got {:?}", + loan_record.state + ); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); } - let usdc_loaned = loan_record.usdc_loaned.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason(format!("Invalid usdc_loaned: {}", e)), - ) - })?; + let usdc_loaned: f64 = parse_as(&loan_record.usdc_loaned, "usdc_loaned")?; - let loan_position_size = loan_record.position_size.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid stored data") - .with_reason(format!("Invalid position_size: {}", e)), - ) - })?; + let loan_position_size: f64 = parse_as(&loan_record.position_size, "loan_position_size")?; info!("Loan record position size (to be closed): {}", loan_position_size); // Check USDC balance in spot account info!("Checking USDC balance in spot account..."); - let usdc_balance = - hypercore_client.get_spot_balance(smart_wallet, "USDC").await.map_err(|e| { - error!("Failed to get USDC balance: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to query USDC balance: {}", e)), - ) - })?; + let usdc_balance = hypercore_client + .get_spot_balance(smart_wallet, "USDC") + .await + .map_err_internal("Failed to get USDC balance")?; info!("User USDC balance: {}, loan amount: {}", usdc_balance, usdc_loaned); if usdc_balance < usdc_loaned { - error!( - "Insufficient USDC balance: user has {} but needs {} to pay back loan", - usdc_balance, usdc_loaned - ); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Insufficient USDC balance").with_reason( - format!( - "User has {} USDC but needs {} USDC to pay back the loan", - usdc_balance, usdc_loaned - ), - ), - )); + let msg = format!("Insufficient USDC balance: {} < {}", usdc_balance, usdc_loaned); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); } info!("✓ USDC balance check passed: user has sufficient USDC"); // Get perp market data - let (perp_meta, perp_mark_price, perp_mid_price) = - hypercore_client.get_perp_market_prices(collateral_ticker).await.map_err(|e| { - error!("Failed to get perp market prices for {}: {}", collateral_ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to get perp market prices: {}", e)), - ) - })?; + let (perp_meta, perp_mark_price, perp_mid_price) = hypercore_client + .get_perp_market_prices(collateral_ticker) + .await + .map_err_internal("Failed to get perp market prices")?; - let perp_asset_id = get_perp_asset_id(collateral_ticker, &perp_meta).map_err(|e| { - error!("Failed to get perp asset ID: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), - ) - })?; + let perp_asset_id = get_perp_asset_id(collateral_ticker, &perp_meta) + .map_err_internal("Failed to get perp asset ID")?; - let perp_asset = perp_meta.universe.get(perp_asset_id as usize).ok_or_else(|| { - error!("Perp asset {} not found in meta", perp_asset_id); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Perp asset {} not found", perp_asset_id)), - ) - })?; + let perp_asset = perp_meta + .universe + .get(perp_asset_id as usize) + .ok_or_internal("Perp asset not found in meta")?; let perp_sz_decimals = perp_asset.sz_decimals; @@ -545,14 +392,7 @@ async fn precheck_close_hedge = None; @@ -591,17 +431,12 @@ async fn precheck_close_hedge= loan record {}", @@ -609,18 +444,12 @@ async fn precheck_close_hedge= {} USDC", @@ -668,18 +497,12 @@ async fn precheck_close_hedge= {} USDC", @@ -698,47 +521,31 @@ async fn precheck_close_hedge { - error!("Order already in terminal state: {}", status); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Order not active") - .with_reason(format!("Order is already {}", status)), - )); + let msg = format!("Order already in terminal state: {}", status); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); }, _ => { - error!("Unexpected order status: {}", status); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Unexpected order status") - .with_reason(format!("Unknown status: {}", status)), - )); + let msg = format!("Unexpected order status: {}", status); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); }, } } else { - error!("Order not found for cloid {}", hedge_open_cloid); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Order not found") - .with_reason(format!("No order found with cloid {}", hedge_open_cloid)), - )); + let msg = format!("Order not found for cloid {}", hedge_open_cloid); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); }; // Validate notional value if closing position if should_close && position_size_to_close > 0.0 { let clamped_size = clamp_size(position_size_to_close, perp_sz_decimals); - let clamped_size_f64 = clamped_size.parse::().map_err(|e| { - error!("Failed to parse clamped close size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped close size: {}", e)), - ) - })?; + let clamped_size_f64: f64 = parse_as(&clamped_size, "clamped_close_size")?; // For closing long position (selling), we want to sell at the highest buy price (bid) let (perp_bid_price, _) = get_bid_ask_prices(perp_mark_price, perp_mid_price); - validate_notional_value(perp_bid_price, clamped_size_f64, "Perp close").map_err(|e| { - error!("{}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Notional value too low").with_reason(e), - ) - })?; + validate_notional_value(perp_bid_price, clamped_size_f64, "Perp close") + .map_err_internal("Notional value too low")?; } info!("✓ Close position precheck passed"); @@ -760,25 +567,15 @@ async fn precheck_move_to_spot( usdc_sold: f64, usdc_loaned: f64, position_data: &Option<(f64, f64, f64, f64, f64)>, -) -> Result { +) -> RpcResult { let initial_margin = usdc_sold - usdc_loaned; - let perp_state = - hypercore_client.get_perp_clearinghouse_state(smart_wallet).await.map_err(|e| { - error!("Failed to get perp state: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to query perp state: {}", e)), - ) - })?; + let perp_state = hypercore_client + .get_perp_clearinghouse_state(smart_wallet) + .await + .map_err_internal("Failed to get perp state")?; - let withdrawable_usdc = perp_state.withdrawable.parse::().map_err(|e| { - error!("Failed to parse withdrawable USDC: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse withdrawable USDC: {}", e)), - ) - })?; + let withdrawable_usdc: f64 = parse_as(&perp_state.withdrawable, "withdrawable_usdc")?; info!("Current withdrawable USDC from perp: {}", withdrawable_usdc); @@ -797,18 +594,12 @@ async fn precheck_move_to_spot( let max_transferable = stored_withdrawable + margin_used; if calculated_amount > max_transferable { - error!( + let msg = format!( "Calculated transfer amount ({}) exceeds max transferable ({} = {} withdrawable + {} margin_used)", calculated_amount, max_transferable, stored_withdrawable, margin_used ); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid transfer amount").with_reason( - format!( - "Calculated transfer amount ({}) exceeds available funds ({})", - calculated_amount, max_transferable - ), - ), - )); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); } info!( @@ -831,55 +622,31 @@ async fn precheck_buy_spot( hypercore_client: &HyperCoreClient, collateral_ticker: &str, collateral_size: f64, -) -> Result { - let (spot_meta, spot_mark_price, spot_mid_price) = - hypercore_client.get_spot_market_prices(collateral_ticker).await.map_err(|e| { - error!("Failed to get spot market prices for {}: {}", collateral_ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to get spot market prices: {}", e)), - ) - })?; +) -> RpcResult { + let (spot_meta, spot_mark_price, spot_mid_price) = hypercore_client + .get_spot_market_prices(collateral_ticker) + .await + .map_err_internal("Failed to get spot market prices")?; - let spot_asset_id = get_spot_asset_id(collateral_ticker, &spot_meta).map_err(|e| { - error!("Failed to get spot asset ID: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), - ) - })?; + let spot_asset_id = get_spot_asset_id(collateral_ticker, &spot_meta) + .map_err_internal("Failed to get spot asset id")?; let spot_token = spot_meta .tokens .iter() .find(|t| t.name.eq_ignore_ascii_case(collateral_ticker)) - .ok_or_else(|| { - error!("Token {} not found in spot meta", collateral_ticker); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Token {} not found in spot meta", collateral_ticker)), - ) - })?; + .ok_or_internal("Token not found in spot meta")?; let spot_sz_decimals = spot_token.sz_decimals; // Validate notional value with clamped size let clamped_size = clamp_size(collateral_size, spot_sz_decimals); - let clamped_size_f64 = clamped_size.parse::().map_err(|e| { - error!("Failed to parse clamped buy size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped buy size: {}", e)), - ) - })?; + let clamped_size_f64: f64 = parse_as(&clamped_size, "clamped_buy_size")?; // For buying, we want to buy at the lowest sell price (ask) let (_, spot_ask_price) = get_bid_ask_prices(spot_mark_price, spot_mid_price); - validate_notional_value(spot_ask_price, clamped_size_f64, "Spot buy").map_err(|e| { - error!("{}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Notional value too low").with_reason(e), - ) - })?; + validate_notional_value(spot_ask_price, clamped_size_f64, "Spot buy") + .map_err_internal("Notional value too low")?; info!("✓ Buy spot precheck passed"); @@ -891,7 +658,7 @@ async fn do_close_hedge Result<(Option, Option, Option), PumpxRpcError> { +) -> RpcResult<(Option, Option, Option)> { let mut hedge_cancel_tx_hash: Option = None; let mut hedge_close_cloid_opt: Option = None; let mut hedge_close_tx_hash: Option = None; @@ -906,7 +673,7 @@ async fn do_close_hedge().map_err(|e| { - error!("Failed to parse clamped close size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped close size: {}", e)), - ) - })?; - let clamped_close_price_f64 = clamped_close_price.parse::().map_err(|e| { - error!("Failed to parse clamped close price: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped close price: {}", e)), - ) - })?; + let clamped_close_size_f64: f64 = parse_as(&clamped_close_size, "clamped_close_size")?; + let clamped_close_price_f64: f64 = parse_as(&clamped_close_price, "clamped_close_price")?; let close_size_units = to_price_units(clamped_close_size_f64); let close_price_units = to_price_units(clamped_close_price_f64); @@ -1000,7 +747,7 @@ async fn do_close_hedge, move_ctx: &MoveToSpotContext, current_nonce: &mut u128, -) -> Result, PumpxRpcError> { +) -> RpcResult> { info!("Action: Transferring USDC from perp to spot..."); // Get initial spot balance BEFORE submitting the transfer @@ -1079,35 +818,17 @@ async fn do_move_to_spot().map_err(|e| { - error!("Failed to parse withdrawable USDC: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse withdrawable USDC: {}", e)), - ) - })?; + .map_err_internal("Failed to get perp state")?; + let withdrawable_usdc: f64 = parse_as(&perp_state.withdrawable, "withdrawable_usdc")?; let final_amount = move_ctx.transfer_amount.min(withdrawable_usdc); + info!( "Calculated transfer_amount={}, withdrawable_usdc={}, final_amount={}", move_ctx.transfer_amount, withdrawable_usdc, final_amount @@ -1120,7 +841,7 @@ async fn do_move_to_spot Result<(u128, Option), PumpxRpcError> { +) -> RpcResult<(u128, Option)> { info!("Action: Buying back collateral in spot market..."); // Get current USDC balance to determine how much collateral we can afford @@ -1181,13 +897,7 @@ async fn do_buy_spot().map_err(|e| { - error!("Failed to parse clamped buy price: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped buy price: {}", e)), - ) - })?; + let clamped_buy_price_f64: f64 = parse_as(&clamped_buy_price, "clamped_buy_price")?; let affordable_collateral = current_spot_usdc / clamped_buy_price_f64; let actual_buy_size = collateral_size.min(affordable_collateral); @@ -1220,13 +924,7 @@ async fn do_buy_spot().map_err(|e| { - error!("Failed to parse clamped buy size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped buy size: {}", e)), - ) - })?; + let clamped_buy_size_f64: f64 = parse_as(&clamped_buy_size, "clamped_buy_size")?; let buy_size_units = to_price_units(clamped_buy_size_f64); let buy_price_units = to_price_units(clamped_buy_price_f64); @@ -1242,7 +940,7 @@ async fn do_buy_spot().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_queryLoanTest, params: {:?}", params); - let address_bytes = - hex::decode(params.omni_account.strip_prefix("0x").unwrap_or(¶ms.omni_account)) - .map_err(|_| { - error!("Failed to decode omni account hex string"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to decode omni account hex string"), - ) - })?; - - if address_bytes.len() != 32 { - error!( - "Invalid omni account length: expected 32 bytes, got {}", - address_bytes.len() - ); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(format!( - "Invalid omni account length: expected 32 bytes, got {}", - address_bytes.len() - )), - )); - } - - let omni_account = AccountId::decode(&mut &address_bytes[..]).map_err(|_| { - error!("Failed to decode AccountId from bytes"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to decode AccountId from bytes"), - ) - })?; + let omni_account = to_omni_account(¶ms.omni_account)?; - // Query records from storage let record_list = ctx.loan_record_storage.query_records(&omni_account, params.nonce); - // Convert to HashMap keyed by nonce (as string) let mut records = HashMap::new(); for (nonce, record) in record_list { records.insert(nonce.to_string(), record); } - Ok(QueryLoanTestResponse { records }) + Ok::(QueryLoanTestResponse { records }) }) .expect("Failed to register omni_queryLoanTest method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_email_verification_code.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_email_verification_code.rs index 445fff4eb2..a15031c185 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_email_verification_code.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_email_verification_code.rs @@ -1,6 +1,8 @@ use crate::{ - detailed_error::DetailedError, error_code::PARSE_ERROR_CODE, server::RpcContext, - validation_helpers::validate_email, Deserialize, + detailed_error::DetailedError, + server::RpcContext, + utils::validation::{parse_rpc_params, validate_email}, + Deserialize, }; use executor_core::intent_executor::IntentExecutor; use executor_primitives::{Hashable, Identity, Web2IdentityType}; @@ -9,7 +11,7 @@ use heima_identity_verification::web2::email::{ generate_verification_code, send_verification_email, send_wildmeta_verification_email, }; use jsonrpsee::{types::ErrorObject, RpcModule}; -use tracing::{error, info}; +use tracing::{debug, error}; #[derive(Debug, Deserialize)] pub struct RequestEmailVerificationCodeParams { @@ -24,19 +26,10 @@ pub fn register_request_email_verification_code< ) { module .register_async_method("omni_requestEmailVerificationCode", |params, ctx, _| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - DetailedError::new(PARSE_ERROR_CODE, "Failed to parse request parameters") - .with_reason(format!("Invalid JSON structure: {}", e)) - .to_error_object() - })?; + debug!("[EMAIL_LIFECYCLE] Received omni_requestEmailVerificationCode, params: {:?}", params); - info!("[EMAIL_LIFECYCLE] Received omni_requestEmailVerificationCode, client_id: {}, user_email: {}", params.client_id, params.user_email); - - validate_email(¶ms.user_email).map_err(|e| { - error!("[EMAIL_LIFECYCLE] Email validation failed for {}: {:?}", params.user_email, e); - e.to_error_object() - })?; + let params = parse_rpc_params::(params)?; + validate_email(¶ms.user_email)?; let email_identity = Identity::from_web2_account(¶ms.user_email, Web2IdentityType::Email); @@ -49,7 +42,7 @@ pub fn register_request_email_verification_code< .insert(&omni_account.hash(), verification_code.clone()) .map_err(|e| { error!("[EMAIL_LIFECYCLE] Failed to store verification code for {}: {:?}", params.user_email, e); - DetailedError::storage_error("insert verification code").to_error_object() + DetailedError::storage_service_error("insert verification code").to_rpc_error() })?; // Get the appropriate mailer for this client @@ -63,7 +56,7 @@ pub fn register_request_email_verification_code< .with_field("client_id") .with_received(¶ms.client_id) .with_reason(format!("Error: {}", e)) - .to_error_object() + .to_rpc_error() })?; // Use Wildmeta template for wildmeta client @@ -76,14 +69,14 @@ pub fn register_request_email_verification_code< .await .map_err(|e| { error!("[EMAIL_LIFECYCLE] Failed to send Wildmeta verification email to {} (client: {}): {:?}", params.user_email, params.client_id, e); - DetailedError::email_service_error(¶ms.user_email).to_error_object() + DetailedError::email_service_error(¶ms.user_email).to_rpc_error() })?; } else { send_verification_email(&*mailer, params.user_email.clone(), verification_code.clone()) .await .map_err(|e| { error!("[EMAIL_LIFECYCLE] Failed to send verification email to {} (client: {}): {:?}", params.user_email, params.client_id, e); - DetailedError::email_service_error(¶ms.user_email).to_error_object() + DetailedError::email_service_error(¶ms.user_email).to_rpc_error() })?; } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_jwt.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_jwt.rs index 265ff0af42..40191c8e81 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_jwt.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_jwt.rs @@ -1,9 +1,10 @@ -use super::common::check_omni_api_response; +use super::check_backend_response; use crate::{ detailed_error::DetailedError, - error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE, *}, - methods::omni::PumpxRpcError, + error_code::*, server::RpcContext, + utils::types::{RpcOptionExt, RpcResultExt}, + utils::validation::parse_rpc_params, verify_auth::verify_auth, Deserialize, }; @@ -50,13 +51,7 @@ pub fn register_request_jwt().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!( "Received omni_requestJwt, user_email: {}, client_id: {}", @@ -67,13 +62,12 @@ pub fn register_request_jwt().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_requestLoanTest, params: {:?}", params); @@ -102,10 +96,8 @@ pub fn register_request_loan_test< nonce: params.loan_nonce.unwrap_or(params.user_operation.nonce as u64), }; - let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INVALID_CHAIN_ID_CODE, "Chain not supported").with_reason(e), - ) + let hypercore_client = HyperCoreClient::new(params.chain_id).map_err(|_| { + DetailedError::internal_error("HyperCore client error").to_rpc_error() })?; let exec_ctx = ExecutionContext { @@ -184,16 +176,11 @@ pub fn register_request_loan_test< if existing.collateral_ticker != collateral_ticker || existing.collateral_size != format!("{}", collateral_size) { - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Loan parameters mismatch") - .with_reason(format!( - "Existing: {}@{}; Requested: {}@{}", - existing.collateral_ticker, - existing.collateral_size, - collateral_ticker, - collateral_size - )), - )); + return Err(DetailedError::invalid_params( + "collateral", + "mismatch collateral ticker or size", + ) + .to_rpc_error()); } match existing.state { @@ -221,16 +208,8 @@ pub fn register_request_loan_test< LoanState::SpotSold => { info!("Resuming from SpotSold"); - let usdc_for_perp = - existing.usdc_for_perp.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new( - INTERNAL_ERROR_CODE, - "Invalid stored data", - ) - .with_reason(format!("Invalid usdc_for_perp: {}", e)), - ) - })?; + let usdc_for_perp: f64 = + parse_as(&existing.usdc_for_perp, "usdc_for_perp")?; let open_ctx = precheck_open_hedge( &hypercore_client, @@ -271,16 +250,8 @@ pub fn register_request_loan_test< LoanState::ToPerpMoved => { info!("Resuming from ToPerpMoved"); - let usdc_for_perp = - existing.usdc_for_perp.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new( - INTERNAL_ERROR_CODE, - "Invalid stored data", - ) - .with_reason(format!("Invalid usdc_for_perp: {}", e)), - ) - })?; + let usdc_for_perp: f64 = + parse_as(&existing.usdc_for_perp, "usdc_for_perp")?; let open_ctx = precheck_open_hedge( &hypercore_client, @@ -314,13 +285,11 @@ pub fn register_request_loan_test< hedge_open_tx_hash, }) }, - _ => Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Invalid loan state") - .with_reason(format!( - "Cannot resume from state: {:?}", - existing.state - )), - )), + _ => Err(DetailedError::internal_error(&format!( + "Unexpected state: {:?}", + existing.state + )) + .to_rpc_error()), } }, } @@ -328,52 +297,25 @@ pub fn register_request_loan_test< .expect("Failed to register omni_requestLoanTest method"); } -fn precheck_params( - params: &RequestLoanTestParams, -) -> Result<(AccountId, String, f64, f64), PumpxRpcError> { - let omni_account = to_omni_account(¶ms.omni_account).map_err(|_| { - PumpxRpcError::from(DetailedError::new(PARSE_ERROR_CODE, "Invalid omni account")) - })?; - - params.user_operation.sender.parse::
().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Invalid sender address") - .with_field("sender") - .with_reason(format!("{}", e)), - ) - })?; +fn precheck_params(params: &RequestLoanTestParams) -> RpcResult<(AccountId, String, f64, f64)> { + let omni_account = to_omni_account(¶ms.omni_account)?; + + validate_evm_address(¶ms.user_operation.sender, "sender")?; if params.collateral_ticker.is_empty() { - return Err(PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Empty collateral ticker") - .with_field("collateral_ticker"), - )); + return Err(DetailedError::parse_error("Empty collateral_ticker").to_rpc_error()); } if params.collateral_size.is_empty() { - return Err(PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Empty collateral size") - .with_field("collateral_size"), - )); + return Err(DetailedError::parse_error("Empty collateral_size").to_rpc_error()); } if params.lending_ratio > 100 { - return Err(PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Invalid lending ratio") - .with_field("lending_ratio") - .with_received(params.lending_ratio.to_string()) - .with_expected("0-100"), - )); + return Err(DetailedError::parse_error("Too large lending_ratio").to_rpc_error()); } let collateral_ticker = params.collateral_ticker.to_uppercase(); - let collateral_size = params.collateral_size.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Invalid collateral size") - .with_field("collateral_size") - .with_reason(format!("{}", e)), - ) - })?; + let collateral_size: f64 = parse_as(¶ms.collateral_size, "collateral_size")?; let lending_ratio_f64 = params.lending_ratio as f64 / 100.0; Ok((omni_account, collateral_ticker, collateral_size, lending_ratio_f64)) @@ -384,84 +326,45 @@ async fn precheck_sell_spot( smart_wallet: &str, collateral_ticker: &str, collateral_size: f64, -) -> Result { - let (spot_meta, spot_mark_price, spot_mid_price) = - hypercore_client.get_spot_market_prices(collateral_ticker).await.map_err(|e| { - error!("Failed to get spot market prices for {}: {}", collateral_ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to get spot market prices: {}", e)), - ) - })?; - - let spot_asset_id = get_spot_asset_id(collateral_ticker, &spot_meta).map_err(|e| { - error!("Failed to get spot asset ID: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), - ) - })?; +) -> RpcResult { + let (spot_meta, spot_mark_price, spot_mid_price) = hypercore_client + .get_spot_market_prices(collateral_ticker) + .await + .map_err_internal("Failed to get spot market prices")?; + let spot_asset_id = get_spot_asset_id(collateral_ticker, &spot_meta) + .map_err_internal("Failed to get spot asset id")?; let spot_token = spot_meta .tokens .iter() .find(|t| t.name.eq_ignore_ascii_case(collateral_ticker)) - .ok_or_else(|| { - error!("Token {} not found in spot meta", collateral_ticker); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Token {} not found in spot meta", collateral_ticker)), - ) - })?; + .ok_or_internal("Token not found in spot meta")?; let spot_sz_decimals = spot_token.sz_decimals; // Validate trade size - validate_trade_size(collateral_size, spot_sz_decimals, None).map_err(|e| { - error!("Invalid collateral size for spot trading: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Invalid collateral size: {}", e)), - ) - })?; + validate_trade_size(collateral_size, spot_sz_decimals, None) + .map_err_internal("Invalid collateral size for spot trading")?; // Validate notional value with clamped size let clamped_size = clamp_size(collateral_size, spot_sz_decimals); - let clamped_size_f64 = clamped_size.parse::().map_err(|e| { - error!("Failed to parse clamped size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped size: {}", e)), - ) - })?; + let clamped_size_f64: f64 = parse_as(&clamped_size, "clamped_size")?; let (spot_bid_price, _) = get_bid_ask_prices(spot_mark_price, spot_mid_price); - validate_notional_value(spot_bid_price, clamped_size_f64, "Spot sell").map_err(|e| { - error!("{}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Notional value too low").with_reason(e), - ) - })?; + validate_notional_value(spot_bid_price, clamped_size_f64, "Spot sell") + .map_err_internal("Notional value too low")?; // Validate balance let user_balance = hypercore_client .get_spot_balance(smart_wallet, collateral_ticker) .await - .map_err(|e| { - error!("Failed to get user balance: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to query balance: {}", e)), - ) - })?; + .map_err_internal("Failed to get user balance")?; if user_balance < collateral_size { - error!("Insufficient balance: has {} but needs {}", user_balance, collateral_size); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Insufficient balance").with_reason(format!( - "User has {} but needs {} {}", - user_balance, collateral_size, collateral_ticker - )), - )); + let msg = + format!("Insufficient balance: has {} but needs {}", user_balance, collateral_size); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); } info!("✓ Spot sell precheck passed"); @@ -474,30 +377,18 @@ async fn precheck_open_hedge( collateral_ticker: &str, usdc_for_perp: f64, lending_ratio_f64: f64, -) -> Result { - let (perp_meta, perp_mark_price, perp_mid_price) = - hypercore_client.get_perp_market_prices(collateral_ticker).await.map_err(|e| { - error!("Failed to get perp market prices for {}: {}", collateral_ticker, e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to get perp market prices: {}", e)), - ) - })?; - - let perp_asset_id = get_perp_asset_id(collateral_ticker, &perp_meta).map_err(|e| { - error!("Failed to get perp asset ID: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), - ) - })?; +) -> RpcResult { + let (perp_meta, perp_mark_price, perp_mid_price) = hypercore_client + .get_perp_market_prices(collateral_ticker) + .await + .map_err_internal("Failed to get perp market prices")?; + let perp_asset_id = get_perp_asset_id(collateral_ticker, &perp_meta) + .map_err_internal("Failed to get perp asset id")?; - let perp_asset = perp_meta.universe.get(perp_asset_id as usize).ok_or_else(|| { - error!("Perp asset {} not found in meta", perp_asset_id); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Perp asset {} not found", perp_asset_id)), - ) - })?; + let perp_asset = perp_meta + .universe + .get(perp_asset_id as usize) + .ok_or_internal("Perp asset not found in meta")?; let perp_sz_decimals = perp_asset.sz_decimals; let perp_max_leverage = perp_asset.max_leverage; @@ -510,32 +401,15 @@ async fn precheck_open_hedge( let estimated_notional = usdc_for_perp * effective_leverage; let estimated_hedge_size = estimated_notional / perp_ask_price; - validate_trade_size(estimated_hedge_size, perp_sz_decimals, None).map_err(|e| { - error!("Invalid estimated hedge size for perp trading: {}", e); - PumpxRpcError::from(DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason( - format!( - "Invalid estimated hedge size (margin={:.2}, leverage={:.2}x, perp_price={:.2}, size={}): {}", - usdc_for_perp, effective_leverage, perp_ask_price, estimated_hedge_size, e - ), - )) - })?; + validate_trade_size(estimated_hedge_size, perp_sz_decimals, None) + .map_err_internal("Invalid estimated hedge size for perp trading")?; // Validate notional value with clamped size let clamped_size = clamp_size(estimated_hedge_size, perp_sz_decimals); - let clamped_size_f64 = clamped_size.parse::().map_err(|e| { - error!("Failed to parse clamped hedge size: {}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped hedge size: {}", e)), - ) - })?; + let clamped_size_f64: f64 = parse_as(&clamped_size, "clamped_hedge_size")?; - validate_notional_value(perp_ask_price, clamped_size_f64, "Perp open").map_err(|e| { - error!("{}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Notional value too low").with_reason(e), - ) - })?; + validate_notional_value(perp_ask_price, clamped_size_f64, "Perp open") + .map_err_internal("Notional value too low")?; info!("✓ Open hedge precheck passed"); @@ -549,7 +423,7 @@ async fn do_sell_spot Result<(f64, u128, Option), PumpxRpcError> { +) -> RpcResult<(f64, u128, Option)> { info!("Action: Selling {} {} in spot market", collateral_size, collateral_ticker); let clamped_size = clamp_size(collateral_size, sell_ctx.spot_sz_decimals); @@ -557,18 +431,8 @@ async fn do_sell_spot().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped size: {}", e)), - ) - })?; - let clamped_price_f64 = clamped_price.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped price: {}", e)), - ) - })?; + let clamped_size_f64: f64 = parse_as(&clamped_size, "clamped_size")?; + let clamped_price_f64: f64 = parse_as(&clamped_price, "clamped_price")?; let spot_sell_cloid = generate_cloid(); @@ -583,7 +447,7 @@ async fn do_sell_spot, usdc_for_perp: f64, current_nonce: &mut u128, -) -> Result, PumpxRpcError> { +) -> RpcResult> { info!("Action: Moving {:.2} USDC to perp", usdc_for_perp); - let initial_perp_balance = exec_ctx + let perp_state = exec_ctx .hypercore_client .get_perp_clearinghouse_state(exec_ctx.smart_wallet) .await - .map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to query perp balance: {}", e)), - ) - })? - .cross_margin_summary - .account_value - .parse::() - .map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse perp balance: {}", e)), - ) - })?; + .map_err_internal("Failed to query perp balance")?; + + let initial_perp_balance: f64 = + parse_as(&perp_state.cross_margin_summary.account_value, "initial_perp_balance")?; // Clear init_code (account already created by sell_spot) let mut user_op = exec_ctx.skeleton_user_op.clone(); user_op.nonce = *current_nonce; user_op.init_code = "0x".to_string(); - let to_perp_move_tx_hash = submit_corewriter_userop( + let to_perp_move_tx_hash = submit_corewriter_user_ops( exec_ctx.ctx.clone(), exec_ctx.omni_account, &user_op, @@ -754,12 +582,7 @@ async fn do_move_to_perp Result<(u128, Option), PumpxRpcError> { +) -> RpcResult<(u128, Option)> { let desired_leverage = 1.0 / (1.0 - lending_ratio_f64); let effective_leverage = desired_leverage.min(open_ctx.perp_max_leverage as f64); @@ -798,12 +621,7 @@ async fn do_open_hedge().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped hedge size: {}", e)), - ) - })?; - let clamped_hedge_price_f64 = clamped_hedge_price.parse::().map_err(|e| { - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason(format!("Failed to parse clamped hedge price: {}", e)), - ) - })?; + let clamped_hedge_size_f64: f64 = parse_as(&clamped_hedge_size, "clamped_hedge_size")?; + let clamped_hedge_price_f64: f64 = parse_as(&clamped_hedge_price, "clamped_hedge_price")?; let hedge_action = build_perp_long_order( open_ctx.perp_asset_id, @@ -837,7 +645,7 @@ async fn do_open_hedge. use crate::detailed_error::DetailedError; -use crate::error_code::{AUTH_VERIFICATION_FAILED_CODE, PARSE_ERROR_CODE}; -use crate::methods::omni::common::check_auth; -use crate::methods::omni::PumpxRpcError; use crate::server::RpcContext; -use crate::utils::omni::to_omni_account; +use crate::utils::omni::extract_omni_account; +use crate::utils::validation::parse_rpc_params; use ethers::types::Bytes; use executor_core::intent_executor::IntentExecutor; use executor_core::native_task::{PumpxChainId, PumxWalletIndex}; @@ -55,49 +53,21 @@ pub fn register_sign_limit_order_params< ) { module .register_async_method("omni_signLimitOrder", |params, ctx, ext| async move { - let oa_str = check_auth(&ext).map_err(|e| { - error!("Authentication check failed: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_suggestion("Please check your authentication credentials"), - ) - })?; - - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; - debug!("Received omni_signLimitOrder, params: {:?}", params); - let omni_account = to_omni_account(&oa_str).map_err(|_| { - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; + let params = parse_rpc_params::(params)?; + + let omni_account = extract_omni_account(&ext)?; // Inline handle_pumpx_sign_limit_order logic let Some(chain) = ChainType::from_pumpx_chain_id(params.chain_id) else { error!("Failed to map pumpx chain_id {}", params.chain_id); - return Err(PumpxRpcError::from( - DetailedError::new( - crate::error_code::INVALID_CHAIN_ID_CODE, - "Chain not supported", - ) - .with_reason(format!("Chain ID {} is not supported", params.chain_id)), - )); + return Err(DetailedError::invalid_chain_id(params.chain_id.into()).to_rpc_error()); }; let unsigned_tx_vec: Vec> = params.unsigned_tx.iter().map(|tx| tx.to_vec()).collect(); - let Ok(signed_txs) = ctx + let signed_txs = ctx .signer_client .request_signatures( chain, @@ -106,16 +76,7 @@ pub fn register_sign_limit_order_params< unsigned_tx_vec, ) .await - else { - error!("Failed to request signatures from pumpx-signer"); - return Err(PumpxRpcError::from( - DetailedError::new( - crate::error_code::SIGNATURE_SERVICE_UNAVAILABLE_CODE, - "Signature service unavailable", - ) - .with_suggestion("Please try again later"), - )); - }; + .map_err(|_| DetailedError::signer_service_error().to_rpc_error())?; Ok(SignLimitOrderResponse { intent_id: params.intent_id, diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_swap_order.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_swap_order.rs index ea0756710f..1d577fd5f4 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_swap_order.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_swap_order.rs @@ -1,27 +1,23 @@ -use super::common::check_omni_api_response; use crate::{ - detailed_error::DetailedError, - error_code::{INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, PARSE_ERROR_CODE, *}, - methods::omni::{common::check_auth, PumpxRpcError}, - server::RpcContext, - Decode, Deserialize, + detailed_error::DetailedError, methods::omni::check_backend_response, server::RpcContext, + utils::omni::extract_omni_account, utils::types::RpcResultExt, + utils::validation::parse_rpc_params, Decode, Deserialize, RpcResult, }; use executor_core::intent_executor::IntentExecutor; use executor_storage::{HeimaJwtStorage, IntentIdStorage, Storage}; use heima_authentication::constants::AUTH_TOKEN_ACCESS_TYPE; use heima_primitives::{ - AccountId, Address20, Address32, BinanceConfig, BoundedVec, ChainAsset, CrossChainSwapProvider, + Address20, Address32, BinanceConfig, BoundedVec, ChainAsset, CrossChainSwapProvider, EthereumToken, Intent, PumpxConfig, PumpxOrderType, SingleChainSwapProvider, SolanaToken, SwapOrder, }; use heima_utils::decode_hex; use jsonrpsee::RpcModule; -use pumpx::constants::*; use pumpx::methods::common::{OrderInfoResponse, SwapType}; use pumpx::methods::send_order_tx::SendOrderTxResponse; +use pumpx::{constants::*, methods::get_user_trade_info::UserTradeInfoResponse}; use serde::Serialize; -use std::str::FromStr; -use tracing::{debug, error, info}; +use tracing::{debug, error}; #[derive(Debug, Deserialize)] pub struct SubmitSwapOrderParams { @@ -43,16 +39,18 @@ pub struct SubmitSwapOrderParams { } impl SubmitSwapOrderParams { - pub fn try_get_from_chain_asset(&self) -> Result { + pub fn try_get_from_chain_asset(&self) -> RpcResult { let from_chain_id = self.from_chain_id; let from_token_ca = self.from_token_ca.clone(); Self::try_get_chain_asset(from_chain_id, from_token_ca) + .map_err(|_| DetailedError::invalid_params("from chain asset", "").to_rpc_error()) } - pub fn try_get_to_chain_asset(&self) -> Result { + pub fn try_get_to_chain_asset(&self) -> RpcResult { let to_chain_id = self.to_chain_id; let to_token_ca = self.to_token_ca.clone(); Self::try_get_chain_asset(to_chain_id, to_token_ca) + .map_err(|_| DetailedError::invalid_params("to chain asset", "").to_rpc_error()) } fn try_get_chain_asset(chain_id: u32, token_ca: Option) -> Result { @@ -110,78 +108,33 @@ pub fn register_submit_swap_order< ) { module .register_async_method("omni_submitSwapOrder", |params, ctx, ext| async move { - let omni_account = check_auth(&ext).map_err(|e| { - error!("Authentication check failed: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_suggestion("Please check your authentication credentials"), - ) - })?; - - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; - debug!("Received omni_submitSwapOrder, params: {:?}", params); - let Ok(omni_account_id) = AccountId::from_str(&omni_account) else { - error!("Failed to parse from omni account token"); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to parse omni account from authentication token"), - )); - }; + let params = parse_rpc_params::(params)?; + let omni_account = extract_omni_account(&ext)?; - let from_chain_asset = params.try_get_from_chain_asset().map_err(|_| { - error!("Failed to get from chain asset"); - PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid params") - .with_suggestion("Invalid method parameters"), - ) - })?; - let to_chain_asset = params.try_get_to_chain_asset().map_err(|_| { - error!("Failed to get to chain asset"); - PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid params") - .with_suggestion("Invalid method parameters"), - ) - })?; + let from_chain_asset = params.try_get_from_chain_asset()?; + let to_chain_asset = params.try_get_to_chain_asset()?; if params.order_type == PumpxOrderType::Limit && !from_chain_asset.is_same_chain(&to_chain_asset) { error!("Limit order must be on the same chain"); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid params") - .with_suggestion("Invalid method parameters"), - )); + return Err(DetailedError::invalid_params( + "chain asset", + "limit order must be on the same chain", + ) + .to_rpc_error()); } let from_amount = BoundedVec::try_from(params.from_amount.as_bytes().to_vec()) - .map_err(|_| { - error!("Failed to convert from_amount to BoundedVec"); - PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid params") - .with_suggestion("Invalid method parameters"), - ) - })?; - + .map_err_parse("Failed to convert from_amount to BoundedVec")?; let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); let Ok(Some(access_token)) = - storage.get(&(omni_account_id.clone(), AUTH_TOKEN_ACCESS_TYPE)) + storage.get(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE)) else { error!("Failed to get access token from storage"); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to get access token from storage"), - )); + return Err(DetailedError::storage_service_error("get access token").to_rpc_error()); }; let swap_order = SwapOrder { @@ -192,46 +145,24 @@ pub fn register_submit_swap_order< }; debug!("Calling pumpx get_user_trade_info"); - let user_trade_info = + let info: UserTradeInfoResponse = ctx.pumpx_api.get_user_trade_info(&access_token).await.map_err(|e| { error!("Failed to get user trade info: {:?}", e); - PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid params") - .with_suggestion("Invalid method parameters"), - ) + DetailedError::pumpx_service_error("get_user_trade_info", format!("{:?}", e)) + .to_rpc_error() })?; - debug!("Response pumpx get_user_trade_info: {:?}", user_trade_info); + debug!("Response pumpx get_user_trade_info: {:?}", info); - let gas_type_base = check_and_get_option_user_trade_info_field( - user_trade_info.data.gas_type_base, - "gas_type_base", - )?; - let gas_type_bsc = check_and_get_option_user_trade_info_field( - user_trade_info.data.gas_type_bsc, - "gas_type_bsc", - )?; - let gas_type_eth = check_and_get_option_user_trade_info_field( - user_trade_info.data.gas_type_eth, - "gas_type_eth", - )?; - let gas_type_sol = check_and_get_option_user_trade_info_field( - user_trade_info.data.gas_type_sol, - "gas_type_sol", - )?; + let gas_type_base = check_user_trade_info(info.data.gas_type_base, "gas_type_base")?; + let gas_type_bsc = check_user_trade_info(info.data.gas_type_bsc, "gas_type_bsc")?; + let gas_type_eth = check_user_trade_info(info.data.gas_type_eth, "gas_type_eth")?; + let gas_type_sol = check_user_trade_info(info.data.gas_type_sol, "gas_type_sol")?; - let is_anti_mev = check_and_get_option_user_trade_info_field( - user_trade_info.data.is_anti_mev, - "is_anti_mev", - )?; - let is_auto_slippage = check_and_get_option_user_trade_info_field( - user_trade_info.data.is_auto_slippage, - "is_auto_slippage", - )?; - let slippage = check_and_get_option_user_trade_info_field( - user_trade_info.data.slippage, - "slippage", - )?; + let is_anti_mev = check_user_trade_info(info.data.is_anti_mev, "is_anti_mev")?; + let is_auto_slippage = + check_user_trade_info(info.data.is_auto_slippage, "is_auto_slippage")?; + let slippage = check_user_trade_info(info.data.slippage, "slippage")?; let gas_type = match params.to_chain_id { BASE_CHAIN_ID => gas_type_base.to_number() as u32, @@ -240,10 +171,7 @@ pub fn register_submit_swap_order< SOLANA_CHAIN_ID => gas_type_sol.to_number() as u32, _ => { error!("Unsupported chain id: {}", params.to_chain_id); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Invalid params") - .with_suggestion("Invalid method parameters"), - )); + return Err(DetailedError::invalid_params("to_chain_id", "").to_rpc_error()); }, }; @@ -278,48 +206,41 @@ pub fn register_submit_swap_order< let intent = Intent::Swap( swap_order, ccs_provider, - scs_provider.try_into().map_err(|_| { - error!("Failed to convert single chain swap provider"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to convert single chain swap provider"), - ) - })?, + scs_provider + .try_into() + .map_err_internal("Failed to convert single chain swap provider")?, ); // Inlined handler logic from handle_request_intent debug!("Intent requested, intent_id: {}", params.intent_id); let intent_id_storage = IntentIdStorage::new(ctx.storage_db.clone()); - let stored_intent_id = match intent_id_storage.get(&omni_account_id) { + let stored_intent_id = match intent_id_storage.get(&omni_account) { Ok(id) => id.unwrap_or_default(), - Err(_) => { - error!("Failed to read intent from store"); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to read intent from store"), - )); + Err(e) => { + error!("Failed to read intent from store: {:?}", e); + return Err( + DetailedError::storage_service_error("get intent ID").to_rpc_error() + ); }, }; if params.intent_id == stored_intent_id + 1 { - if intent_id_storage.insert(&omni_account_id, params.intent_id).is_err() { - error!("Failed to save intent id"); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to save intent id"), - )); + if let Err(e) = intent_id_storage.insert(&omni_account, params.intent_id) { + error!("Failed to save intent id: {:?}", e); + return Err( + DetailedError::storage_service_error("insert intent ID").to_rpc_error() + ); } } else { error!( - "Intent id different than expected, expected: {:?}, got: {:?}", + "Invalid intent_id, expected: {:?}, got: {:?}", stored_intent_id + 1, params.intent_id ); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_PARAMS_CODE, "Intent nonce mismatch") - .with_reason("Intent ID does not match expected value"), - )); + return Err( + DetailedError::invalid_params("intent_id", "nonce mistmatch").to_rpc_error() + ); } let swap_response = match intent { @@ -328,16 +249,15 @@ pub fn register_submit_swap_order< | Intent::CallEthereum(_) | Intent::TransferEthereum(_) | Intent::TransferSolana(_) => { - info!("Intent temporarily rejected, intent_id: {}", params.intent_id); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("This intent type is temporarily not supported"), - )); + let msg = + format!("Intent temporarily rejected, intent_id: {}", params.intent_id); + error!(msg); + return Err(DetailedError::internal_error(&msg).to_rpc_error()); }, Intent::Swap(..) => { let response = match ctx .cross_chain_intent_executor - .execute(&omni_account_id, params.intent_id, intent.clone()) + .execute(&omni_account, params.intent_id, intent.clone()) .await { Ok((response, _)) => response, @@ -347,29 +267,18 @@ pub fn register_submit_swap_order< None }, }; - if let Some(response) = response { - response - } else { - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Intent execution failed"), - )); - } + response.ok_or( + DetailedError::internal_error("Intent execution failed").to_rpc_error(), + )? }, }; // Process the swap response based on order type if params.order_type == PumpxOrderType::Market { let market_order_response: SendOrderTxResponse = - Decode::decode(&mut swap_response.as_slice()).map_err(|e| { - error!("Failed to decode market order response: {:?}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason( - format!("Failed to decode market order response: {:?}", e), - ), - ) - })?; - check_omni_api_response(market_order_response.clone(), "Market order".into())?; + Decode::decode(&mut swap_response.as_slice()) + .map_err_internal("Failed to decode market order response")?; + check_backend_response(&market_order_response, "market_order")?; let response = PumpxSubmitSwapOrderResponse { backend_response: BackendResponse { limit_order_response: None, @@ -379,15 +288,9 @@ pub fn register_submit_swap_order< Ok(response) } else { let limit_order_response: OrderInfoResponse = - Decode::decode(&mut swap_response.as_slice()).map_err(|e| { - error!("Failed to decode limit order response: {:?}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason( - format!("Failed to decode limit order response: {:?}", e), - ), - ) - })?; - check_omni_api_response(limit_order_response.clone(), "Limit order".into())?; + Decode::decode(&mut swap_response.as_slice()) + .map_err_internal("Failed to decode limit order response")?; + check_backend_response(&limit_order_response, "limit_order")?; let response = PumpxSubmitSwapOrderResponse { backend_response: BackendResponse { limit_order_response: Some(limit_order_response), @@ -400,18 +303,10 @@ pub fn register_submit_swap_order< .expect("Failed to register omni_submitSwapOrder method"); } -fn check_and_get_option_user_trade_info_field( - field_value: Option, - field_name: &str, -) -> Result { +fn check_user_trade_info(field_value: Option, field_name: &str) -> RpcResult { field_value.ok_or_else(|| { - error!("Response data.{} of call get_user_trade_info is none", field_name); - PumpxRpcError::from( - DetailedError::new( - PUMPX_API_GET_ACCOUNT_USER_ID_FAILED_CODE, - "Failed to get user trade info from API", - ) - .with_suggestion("User trade info retrieval failed. Please try again later."), - ) + let msg = format!("Response data.{} of call get_user_trade_info is none", field_name); + error!(msg); + DetailedError::internal_error(&msg).to_rpc_error() }) } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op.rs deleted file mode 100644 index 793e7adbe9..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op.rs +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -use crate::detailed_error::DetailedError; -use crate::error_code::{AUTH_VERIFICATION_FAILED_CODE, PARSE_ERROR_CODE}; -use crate::methods::omni::common::check_auth; -use crate::methods::omni::PumpxRpcError; -use crate::server::RpcContext; -use crate::utils::omni::to_omni_account; -use crate::utils::paymaster::{ - extract_paymaster_address, is_whitelisted_paymaster, parse_whitelisted_paymasters, - process_erc20_paymaster_data, -}; -use crate::utils::user_op::{convert_to_packed_user_op, substrate_to_ethereum_signature}; -use crate::validation_helpers::{ - validate_chain_id, validate_user_operations, validate_wallet_index, -}; -use aa_contracts_client::calculate_user_operation_hash; -use alloy::primitives::Bytes; -use binance_api::BinancePaymasterApi; -use executor_core::intent_executor::IntentExecutor; -use executor_core::types::SerializablePackedUserOperation; -use executor_primitives::ChainId; -use jsonrpsee::RpcModule; -use serde::{Deserialize, Serialize}; -use signer_client::ChainType; -use tracing::{debug, error, info}; - -#[derive(Debug, Deserialize)] -pub struct SubmitUserOpParams { - pub user_operations: Vec, - pub chain_id: ChainId, - pub wallet_index: u32, -} - -#[derive(Serialize, Clone)] -pub struct SubmitUserOpResponse { - pub transaction_hash: Option, -} - -pub fn register_submit_user_op( - module: &mut RpcModule>, -) { - module - .register_async_method("omni_submitUserOp", |params, ctx, ext| async move { - let oa_str = check_auth(&ext).map_err(|e| { - error!("Authentication check failed: {:?}", e); - PumpxRpcError::from( - DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication failed") - .with_suggestion("Please provide valid authentication credentials"), - ) - })?; - - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Failed to parse request parameters") - .with_suggestion(format!("Invalid JSON structure: {}", e)), - ) - })?; - - debug!("Received omni_submitUserOp, params: {:?}", params); - - validate_chain_id(params.chain_id as u32, Some("evm")).map_err(PumpxRpcError::from)?; - - validate_wallet_index(params.wallet_index).map_err(PumpxRpcError::from)?; - - validate_user_operations(¶ms.user_operations).map_err(PumpxRpcError::from)?; - - let omni_account = to_omni_account(&oa_str).map_err(|_| { - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; - - // Inlined handler logic from handle_submit_user_op - info!( - "Processing SubmitUserOp for {} UserOperations on chain_id: {}", - params.user_operations.len(), - params.chain_id - ); - - // Get EntryPoint client for this chain (needed for both signing and submission) - let entry_point_client = ctx.entry_point_clients.get(¶ms.chain_id).ok_or_else(|| { - error!("No EntryPoint client configured for chain_id: {}", params.chain_id); - PumpxRpcError::from(DetailedError::chain_not_supported(params.chain_id)) - })?; - - // Parse whitelisted paymasters once - let whitelisted_paymaster = parse_whitelisted_paymasters(); - - // Process each UserOperation in the batch - let mut aa_user_ops = Vec::new(); - - for (index, serializable_user_op) in params.user_operations.iter().enumerate() { - // Convert SerializablePackedUserOperation to PackedUserOperation - let mut packed_user_op = convert_to_packed_user_op(serializable_user_op.clone()) - .map_err(|e| { - error!("Failed to convert UserOperation {}: {}", index, e); - PumpxRpcError::from(DetailedError::invalid_user_operation_error(&format!( - "Invalid user operation at index {}", - index - ))) - })?; - - // Check userOp signature status and validate paymaster usage - if packed_user_op.signature.is_empty() { - // UNSIGNED userOp: If paymaster specified, must be whitelisted - if !packed_user_op.paymasterAndData.is_empty() { - if let Some(paymaster_address) = - extract_paymaster_address(&packed_user_op.paymasterAndData) - { - if !is_whitelisted_paymaster(&paymaster_address, &whitelisted_paymaster) - { - error!( - "UserOperation {} uses non-whitelisted paymaster {}. Only whitelisted paymasters are allowed for unsigned userOps.", - index, paymaster_address - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "UserOperation at index {} uses non-whitelisted paymaster {}", - index, paymaster_address - )), - )); - } - } - - match process_erc20_paymaster_data( - ctx.binance_api_client.as_ref() as &dyn BinancePaymasterApi, - &packed_user_op.paymasterAndData, - params.chain_id, - ) - .await - { - Ok(Some(updated_paymaster_data)) => { - packed_user_op.paymasterAndData = updated_paymaster_data; - info!("Updated ERC20 paymaster data for UserOperation {}", index); - }, - Ok(None) => { - // Not an ERC20 paymaster, continue as normal - debug!("UserOperation {} does not use ERC20 paymaster", index); - }, - Err(e) => { - error!( - "Failed to process ERC20 paymaster data for UserOperation {}: {}", - index, e - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "ERC20 paymaster processing failed for operation at index {}: {}", - index, e - )), - )); - }, - } - } - - info!("Requesting signature from pumpx signer for UserOperation {}", index); - - // Log UserOp details for debugging - info!( - "UserOp details - Sender: {}, Nonce: {}, InitCode length: {}, CallData length: {}", - packed_user_op.sender, - packed_user_op.nonce, - packed_user_op.initCode.len(), - packed_user_op.callData.len() - ); - - let entry_point_address = entry_point_client.entry_point_address(); - - let user_op_hash_bytes = calculate_user_operation_hash( - &packed_user_op, - entry_point_address, - params.chain_id, - ); - let message_to_sign = user_op_hash_bytes.to_vec(); - - info!( - "Signing UserOp hash: 0x{}, EntryPoint: {}, ChainID: {}", - hex::encode(user_op_hash_bytes), - entry_point_address, - params.chain_id - ); - - // Request signature from pumpx signer for EVM chain - let signature_result = ctx - .signer_client - .request_signature( - ChainType::Evm, - params.wallet_index, - omni_account.clone().into(), - message_to_sign, - ) - .await; - - let signature = match signature_result { - Ok(sig) => substrate_to_ethereum_signature(&sig) - .map_err(|e| { - error!("Failed to convert signature: {}", e); - PumpxRpcError::from(DetailedError::signature_service_unavailable()) - })? - .to_vec(), - Err(_) => { - error!("Failed to sign user operation {}", index); - return Err(PumpxRpcError::from( - DetailedError::signature_service_unavailable(), - )); - }, - }; - - // Prepend 0x01 byte to indicate Root signature type (according to UserOpSigner enum) - let mut signature_with_prefix: Vec = vec![0x01]; - signature_with_prefix.extend_from_slice(&signature); - packed_user_op.signature = Bytes::from(signature_with_prefix); - info!("UserOperation {} signed successfully", index); - } else { - // SIGNED userOp: Only allowed if no paymaster specified - if !packed_user_op.paymasterAndData.is_empty() { - error!( - "UserOperation {} is signed but has paymaster data. Signed userOps are only allowed without paymaster.", - index - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "UserOperation at index {} is signed but specifies a paymaster", - index - )), - )); - } - info!("UserOperation {} is signed with no paymaster, processing", index); - } - - // Convert to aa_contracts_client::PackedUserOperation for EntryPoint call - let aa_user_op = aa_contracts_client::PackedUserOperation { - sender: packed_user_op.sender, - nonce: packed_user_op.nonce, - initCode: packed_user_op.initCode.clone(), - callData: packed_user_op.callData.clone(), - accountGasLimits: packed_user_op.accountGasLimits, - preVerificationGas: packed_user_op.preVerificationGas, - gasFees: packed_user_op.gasFees, - paymasterAndData: packed_user_op.paymasterAndData.clone(), - signature: packed_user_op.signature.clone(), - }; - aa_user_ops.push(aa_user_op); - } - - // Get beneficiary address from the EntryPoint client's wallet - let beneficiary = entry_point_client.get_wallet_address().await.map_err(|_| { - let err_msg = "Failed to get wallet address from EntryPoint client".to_string(); - error!("{}", err_msg.clone()); - PumpxRpcError::from_code_and_message( - crate::error_code::INTERNAL_ERROR_CODE, - err_msg, - ) - })?; - - // Run batch simulation for all UserOperations before submission - info!("Running batch simulation for {} UserOperations", aa_user_ops.len()); - match entry_point_client.simulate_handle_ops(&aa_user_ops, beneficiary).await { - Ok(simulation_results) => { - for (index, result) in simulation_results.iter().enumerate() { - info!( - "UserOperation {} simulation successful. PreOpGas: {}, Paid: {}, AccountValidation: {}, PaymasterValidation: {}", - index, - result.preOpGas, - result.paid, - result.accountValidationData, - result.paymasterValidationData - ); - } - info!("All {} UserOperations passed batch simulation checks", aa_user_ops.len()); - }, - Err(e) => { - let err_msg: String = format!("Batch UserOperation simulation failed: {}", e); - error!("{}", err_msg.clone()); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&err_msg), - )); - }, - } - - // Submit all UserOperations via EntryPoint.handleOps() with retry logic - let transaction_hash = - match entry_point_client.handle_ops_with_retry(&aa_user_ops, beneficiary).await { - Ok(tx_hash) => { - // Return the actual transaction hash from handle_ops - Some(tx_hash) - }, - Err(_) => { - let err_msg = - "Failed to submit UserOperations to EntryPoint via handleOps after retries" - .to_string(); - error!("{}", err_msg.clone()); - return Err(PumpxRpcError::from_code_and_message( - crate::error_code::INTERNAL_ERROR_CODE, - err_msg, - )); - }, - }; - - Ok(SubmitUserOpResponse { transaction_hash }) - }) - .expect("Failed to register omni_submitUserOp method"); -} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_test.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_test.rs index 64089b1769..20f3f8ebb7 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_test.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_test.rs @@ -14,26 +14,18 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use crate::detailed_error::DetailedError; -use crate::error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE}; -use crate::methods::omni::PumpxRpcError; use crate::server::RpcContext; -use crate::utils::paymaster::{ - extract_paymaster_address, is_whitelisted_paymaster, parse_whitelisted_paymasters, - process_erc20_paymaster_data, -}; -use crate::utils::user_op::{convert_to_packed_user_op, substrate_to_ethereum_signature}; -use aa_contracts_client::calculate_user_operation_hash; -use alloy::primitives::{Address, Bytes}; -use binance_api::BinancePaymasterApi; +use crate::utils::omni::to_omni_account; +use crate::utils::user_op::submit_user_ops; +use crate::utils::validation::{parse_as, parse_rpc_params}; +use alloy::primitives::Address; use executor_core::intent_executor::IntentExecutor; use executor_core::types::SerializablePackedUserOperation; -use executor_primitives::{AccountId, ChainId}; +use executor_primitives::ChainId; +use jsonrpsee::types::ErrorObjectOwned; use jsonrpsee::RpcModule; -use parity_scale_codec::Decode; use serde::{Deserialize, Serialize}; -use signer_client::ChainType; -use tracing::{debug, error, info}; +use tracing::debug; #[derive(Debug, Deserialize)] pub struct SubmitUserOpTestParams { @@ -57,285 +49,28 @@ pub fn register_submit_user_op_test< ) { module .register_async_method("omni_submitUserOpTest", |params, ctx, _ext| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields"), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_submitUserOpTest, params: {:?}", params); - let address_bytes = - hex::decode(params.omni_account.strip_prefix("0x").unwrap_or(¶ms.omni_account)) - .map_err(|_| { - error!("Failed to decode omni account hex string"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to decode omni account hex string"), - ) - })?; - - if address_bytes.len() != 32 { - error!( - "Invalid omni account length: expected 32 bytes, got {}", - address_bytes.len() - ); - return Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(format!( - "Invalid omni account length: expected 32 bytes, got {}", - address_bytes.len() - )), - )); - } - for op in ¶ms.user_operations { - op.sender.parse::
().map_err(|e| { - error!("Invalid sender address '{}': {}", op.sender, e); - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_field("sender") - .with_reason(format!("Invalid sender address '{}': {}", op.sender, e)), - ) - })?; + let _: Address = parse_as(&op.sender, "sender")?; } - let omni_account = AccountId::decode(&mut &address_bytes[..]).map_err(|_| { - error!("Failed to decode AccountId from bytes"); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to decode AccountId from bytes"), - ) - })?; - - // Inlined handler logic from handle_submit_user_op - info!( - "Processing SubmitUserOp for {} UserOperations on chain_id: {}", - params.user_operations.len(), - params.chain_id - ); - - // Get EntryPoint client for this chain (needed for both signing and submission) - let entry_point_client = ctx.entry_point_clients.get(¶ms.chain_id).ok_or_else(|| { - error!("No EntryPoint client configured for chain_id: {}", params.chain_id); - PumpxRpcError::from(DetailedError::chain_not_supported(params.chain_id)) - })?; - - // Parse whitelisted paymasters once - let whitelisted_paymaster = parse_whitelisted_paymasters(); - - // Process each UserOperation in the batch - let mut aa_user_ops = Vec::new(); - - for (index, serializable_user_op) in params.user_operations.iter().enumerate() { - // Convert SerializablePackedUserOperation to PackedUserOperation - let mut packed_user_op = convert_to_packed_user_op(serializable_user_op.clone()) - .map_err(|e| { - error!("Failed to convert UserOperation {}: {}", index, e); - PumpxRpcError::from(DetailedError::invalid_user_operation_error(&format!( - "Invalid user operation at index {}", - index - ))) - })?; - - // Check userOp signature status and validate paymaster usage - if packed_user_op.signature.is_empty() { - // UNSIGNED userOp: If paymaster specified, must be whitelisted - if !packed_user_op.paymasterAndData.is_empty() { - if let Some(paymaster_address) = - extract_paymaster_address(&packed_user_op.paymasterAndData) - { - if !is_whitelisted_paymaster(&paymaster_address, &whitelisted_paymaster) - { - error!( - "UserOperation {} uses non-whitelisted paymaster {}. Only whitelisted paymasters are allowed for unsigned userOps.", - index, paymaster_address - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "UserOperation at index {} uses non-whitelisted paymaster {}", - index, paymaster_address - )), - )); - } - } - - match process_erc20_paymaster_data( - ctx.binance_api_client.as_ref() as &dyn BinancePaymasterApi, - &packed_user_op.paymasterAndData, - params.chain_id, - ) - .await - { - Ok(Some(updated_paymaster_data)) => { - packed_user_op.paymasterAndData = updated_paymaster_data; - info!("Updated ERC20 paymaster data for UserOperation {}", index); - }, - Ok(None) => { - // Not an ERC20 paymaster, continue as normal - debug!("UserOperation {} does not use ERC20 paymaster", index); - }, - Err(e) => { - error!( - "Failed to process ERC20 paymaster data for UserOperation {}: {}", - index, e - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "ERC20 paymaster processing failed for operation at index {}: {}", - index, e - )), - )); - }, - } - } - - info!("Requesting signature from pumpx signer for UserOperation {}", index); - - // Log UserOp details for debugging - info!( - "UserOp details - Sender: {}, Nonce: {}, InitCode length: {}, CallData length: {}", - packed_user_op.sender, - packed_user_op.nonce, - packed_user_op.initCode.len(), - packed_user_op.callData.len() - ); - - let entry_point_address = entry_point_client.entry_point_address(); - - let user_op_hash_bytes = calculate_user_operation_hash( - &packed_user_op, - entry_point_address, - params.chain_id, - ); - let message_to_sign = user_op_hash_bytes.to_vec(); - - info!( - "Signing UserOp hash: 0x{}, EntryPoint: {}, ChainID: {}", - hex::encode(user_op_hash_bytes), - entry_point_address, - params.chain_id - ); - - // Request signature from pumpx signer for EVM chain - let signature_result = ctx - .signer_client - .request_signature( - ChainType::Evm, - params.wallet_index, - omni_account.clone().into(), - message_to_sign, - ) - .await; - - let signature = match signature_result { - Ok(sig) => substrate_to_ethereum_signature(&sig) - .map_err(|e| { - error!("Failed to convert signature: {}", e); - PumpxRpcError::from(DetailedError::signature_service_unavailable()) - })? - .to_vec(), - Err(_) => { - error!("Failed to sign user operation {}", index); - return Err(PumpxRpcError::from( - DetailedError::signature_service_unavailable(), - )); - }, - }; - - // Prepend 0x01 byte to indicate Root signature type (according to UserOpSigner enum) - let mut signature_with_prefix: Vec = vec![0x01]; - signature_with_prefix.extend_from_slice(&signature); - packed_user_op.signature = Bytes::from(signature_with_prefix); - info!("UserOperation {} signed successfully", index); - } else { - // SIGNED userOp: Only allowed if no paymaster specified - if !packed_user_op.paymasterAndData.is_empty() { - error!( - "UserOperation {} is signed but has paymaster data. Signed userOps are only allowed without paymaster.", - index - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "UserOperation at index {} is signed but specifies a paymaster", - index - )), - )); - } - info!("UserOperation {} is signed with no paymaster, processing", index); - } - - // Convert to aa_contracts_client::PackedUserOperation for EntryPoint call - let aa_user_op = aa_contracts_client::PackedUserOperation { - sender: packed_user_op.sender, - nonce: packed_user_op.nonce, - initCode: packed_user_op.initCode.clone(), - callData: packed_user_op.callData.clone(), - accountGasLimits: packed_user_op.accountGasLimits, - preVerificationGas: packed_user_op.preVerificationGas, - gasFees: packed_user_op.gasFees, - paymasterAndData: packed_user_op.paymasterAndData.clone(), - signature: packed_user_op.signature.clone(), - }; - aa_user_ops.push(aa_user_op); - } - - // Get beneficiary address from the EntryPoint client's wallet - let beneficiary = entry_point_client.get_wallet_address().await.map_err(|_| { - let err_msg = "Failed to get wallet address from EntryPoint client".to_string(); - error!("{}", err_msg.clone()); - PumpxRpcError::from_code_and_message( - crate::error_code::INTERNAL_ERROR_CODE, - err_msg, - ) - })?; - - // Run batch simulation for all UserOperations before submission - info!("Running batch simulation for {} UserOperations", aa_user_ops.len()); - match entry_point_client.simulate_handle_ops(&aa_user_ops, beneficiary).await { - Ok(simulation_results) => { - for (index, result) in simulation_results.iter().enumerate() { - info!( - "UserOperation {} simulation successful. PreOpGas: {}, Paid: {}, AccountValidation: {}, PaymasterValidation: {}", - index, - result.preOpGas, - result.paid, - result.accountValidationData, - result.paymasterValidationData - ); - } - info!("All {} UserOperations passed batch simulation checks", aa_user_ops.len()); - }, - Err(e) => { - let err_msg: String = format!("Batch UserOperation simulation failed: {}", e); - error!("{}", err_msg.clone()); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&err_msg), - )); - }, - } + let omni_account = to_omni_account(¶ms.omni_account)?; - // Submit all UserOperations via EntryPoint.handleOps() with retry logic - let transaction_hash = - match entry_point_client.handle_ops_with_retry(&aa_user_ops, beneficiary).await { - Ok(tx_hash) => { - // Return the actual transaction hash from handle_ops - Some(tx_hash) - }, - Err(_) => { - let err_msg = - "Failed to submit UserOperations to EntryPoint via handleOps after retries" - .to_string(); - error!("{}", err_msg.clone()); - return Err(PumpxRpcError::from_code_and_message( - crate::error_code::INTERNAL_ERROR_CODE, - err_msg, - )); - }, - }; + let transaction_hash = submit_user_ops( + &ctx, + params.user_operations, + params.chain_id, + params.wallet_index, + &omni_account, + ) + .await?; - Ok(SubmitUserOpTestResponse { transaction_hash }) + Ok::(SubmitUserOpTestResponse { + transaction_hash, + }) }) .expect("Failed to register omni_submitUserOpTest method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_with_auth.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_with_auth.rs index 8d7750b44d..2c553bab32 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_with_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_user_op_with_auth.rs @@ -1,23 +1,16 @@ -use crate::auth_utils::{ - verify_payload_timestamp, verify_wildmeta_backend_signature, verify_wildmeta_signature, -}; use crate::detailed_error::DetailedError; -use crate::error_code::{ - AUTH_VERIFICATION_FAILED_CODE, INVALID_USER_OPERATION_CODE, PARSE_ERROR_CODE, -}; -use crate::methods::omni::PumpxRpcError; +use crate::error_code::{AUTH_VERIFICATION_FAILED_CODE, INVALID_USEROP_CODE}; use crate::server::RpcContext; -use crate::utils::paymaster::{ - extract_paymaster_address, is_whitelisted_paymaster, parse_whitelisted_paymasters, - process_erc20_paymaster_data, +use crate::utils::auth::{ + verify_payload_timestamp, verify_wildmeta_backend_signature, verify_wildmeta_signature, }; -use crate::utils::user_op::{convert_to_packed_user_op, substrate_to_ethereum_signature}; -use crate::validation_helpers::{ - validate_chain_id, validate_user_operations, validate_wallet_index, +use crate::utils::types::RpcResultExt; +use crate::utils::user_op::submit_user_ops; +use crate::utils::validation::{ + parse_rpc_params, validate_chain_id, validate_user_operations, validate_wallet_index, }; -use aa_contracts_client::calculate_user_operation_hash; -use alloy::primitives::{hex, Address, Bytes}; -use binance_api::BinancePaymasterApi; +use crate::RpcResult; +use alloy::primitives::{hex, Address}; use executor_core::intent_executor::IntentExecutor; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::{ChainId, ClientAuth, Identity, UserAuth, UserId}; @@ -27,7 +20,7 @@ use pumpx::pubkey_to_address; use serde::{Deserialize, Serialize}; use signer_client::ChainType; use std::sync::Arc; -use tracing::{debug, error, info}; +use tracing::{debug, error}; // Chain ID constants const ARBITRUM_MAINNET: ChainId = 42161; @@ -61,70 +54,63 @@ pub struct SubmitUserOpWithAuthResponse { } /// Validates USDC transfer call for Arbitrum chains -fn validate_arbitrum_usdc_transfer( - call_data: &str, - chain_id: ChainId, -) -> Result<(), PumpxRpcError> { +fn validate_arbitrum_usdc_transfer(call_data: &str, chain_id: ChainId) -> RpcResult<()> { // Validate chain is supported for Arbitrum USDC validation match chain_id { ARBITRUM_MAINNET | ARBITRUM_SEPOLIA => {}, _ => { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Chain ID is not supported for Arbitrum USDC validation", - ) - .with_field("chain_id") - .with_received(chain_id.to_string()) - .with_expected("42161 or 421614"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Chain ID is not supported for Arbitrum USDC validation", + ) + .with_field("chain_id") + .with_received(chain_id.to_string()) + .with_expected("42161 or 421614") + .to_rpc_error()); }, }; // Parse calldata - should be USDC.transfer method call let call_bytes = hex::decode(call_data.strip_prefix("0x").unwrap_or(call_data)).map_err(|e| { - PumpxRpcError::from( - DetailedError::new( - // todo: proper error code - AUTH_VERIFICATION_FAILED_CODE, - "Invalid hex encoding in call data", - ) - .with_field("call_data") - .with_reason(format!("Hex decode error: {}", e)), + DetailedError::new( + // todo: proper error code + AUTH_VERIFICATION_FAILED_CODE, + "Invalid hex encoding in call data", ) + .with_field("call_data") + .with_reason(format!("Hex decode error: {}", e)) + .to_rpc_error() })?; // Check if it's an ERC20 transfer call (method signature 0xa9059cbb) if call_bytes.len() < 4 || call_bytes[0..4] != ERC20_TRANSFER_SIGNATURE { - return Err(PumpxRpcError::from( - DetailedError::new( - // todo: proper error code - AUTH_VERIFICATION_FAILED_CODE, - "Call data is not an ERC20 transfer function call", - ) - .with_field("method_signature") - .with_received(if call_bytes.len() >= 4 { - format!("0x{}", hex::encode(&call_bytes[0..4])) - } else { - "".to_string() - }) - .with_expected("0xa9059cbb (ERC20.transfer)"), - )); + return Err(DetailedError::new( + // todo: proper error code + AUTH_VERIFICATION_FAILED_CODE, + "Call data is not an ERC20 transfer function call", + ) + .with_field("method_signature") + .with_received(if call_bytes.len() >= 4 { + format!("0x{}", hex::encode(&call_bytes[0..4])) + } else { + "".to_string() + }) + .with_expected("0xa9059cbb (ERC20.transfer)") + .to_rpc_error()); } // Decode the transfer call to get recipient if call_bytes.len() < 68 { - return Err(PumpxRpcError::from( - DetailedError::new( - // todo: proper error code - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short for ERC20 transfer", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected("68 bytes minimum"), - )); + return Err(DetailedError::new( + // todo: proper error code + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short for ERC20 transfer", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("68 bytes minimum") + .to_rpc_error()); } // Extract recipient address (bytes 4-36, but we need the last 20 bytes) @@ -133,62 +119,58 @@ fn validate_arbitrum_usdc_transfer( // Validate recipient is the official arb -> hyper bridge if recipient.to_lowercase() != ARB_TO_HYPER_BRIDGE_ADDRESS.to_lowercase() { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "USDC transfer recipient is not the official Arbitrum to Hyperliquid bridge", - ) - .with_field("recipient") - .with_received(recipient) - .with_expected(ARB_TO_HYPER_BRIDGE_ADDRESS), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "USDC transfer recipient is not the official Arbitrum to Hyperliquid bridge", + ) + .with_field("recipient") + .with_received(recipient) + .with_expected(ARB_TO_HYPER_BRIDGE_ADDRESS) + .to_rpc_error()); } Ok(()) } /// Validates core writer call for HyperEVM chains -fn validate_hyperevm_core_writer(call_data: &str, chain_id: ChainId) -> Result<(), PumpxRpcError> { +fn validate_hyperevm_core_writer(call_data: &str, chain_id: ChainId) -> RpcResult<()> { // Validate chain is HyperEVM if chain_id != HYPEREVM_MAINNET && chain_id != HYPEREVM_TESTNET { - return Err(PumpxRpcError::from( - DetailedError::new( - // todo: proper error code - AUTH_VERIFICATION_FAILED_CODE, - "Chain ID is not supported for HyperEVM validation", - ) - .with_field("chain_id") - .with_received(chain_id.to_string()) - .with_expected("999 or 998"), - )); + return Err(DetailedError::new( + // todo: proper error code + AUTH_VERIFICATION_FAILED_CODE, + "Chain ID is not supported for HyperEVM validation", + ) + .with_field("chain_id") + .with_received(chain_id.to_string()) + .with_expected("999 or 998") + .to_rpc_error()); } // Parse calldata let call_bytes = hex::decode(call_data.strip_prefix("0x").unwrap_or(call_data)).map_err(|e| { - PumpxRpcError::from( - DetailedError::new( - // todo: proper error code - AUTH_VERIFICATION_FAILED_CODE, - "Invalid hex encoding in call data", - ) - .with_field("call_data") - .with_reason(format!("Hex decode error: {}", e)), + DetailedError::new( + // todo: proper error code + AUTH_VERIFICATION_FAILED_CODE, + "Invalid hex encoding in call data", ) + .with_field("call_data") + .with_reason(format!("Hex decode error: {}", e)) + .to_rpc_error() })?; // Extract method signature if available if call_bytes.len() < 4 { - return Err(PumpxRpcError::from( - DetailedError::new( - // todo: proper error code - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short to contain method signature", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected("4 bytes minimum"), - )); + return Err(DetailedError::new( + // todo: proper error code + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short to contain method signature", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("4 bytes minimum") + .to_rpc_error()); } // Proper action_id extraction based on payload format: @@ -199,15 +181,14 @@ fn validate_hyperevm_core_writer(call_data: &str, chain_id: ChainId) -> Result<( // The calldata should contain the raw payload as data parameter in method call // First decode the actual payload from the method call parameters if call_bytes.len() < 4 + 32 + 32 { - return Err(PumpxRpcError::from( - DetailedError::new( - INVALID_USER_OPERATION_CODE, - "Call data too short for payload parameter", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected("68 bytes minimum (4 bytes method + 32 bytes offset + 32 bytes length)"), - )); + return Err(DetailedError::new( + INVALID_USEROP_CODE, + "Call data too short for payload parameter", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("68 bytes minimum (4 bytes method + 32 bytes offset + 32 bytes length)") + .to_rpc_error()); } // Skip method signature (4 bytes) and offset parameter (32 bytes) @@ -222,15 +203,14 @@ fn validate_hyperevm_core_writer(call_data: &str, chain_id: ChainId) -> Result<( // Ensure we have enough data for the payload if call_bytes.len() < 68 + payload_length { - return Err(PumpxRpcError::from( - DetailedError::new( - INVALID_USER_OPERATION_CODE, - "Call data too short for declared payload length", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected(format!("{} bytes", 68 + payload_length)), - )); + return Err(DetailedError::new( + INVALID_USEROP_CODE, + "Call data too short for declared payload length", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected(format!("{} bytes", 68 + payload_length)) + .to_rpc_error()); } // Extract the actual payload @@ -238,26 +218,24 @@ fn validate_hyperevm_core_writer(call_data: &str, chain_id: ChainId) -> Result<( // Validate payload format: minimum 4 bytes (1 version + 3 action_id) if payload.len() < 4 { - return Err(PumpxRpcError::from( - DetailedError::new( - INVALID_USER_OPERATION_CODE, - "Payload too short for version and action_id", - ) - .with_field("payload_length") - .with_received(payload.len().to_string()) - .with_expected("4 bytes minimum (1 version + 3 action_id)"), - )); + return Err(DetailedError::new( + INVALID_USEROP_CODE, + "Payload too short for version and action_id", + ) + .with_field("payload_length") + .with_received(payload.len().to_string()) + .with_expected("4 bytes minimum (1 version + 3 action_id)") + .to_rpc_error()); } // Extract version (first byte) let version = payload[0]; if version != 0x01 { - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid payload version") - .with_field("version") - .with_received(format!("0x{:02x}", version)) - .with_expected("0x01"), - )); + return Err(DetailedError::new(INVALID_USEROP_CODE, "Invalid payload version") + .with_field("version") + .with_received(format!("0x{:02x}", version)) + .with_expected("0x01") + .to_rpc_error()); } // Extract action_id (next 3 bytes) and convert to u32 @@ -274,44 +252,34 @@ fn validate_hyperevm_core_writer(call_data: &str, chain_id: ChainId) -> Result<( ]; if !valid_action_ids.contains(&action_id) { - return Err(PumpxRpcError::from( - DetailedError::new( - INVALID_USER_OPERATION_CODE, - "Invalid action_id for HyperEVM core writer call", - ) - .with_field("action_id") - .with_received(format!("0x{:06x}", action_id)) - .with_expected("One of: 0x000002, 0x000003, 0x000004, 0x000005, 0x000007"), - )); + return Err(DetailedError::new( + INVALID_USEROP_CODE, + "Invalid action_id for HyperEVM core writer call", + ) + .with_field("action_id") + .with_received(format!("0x{:06x}", action_id)) + .with_expected("One of: 0x000002, 0x000003, 0x000004, 0x000005, 0x000007") + .to_rpc_error()); } Ok(()) } /// Extract target addresses and inner calldata from OmniAccount execute() or executeBatch() calldata -fn extract_execute_params_from_calldata( - call_data: &str, -) -> Result, PumpxRpcError> { +fn extract_execute_params_from_calldata(call_data: &str) -> RpcResult> { // Parse calldata - should be OmniAccount.execute() or executeBatch() - let call_bytes = - hex::decode(call_data.strip_prefix("0x").unwrap_or(call_data)).map_err(|e| { - PumpxRpcError::from( - DetailedError::new(PARSE_ERROR_CODE, "Invalid hex encoding in call data") - .with_field("call_data") - .with_reason(format!("Hex decode error: {}", e)), - ) - })?; + let call_bytes = hex::decode(call_data.strip_prefix("0x").unwrap_or(call_data)) + .map_err_parse("Failded to decode call_data")?; if call_bytes.len() < 4 { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short to contain method signature", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected("4 bytes minimum"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short to contain method signature", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("4 bytes minimum") + .to_rpc_error()); } // Check method signatures @@ -328,33 +296,29 @@ fn extract_execute_params_from_calldata( // Handle executeBatch call parse_execute_batch(&call_bytes) } else { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Call data is not an OmniAccount execute or executeBatch function call", - ) - .with_field("method_signature") - .with_received(format!("0x{}", hex::encode(method_sig))) - .with_expected("0xb61d27f6 (execute) or 0x18dfeb3c (executeBatch)"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Call data is not an OmniAccount execute or executeBatch function call", + ) + .with_field("method_signature") + .with_received(format!("0x{}", hex::encode(method_sig))) + .with_expected("0xb61d27f6 (execute) or 0x18dfeb3c (executeBatch)") + .to_rpc_error()); } } /// Parse single execute(address,uint256,bytes) call -fn parse_single_execute(call_bytes: &[u8]) -> Result<(Address, String), PumpxRpcError> { +fn parse_single_execute(call_bytes: &[u8]) -> RpcResult<(Address, String)> { // Minimum length check: method(4) + target(32) + value(32) + data_offset(32) = 100 bytes if call_bytes.len() < 100 { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short for OmniAccount execute call", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected( - "100 bytes minimum (method + target + value + data_offset + data_length)", - ), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short for OmniAccount execute call", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("100 bytes minimum (method + target + value + data_offset + data_length)") + .to_rpc_error()); } // Extract target address (bytes 16-36, last 20 bytes of the first 32-byte parameter) @@ -363,15 +327,14 @@ fn parse_single_execute(call_bytes: &[u8]) -> Result<(Address, String), PumpxRpc // Skip method(4) + target(32) + value(32) + data_offset(32) = 100 bytes to get to data length if call_bytes.len() < 132 { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short to contain data length", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected("132 bytes minimum (method + params + data_length)"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short to contain data length", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("132 bytes minimum (method + params + data_length)") + .to_rpc_error()); } // Extract data length (bytes 100-132) @@ -386,15 +349,14 @@ fn parse_single_execute(call_bytes: &[u8]) -> Result<(Address, String), PumpxRpc // Extract the actual inner calldata let data_start = 132; if call_bytes.len() < data_start + data_length { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short for declared data length", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected(format!("{} bytes", data_start + data_length)), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short for declared data length", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected(format!("{} bytes", data_start + data_length)) + .to_rpc_error()); } let inner_data = &call_bytes[data_start..data_start + data_length]; @@ -404,18 +366,17 @@ fn parse_single_execute(call_bytes: &[u8]) -> Result<(Address, String), PumpxRpc } /// Parse executeBatch((address,uint256,bytes)[]) call -fn parse_execute_batch(call_bytes: &[u8]) -> Result, PumpxRpcError> { +fn parse_execute_batch(call_bytes: &[u8]) -> RpcResult> { // Minimum length: method(4) + array_offset(32) + array_length(32) = 68 bytes if call_bytes.len() < 68 { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Call data too short for executeBatch call", - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected("68 bytes minimum"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Call data too short for executeBatch call", + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected("68 bytes minimum") + .to_rpc_error()); } // Skip method signature (4 bytes) and array offset (32 bytes) to get array length @@ -435,15 +396,14 @@ fn parse_execute_batch(call_bytes: &[u8]) -> Result, Pump for i in 0..array_length { // Each struct entry is at least 96 bytes (target + value + data_offset) if current_pos + 96 > call_bytes.len() { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - format!("Call data too short for batch entry {}", i), - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected(format!("{} bytes minimum", current_pos + 96)), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + format!("Call data too short for batch entry {}", i), + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected(format!("{} bytes minimum", current_pos + 96)) + .to_rpc_error()); } // Extract target address (last 20 bytes of the 32-byte slot) @@ -464,15 +424,14 @@ fn parse_execute_batch(call_bytes: &[u8]) -> Result, Pump // The offset is relative to the start of the current Call struct let data_length_pos = current_pos + relative_data_offset; if data_length_pos + 32 > call_bytes.len() { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - format!("Call data too short for batch entry {} data length", i), - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected(format!("{} bytes minimum", data_length_pos + 32)), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + format!("Call data too short for batch entry {} data length", i), + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected(format!("{} bytes minimum", data_length_pos + 32)) + .to_rpc_error()); } // Extract data length @@ -487,15 +446,14 @@ fn parse_execute_batch(call_bytes: &[u8]) -> Result, Pump // Extract the actual data let data_start = data_length_pos + 32; if data_start + data_length > call_bytes.len() { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - format!("Call data too short for batch entry {} data", i), - ) - .with_field("call_data_length") - .with_received(call_bytes.len().to_string()) - .with_expected(format!("{} bytes minimum", data_start + data_length)), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + format!("Call data too short for batch entry {} data", i), + ) + .with_field("call_data_length") + .with_received(call_bytes.len().to_string()) + .with_expected(format!("{} bytes minimum", data_start + data_length)) + .to_rpc_error()); } let inner_data = &call_bytes[data_start..data_start + data_length]; @@ -515,18 +473,17 @@ fn parse_execute_batch(call_bytes: &[u8]) -> Result, Pump fn validate_backend_calldata( user_operations: &[SerializablePackedUserOperation], chain_id: ChainId, -) -> Result<(), PumpxRpcError> { +) -> RpcResult<()> { for (index, user_op) in user_operations.iter().enumerate() { // Extract the target addresses and inner calldata from the execute()/executeBatch() calldata let execute_params = extract_execute_params_from_calldata(&user_op.call_data).map_err(|_e| { - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Failed to parse OmniAccount execute calldata", - ) - .with_field(format!("user_operations[{}].call_data", index)), + DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Failed to parse OmniAccount execute calldata", ) + .with_field(format!("user_operations[{}].call_data", index)) + .to_rpc_error() })?; // Validate each execute call in the batch (or single call) @@ -541,18 +498,17 @@ fn validate_backend_calldata( }; if *target_address != expected_usdc.parse::
().unwrap() { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "For Arbitrum backend requests, target contract must be USDC", - ) - .with_field(format!( - "user_operations[{}].call[{}] target address", - index, call_index - )) - .with_received(format!("{:?}", target_address)) - .with_expected(expected_usdc), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "For Arbitrum backend requests, target contract must be USDC", + ) + .with_field(format!( + "user_operations[{}].call[{}] target address", + index, call_index + )) + .with_received(format!("{:?}", target_address)) + .with_expected(expected_usdc) + .to_rpc_error()); } validate_arbitrum_usdc_transfer(inner_calldata, chain_id)?; @@ -560,29 +516,30 @@ fn validate_backend_calldata( HYPEREVM_MAINNET | HYPEREVM_TESTNET => { // For HyperEVM: target must be core writer and calldata must have valid action_id if *target_address != HYPEREVM_CORE_WRITER_ADDRESS.parse::
().unwrap() { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "For HyperEVM backend requests, target contract must be core writer", - ) - .with_field(format!("user_operations[{}].call[{}] target address", index, call_index)) - .with_received(format!("{:?}", target_address)) - .with_expected(HYPEREVM_CORE_WRITER_ADDRESS), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "For HyperEVM backend requests, target contract must be core writer", + ) + .with_field(format!( + "user_operations[{}].call[{}] target address", + index, call_index + )) + .with_received(format!("{:?}", target_address)) + .with_expected(HYPEREVM_CORE_WRITER_ADDRESS) + .to_rpc_error()); } validate_hyperevm_core_writer(inner_calldata, chain_id)?; }, _ => { - return Err(PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Chain ID not supported for backend wallet_index validation", - ) - .with_field("chain_id") - .with_received(chain_id.to_string()) - .with_expected("42161, 421614, 999, or 998"), - )); + return Err(DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Chain ID not supported for backend wallet_index validation", + ) + .with_field("chain_id") + .with_received(chain_id.to_string()) + .with_expected("42161, 421614, 999, or 998") + .to_rpc_error()); }, } } @@ -598,23 +555,14 @@ pub fn register_submit_user_op_with_auth< ) { module .register_async_method("omni_submitUserOpWithAuth", |params, ctx, _ext| async move { - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse request parameters", - ) - .with_reason(format!("Invalid JSON structure: {}", e)), - ) - })?; + let params = parse_rpc_params::(params)?; debug!("Received omni_submitUserOpWithAuth, params: {:?}", params); // Validate common parameters - validate_chain_id(params.chain_id as u32, Some("evm")).map_err(PumpxRpcError::from)?; - validate_wallet_index(params.wallet_index).map_err(PumpxRpcError::from)?; - validate_user_operations(¶ms.user_operations).map_err(PumpxRpcError::from)?; + validate_chain_id(params.chain_id as u32)?; + validate_wallet_index(params.wallet_index)?; + validate_user_operations(¶ms.user_operations)?; let main_address = match ¶ms.client_auth { ClientAuth::WildmetaHl { @@ -627,32 +575,15 @@ pub fn register_submit_user_op_with_auth< verify_wildmeta_signature_wrapper(agent_address, business_json, signature)?; let business_data: serde_json::Value = serde_json::from_str(business_json) - .map_err(|e| { - error!("Failed to parse business_json: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse business JSON", - ) - .with_field("business_json") - .with_reason(format!("JSON parse error: {}", e)), - ) - })?; + .map_err_internal("Failed to parse business_json")?; let timestamp = business_data .get("timestamp") .and_then(|v| v.as_u64()) .ok_or_else(|| { error!("Missing timestamp in business_json"); - PumpxRpcError::from( - DetailedError::new( - crate::error_code::MISSING_REQUIRED_FIELD_CODE, - "Missing required field in business JSON", - ) - .with_field("timestamp") - .with_expected("Unix timestamp as number") - .with_reason("Business JSON must contain a 'timestamp' field"), - ) + DetailedError::invalid_params("business_json", "missing timestamp") + .to_rpc_error() })?; verify_payload_timestamp_wrapper( @@ -665,30 +596,22 @@ pub fn register_submit_user_op_with_auth< .wildmeta_api .verify_hyperliquid_link(agent_address, main_address, *login_type) .await - .map_err(|e| { - error!("Failed to verify hyperliquid link: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - crate::error_code::EXTERNAL_API_ERROR_CODE, - "Failed to verify Hyperliquid account link", - ) - .with_field("operation") - .with_received("verify_hyperliquid_link") - .with_reason("Could not verify agent and main address linkage"), - ) + .map_err(|_| { + let msg = "Failed to verify hyperliquid link"; + error!(msg); + DetailedError::wildmeta_service_error("verify_hyperliquid_link") + .to_rpc_error() })?; if !linked { error!("Agent and main addresses are not linked"); - return Err(PumpxRpcError::from( - DetailedError::new( + return Err(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, "Agent and main addresses are not linked", ) .with_field("agent_address") .with_received(agent_address.to_string()) - .with_suggestion("Ensure the agent address is properly linked to the main address"), - )); + .with_suggestion("Ensure the agent address is properly linked to the main address").to_rpc_error()); } Some(main_address.clone()) @@ -700,16 +623,14 @@ pub fn register_submit_user_op_with_auth< "WildmetaBackend requires wallet_index to be 0 or 1, got: {}", params.wallet_index ); - return Err(PumpxRpcError::from( - DetailedError::new( + return Err(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, "Invalid wallet index for WildmetaBackend authentication", ) .with_field("wallet_index") .with_received(params.wallet_index.to_string()) .with_expected("0 or 1") - .with_suggestion("WildmetaBackend authentication requires wallet_index to be 0 or 1"), - )); + .with_suggestion("WildmetaBackend authentication requires wallet_index to be 0 or 1").to_rpc_error()); } // Additional validation for wallet_index == 0 @@ -723,25 +644,21 @@ pub fn register_submit_user_op_with_auth< "WildmetaBackend requires client_id to be 'wildmeta', got: {}", params.client_id ); - return Err(PumpxRpcError::from( - DetailedError::new( + return Err(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, "Invalid client ID for WildmetaBackend authentication", ) .with_field("client_id") .with_received(params.client_id.clone()) .with_expected("wildmeta") - .with_suggestion("WildmetaBackend authentication requires client_id to be 'wildmeta'"), - )); + .with_suggestion("WildmetaBackend authentication requires client_id to be 'wildmeta'").to_rpc_error()); } // Get entry point address for the chain let entry_point_client = ctx.entry_point_clients.get(¶ms.chain_id).ok_or_else(|| { error!("No entry point client found for chain_id: {}", params.chain_id); - PumpxRpcError::from( - DetailedError::chain_not_supported(params.chain_id), - ) + DetailedError::invalid_chain_id(params.chain_id).to_rpc_error() })?; let entry_point_address = entry_point_client.entry_point_address(); @@ -758,30 +675,13 @@ pub fn register_submit_user_op_with_auth< None }, _ => { - error!("Invalid client auth type"); - return Err(PumpxRpcError::from( - DetailedError::new( - PARSE_ERROR_CODE, - "Invalid client authentication type", - ) - .with_field("client_auth") - .with_expected("WildmetaHl or WildmetaBackend") - .with_suggestion("Use a supported authentication method"), - )); + let msg = "Invalid client auth type"; + error!(msg); + return Err(DetailedError::parse_error(msg).to_rpc_error()); }, }; - let identity = Identity::try_from(params.user_id.clone()).map_err(|e| { - error!("Failed to convert UserId to Identity: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - crate::error_code::ACCOUNT_PARSE_ERROR_CODE, - "Invalid user identity format", - ) - .with_field("user_id") - .with_reason(format!("Failed to parse user identity: {}", e)), - ) - })?; + let identity = Identity::try_from(params.user_id.clone()).map_err_parse("Failed to convert user_id to identity")?; // Only validate main_address if it's provided (not None) if let Some(main_addr) = &main_address { @@ -790,22 +690,20 @@ pub fn register_submit_user_op_with_auth< if let UserId::Evm(user_address) = ¶ms.user_id { if user_address.to_lowercase() != main_addr.to_lowercase() { error!("Main address does not match user_id for EVM identity"); - return Err(PumpxRpcError::from( - DetailedError::new( + return Err(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, "User address does not match authenticated main address for EVM identity", ) .with_field("user_address") .with_received(user_address.to_string()) .with_expected(main_addr.to_string()) - .with_suggestion("For EVM identity, the user_id must match the authenticated main address"), - )); + .with_suggestion("For EVM identity, the user_id must match the authenticated main address").to_rpc_error()); } } }, _ => { let omni_account = identity.to_omni_account(¶ms.client_id); - let derived_pubkey = ctx + let derived_address = ctx .signer_client .request_wallet( ChainType::Evm, @@ -813,40 +711,21 @@ pub fn register_submit_user_op_with_auth< *omni_account.as_ref(), ) .await - .map_err(|e| { - error!("Failed to derive EVM address: {:?}", e); - PumpxRpcError::from( - DetailedError::signer_service_error( - "request_wallet", - &format!("Failed to derive wallet: {:?}", e), - ), - ) - })?; - let derived_address = pubkey_to_address(ChainType::Evm, &derived_pubkey) - .map_err(|e| { - error!("Failed to convert derived pubkey to address: {:?}", e); - PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Failed to convert derived public key to address", - ) - .with_field("operation") - .with_received("pubkey_to_address conversion") - .with_reason(format!("Internal error converting public key to address: {:?}", e)), - ) - })?; + .map_err(|_| { + DetailedError::signer_service_error().to_rpc_error() + }) + .and_then(|pk| pubkey_to_address(ChainType::Evm, &pk).map_err_internal("Failed to convert pubkey to address"))?; + if derived_address.to_lowercase() != main_addr.to_lowercase() { error!("Main address does not match derived EVM address"); - return Err(PumpxRpcError::from( - DetailedError::new( + return Err(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, "Derived address does not match authenticated address", ) .with_field("derived_address") .with_received(derived_address.to_string()) .with_expected(main_addr.to_string()) - .with_suggestion("The derived EVM address must match the authenticated main address"), - )); + .with_suggestion("The derived EVM address must match the authenticated main address").to_rpc_error()); } }, } @@ -854,265 +733,32 @@ pub fn register_submit_user_op_with_auth< let account_id = identity.to_omni_account(¶ms.client_id); - // Validate each operation's sender address (already done by validate_user_operations) - // Additional validation for address format if needed - for (index, op) in params.user_operations.iter().enumerate() { - op.sender.parse::
().map_err(|e| { - error!("Invalid sender address '{}': {}", op.sender, e); - PumpxRpcError::from( - DetailedError::invalid_address_format( - &format!("user_operations[{}].sender", index), - &op.sender, - "0x-prefixed 20-byte Ethereum address (40 hex chars)", - ), - ) - })?; - } - - // Inlined handler logic from handle_submit_user_op - info!( - "Processing SubmitUserOp for {} UserOperations on chain_id: {}", - params.user_operations.len(), - params.chain_id - ); - - // Get EntryPoint client for this chain (needed for both signing and submission) - let entry_point_client = ctx.entry_point_clients.get(¶ms.chain_id).ok_or_else(|| { - error!("No EntryPoint client configured for chain_id: {}", params.chain_id); - PumpxRpcError::from(DetailedError::chain_not_supported(params.chain_id)) - })?; - - // Parse whitelisted paymasters once - let whitelisted_paymaster = parse_whitelisted_paymasters(); - - // Process each UserOperation in the batch - let mut aa_user_ops = Vec::new(); - - for (index, serializable_user_op) in params.user_operations.iter().enumerate() { - // Convert SerializablePackedUserOperation to PackedUserOperation - let mut packed_user_op = convert_to_packed_user_op(serializable_user_op.clone()) - .map_err(|e| { - error!("Failed to convert UserOperation {}: {}", index, e); - PumpxRpcError::from(DetailedError::invalid_user_operation_error(&format!( - "Invalid user operation at index {}", - index - ))) - })?; - - // Check userOp signature status and validate paymaster usage - if packed_user_op.signature.is_empty() { - // UNSIGNED userOp: If paymaster specified, must be whitelisted - if !packed_user_op.paymasterAndData.is_empty() { - if let Some(paymaster_address) = - extract_paymaster_address(&packed_user_op.paymasterAndData) - { - if !is_whitelisted_paymaster(&paymaster_address, &whitelisted_paymaster) - { - error!( - "UserOperation {} uses non-whitelisted paymaster {}. Only whitelisted paymasters are allowed for unsigned userOps.", - index, paymaster_address - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "UserOperation at index {} uses non-whitelisted paymaster {}", - index, paymaster_address - )), - )); - } - } - - match process_erc20_paymaster_data( - ctx.binance_api_client.as_ref() as &dyn BinancePaymasterApi, - &packed_user_op.paymasterAndData, - params.chain_id, - ) - .await - { - Ok(Some(updated_paymaster_data)) => { - packed_user_op.paymasterAndData = updated_paymaster_data; - info!("Updated ERC20 paymaster data for UserOperation {}", index); - }, - Ok(None) => { - // Not an ERC20 paymaster, continue as normal - debug!("UserOperation {} does not use ERC20 paymaster", index); - }, - Err(e) => { - error!( - "Failed to process ERC20 paymaster data for UserOperation {}: {}", - index, e - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "ERC20 paymaster processing failed for operation at index {}: {}", - index, e - )), - )); - }, - } - } - - info!("Requesting signature from pumpx signer for UserOperation {}", index); - - // Log UserOp details for debugging - info!( - "UserOp details - Sender: {}, Nonce: {}, InitCode length: {}, CallData length: {}", - packed_user_op.sender, - packed_user_op.nonce, - packed_user_op.initCode.len(), - packed_user_op.callData.len() - ); - - let entry_point_address = entry_point_client.entry_point_address(); - - let user_op_hash_bytes = calculate_user_operation_hash( - &packed_user_op, - entry_point_address, - params.chain_id, - ); - let message_to_sign = user_op_hash_bytes.to_vec(); - - info!( - "Signing UserOp hash: 0x{}, EntryPoint: {}, ChainID: {}", - hex::encode(user_op_hash_bytes), - entry_point_address, - params.chain_id - ); - - // Request signature from pumpx signer for EVM chain - let signature_result = ctx - .signer_client - .request_signature( - ChainType::Evm, - params.wallet_index, - account_id.clone().into(), - message_to_sign, - ) - .await; - - let signature = match signature_result { - Ok(sig) => substrate_to_ethereum_signature(&sig) - .map_err(|e| { - error!("Failed to convert signature: {}", e); - PumpxRpcError::from(DetailedError::signature_service_unavailable()) - })? - .to_vec(), - Err(_) => { - error!("Failed to sign user operation {}", index); - return Err(PumpxRpcError::from( - DetailedError::signature_service_unavailable(), - )); - }, - }; - - // Prepend 0x01 byte to indicate Root signature type (according to UserOpSigner enum) - let mut signature_with_prefix: Vec = vec![0x01]; - signature_with_prefix.extend_from_slice(&signature); - packed_user_op.signature = Bytes::from(signature_with_prefix); - info!("UserOperation {} signed successfully", index); - } else { - // SIGNED userOp: Only allowed if no paymaster specified - if !packed_user_op.paymasterAndData.is_empty() { - error!( - "UserOperation {} is signed but has paymaster data. Signed userOps are only allowed without paymaster.", - index - ); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&format!( - "UserOperation at index {} is signed but specifies a paymaster", - index - )), - )); - } - info!("UserOperation {} is signed with no paymaster, processing", index); - } - - // Convert to aa_contracts_client::PackedUserOperation for EntryPoint call - let aa_user_op = aa_contracts_client::PackedUserOperation { - sender: packed_user_op.sender, - nonce: packed_user_op.nonce, - initCode: packed_user_op.initCode.clone(), - callData: packed_user_op.callData.clone(), - accountGasLimits: packed_user_op.accountGasLimits, - preVerificationGas: packed_user_op.preVerificationGas, - gasFees: packed_user_op.gasFees, - paymasterAndData: packed_user_op.paymasterAndData.clone(), - signature: packed_user_op.signature.clone(), - }; - aa_user_ops.push(aa_user_op); - } - - // Get beneficiary address from the EntryPoint client's wallet - let beneficiary = entry_point_client.get_wallet_address().await.map_err(|_| { - let err_msg = "Failed to get wallet address from EntryPoint client".to_string(); - error!("{}", err_msg.clone()); - PumpxRpcError::from_code_and_message( - crate::error_code::INTERNAL_ERROR_CODE, - err_msg, - ) - })?; - - // Run batch simulation for all UserOperations before submission - info!("Running batch simulation for {} UserOperations", aa_user_ops.len()); - match entry_point_client.simulate_handle_ops(&aa_user_ops, beneficiary).await { - Ok(simulation_results) => { - for (index, result) in simulation_results.iter().enumerate() { - info!( - "UserOperation {} simulation successful. PreOpGas: {}, Paid: {}, AccountValidation: {}, PaymasterValidation: {}", - index, - result.preOpGas, - result.paid, - result.accountValidationData, - result.paymasterValidationData - ); - } - info!("All {} UserOperations passed batch simulation checks", aa_user_ops.len()); - }, - Err(e) => { - let err_msg: String = format!("Batch UserOperation simulation failed: {}", e); - error!("{}", err_msg.clone()); - return Err(PumpxRpcError::from( - DetailedError::invalid_user_operation_error(&err_msg), - )); - }, - } - - // Submit all UserOperations via EntryPoint.handleOps() with retry logic - let transaction_hash = - match entry_point_client.handle_ops_with_retry(&aa_user_ops, beneficiary).await { - Ok(tx_hash) => { - // Return the actual transaction hash from handle_ops - Some(tx_hash) - }, - Err(_) => { - let err_msg = - "Failed to submit UserOperations to EntryPoint via handleOps after retries" - .to_string(); - error!("{}", err_msg.clone()); - return Err(PumpxRpcError::from_code_and_message( - crate::error_code::INTERNAL_ERROR_CODE, - err_msg, - )); - }, - }; + // Call the common submission logic + let transaction_hash = submit_user_ops( + &ctx, + params.user_operations, + params.chain_id, + params.wallet_index, + &account_id, + ).await?; Ok(SubmitUserOpWithAuthResponse { transaction_hash }) }) .expect("Failed to register omni_submitUserOpWithAuth method"); } -// Wrapper functions to convert shared function return types to PumpxRpcError +// Wrapper functions to convert shared function return types to DetailedError fn verify_wildmeta_signature_wrapper( agent_address: &str, business_json: &str, signature: &str, -) -> Result<(), PumpxRpcError> { - verify_wildmeta_signature(agent_address, business_json, signature).map_err(|err| { - PumpxRpcError::from( - DetailedError::new(err.code(), "Wildmeta signature verification failed") - .with_field("agent_address") - .with_received(agent_address.to_string()) - .with_suggestion("Ensure the signature is valid and matches the agent address"), - ) +) -> RpcResult<()> { + verify_wildmeta_signature(agent_address, business_json, signature).map_err(|e| { + DetailedError::new(e.code(), "Wildmeta signature verification failed") + .with_field("agent_address") + .with_received(agent_address.to_string()) + .with_suggestion("Ensure the signature is valid and matches the agent address") + .to_rpc_error() }) } @@ -1120,14 +766,13 @@ fn verify_payload_timestamp_wrapper( storage: &Arc, main_address: &str, new_timestamp: u64, -) -> Result<(), PumpxRpcError> { - verify_payload_timestamp(storage, main_address, new_timestamp).map_err(|err| { - PumpxRpcError::from( - DetailedError::new(err.code(), "Timestamp verification failed") - .with_field("timestamp") - .with_received(new_timestamp.to_string()) - .with_suggestion("Timestamp must be greater than the previously used timestamp"), - ) +) -> RpcResult<()> { + verify_payload_timestamp(storage, main_address, new_timestamp).map_err(|e| { + DetailedError::new(e.code(), "Timestamp verification failed") + .with_field("timestamp") + .with_received(new_timestamp.to_string()) + .with_suggestion("Timestamp must be greater than the previously used timestamp") + .to_rpc_error() }) } @@ -1137,7 +782,7 @@ fn verify_wildmeta_backend_signature_wrapper( chain_id: ChainId, entry_point_address: Address, expected_pubkey: &[u8; 33], -) -> Result<(), PumpxRpcError> { +) -> RpcResult<()> { verify_wildmeta_backend_signature( signature, user_operations, @@ -1145,476 +790,20 @@ fn verify_wildmeta_backend_signature_wrapper( entry_point_address, expected_pubkey, ) - .map_err(|err| { - PumpxRpcError::from( - DetailedError::new(err.code(), "Backend signature verification failed") - .with_field("signature") - .with_suggestion("Ensure the backend signature is valid for the given operations"), - ) + .map_err(|e| { + DetailedError::new(e.code(), "Backend signature verification failed") + .with_field("signature") + .with_suggestion("Ensure the backend signature is valid for the given operations") + .to_rpc_error() }) } #[cfg(test)] mod tests { use super::*; - use crate::utils::user_op::convert_to_packed_user_op; - use executor_storage::StorageDB; - use tempfile::tempdir; - - #[test] - fn test_verify_wildmeta_signature_with_real_data() { - let business_json = r#"{"action":"trade","amount":1.5,"customField1":"buy","customField2":"market","leverage":10,"metadata":{"features":{"darkMode":true,"notifications":false},"userAgent":"mobile-app","version":"1.0.0"},"positions":[{"entryPrice":50000,"metadata":{"openTime":1640995200,"strategy":"momentum"},"side":"long","size":1.5,"symbol":"BTC/USD"},{"entryPrice":3000,"metadata":{"openTime":1640995300,"strategy":"reversal"},"side":"short","size":2,"symbol":"ETH/USD"}],"price":50000,"riskManagement":{"maxLeverage":20,"stopLoss":{"enabled":true,"percentage":0.05},"takeProfit":{"enabled":true,"percentage":0.1}},"slippage":0.01,"symbol":"BTC/USD","timestamp":1752573555}"#; - let signature = "0x46c737250d61b60cbf0f46a6755e59815844a2f7cdb9dc16bf867b57bfed3526424343a237c15eef9089d571d1f60fd0bd7f91d5888c649216a7df147b386a681c"; - let agent_address = "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd"; - - let result = verify_wildmeta_signature_wrapper(agent_address, business_json, signature); - assert!(result.is_ok(), "Signature verification should succeed"); - } - - #[test] - fn test_verify_wildmeta_signature_invalid_signature() { - let business_json = r#"{"action":"trade","timestamp":1752573555}"#; - let signature = "0x020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - let agent_address = "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd"; - - let result = verify_wildmeta_signature_wrapper(agent_address, business_json, signature); - assert!(result.is_err(), "Should fail with invalid signature"); - } - - #[test] - fn test_verify_wildmeta_signature_wrong_signer() { - let business_json = r#"{"action":"trade","amount":1.5,"customField1":"buy","customField2":"market","leverage":10,"metadata":{"features":{"darkMode":true,"notifications":false},"userAgent":"mobile-app","version":"1.0.0"},"positions":[{"entryPrice":50000,"metadata":{"openTime":1640995200,"strategy":"momentum"},"side":"long","size":1.5,"symbol":"BTC/USD"},{"entryPrice":3000,"metadata":{"openTime":1640995300,"strategy":"reversal"},"side":"short","size":2,"symbol":"ETH/USD"}],"price":50000,"riskManagement":{"maxLeverage":20,"stopLoss":{"enabled":true,"percentage":0.05},"takeProfit":{"enabled":true,"percentage":0.1}},"slippage":0.01,"symbol":"BTC/USD","timestamp":1752573555}"#; - - let signature = "0x46c737250d61b60cbf0f46a6755e59815844a2f7cdb9dc16bf867b57bfed3526424343a237c15eef9089d571d1f60fd0bd7f91d5888c649216a7df147b386a681c"; - // Use a different address than the actual signer - let wrong_agent_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; - - let result = verify_wildmeta_signature(wrong_agent_address, business_json, signature); - assert!(result.is_err(), "Should fail with wrong signer address"); - } - - #[test] - fn test_verify_wildmeta_signature_invalid_hex() { - let business_json = r#"{"timestamp":1752573555}"#; - let signature = "invalid_hex"; - let agent_address = "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd"; - - let result = verify_wildmeta_signature_wrapper(agent_address, business_json, signature); - assert!(result.is_err(), "Should fail with invalid hex signature"); - } - - #[test] - fn test_parse_business_json_timestamp() { - let business_json = r#"{"action":"trade","timestamp":1752573555}"#; - - let parsed: serde_json::Value = serde_json::from_str(business_json).unwrap(); - let timestamp = parsed.get("timestamp").and_then(|v| v.as_u64()); - - assert_eq!(timestamp, Some(1752573555), "Should correctly parse timestamp"); - } - - #[test] - fn test_parse_business_json_missing_timestamp() { - let business_json = r#"{"action":"trade","amount":1.5}"#; - - let parsed: serde_json::Value = serde_json::from_str(business_json).unwrap(); - let timestamp = parsed.get("timestamp").and_then(|v| v.as_u64()); - - assert_eq!(timestamp, None, "Should return None for missing timestamp"); - } - - #[test] - fn test_verify_payload_timestamp_success() { - let tmp_dir = tempdir().unwrap(); - let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); - let storage = Arc::new(WildmetaTimestampStorage::new(db)); - - let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; - - // First timestamp should succeed - let result = verify_payload_timestamp_wrapper(&storage, main_address, 1000); - assert!(result.is_ok(), "First timestamp should succeed"); - - // Higher timestamp should succeed - let result = verify_payload_timestamp_wrapper(&storage, main_address, 2000); - assert!(result.is_ok(), "Higher timestamp should succeed"); - } - - #[test] - fn test_verify_payload_timestamp_fails_with_old_timestamp() { - let tmp_dir = tempdir().unwrap(); - let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); - let storage = Arc::new(WildmetaTimestampStorage::new(db)); - - let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; - - // Store initial timestamp - let result = verify_payload_timestamp_wrapper(&storage, main_address, 1000); - assert!(result.is_ok()); - - // Same timestamp should fail - let result = verify_payload_timestamp_wrapper(&storage, main_address, 1000); - assert!(result.is_err(), "Same timestamp should fail"); - - // Lower timestamp should fail - let result = verify_payload_timestamp_wrapper(&storage, main_address, 500); - assert!(result.is_err(), "Lower timestamp should fail"); - } - - #[test] - fn test_verify_payload_timestamp_first_time() { - let tmp_dir = tempdir().unwrap(); - let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); - let storage = Arc::new(WildmetaTimestampStorage::new(db)); - - let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; - - // Any timestamp should succeed for first time - let result = verify_payload_timestamp_wrapper(&storage, main_address, 1); - assert!(result.is_ok(), "First timestamp should succeed even if it's 1"); - } - - #[test] - fn test_verify_payload_timestamp_persistence() { - let tmp_dir = tempdir().unwrap(); - let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); - let storage = Arc::new(WildmetaTimestampStorage::new(db)); - - let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; - - // Store timestamp - verify_payload_timestamp_wrapper(&storage, main_address, 1000).unwrap(); - - // Verify it's persisted by checking that lower timestamp fails - let result = verify_payload_timestamp_wrapper(&storage, main_address, 999); - assert!(result.is_err(), "Timestamp should be persisted"); - - // Verify exact stored value fails - let result = verify_payload_timestamp_wrapper(&storage, main_address, 1000); - assert!(result.is_err(), "Exact stored timestamp should fail"); - - // Higher should succeed - let result = verify_payload_timestamp_wrapper(&storage, main_address, 1001); - assert!(result.is_ok(), "Higher timestamp should succeed"); - } - - #[test] - fn test_wildmeta_backend_auth_parsing() { - use executor_primitives::ClientAuth; - - let json = - r#"{"type": "wildmeta_backend", "value": { "signature": "0x1234567890abcdef" }}"#; - let deserialized: ClientAuth = serde_json::from_str(json).unwrap(); - - match deserialized { - ClientAuth::WildmetaBackend { signature } => { - assert_eq!(signature, "0x1234567890abcdef"); - }, - _ => panic!("Expected WildmetaBackend variant"), - } - } - - #[test] - fn test_verify_wildmeta_backend_signature_wrapper_invalid_signature() { - use executor_core::types::SerializablePackedUserOperation; - - // Create a test user operation - let user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0x".to_string(), - call_data: "0x".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: None, - }; - - let invalid_signature = "0x020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - let chain_id = 1; - let entry_point_address = Address::from([0u8; 20]); - let expected_pubkey = [0u8; 33]; - - let result = verify_wildmeta_backend_signature_wrapper( - invalid_signature, - &[user_op], - chain_id, - entry_point_address, - &expected_pubkey, - ); - - assert!(result.is_err(), "Should fail with invalid signature"); - } - - #[test] - fn test_wildmeta_backend_validation_invalid_wallet_index() { - use executor_primitives::ClientAuth; - - let auth = ClientAuth::WildmetaBackend { signature: "0x1234567890abcdef".to_string() }; - - // Test with wallet_index != 1 should fail - // This would be tested in an integration test with actual RPC call - // For now we verify the auth variant is parsed correctly - match auth { - ClientAuth::WildmetaBackend { signature } => { - assert_eq!(signature, "0x1234567890abcdef"); - }, - _ => panic!("Expected WildmetaBackend variant"), - } - } - - #[test] - fn test_wildmeta_backend_validation_invalid_client_id() { - use executor_primitives::ClientAuth; - - let auth = ClientAuth::WildmetaBackend { signature: "0x1234567890abcdef".to_string() }; - - // Test with client_id != "wildmeta" should fail - // This would be tested in an integration test with actual RPC call - // For now we verify the auth variant is parsed correctly - match auth { - ClientAuth::WildmetaBackend { signature } => { - assert_eq!(signature, "0x1234567890abcdef"); - }, - _ => panic!("Expected WildmetaBackend variant"), - } - } - - #[test] - fn test_wildmeta_backend_valid_signature_verification() { - use aa_contracts_client::calculate_user_operation_hash; - use alloy::primitives::{keccak256, Address}; - use executor_core::types::SerializablePackedUserOperation; - use executor_crypto::secp256k1::{ - secp256k1_ecdsa_recover_compressed, secp256k1_ecdsa_sign, - }; - - // Create a test private key (32 bytes) - let private_key: [u8; 32] = [ - 0x47, 0xf7, 0x8f, 0x59, 0x81, 0x2d, 0x6d, 0x1f, 0x2c, 0x8a, 0x65, 0x04, 0x19, 0x0d, - 0x63, 0x7f, 0x34, 0x6c, 0x4b, 0x6f, 0x7d, 0x20, 0x45, 0x32, 0x15, 0x68, 0x91, 0x73, - 0xa2, 0xb8, 0xc9, 0xe4, - ]; - - // Create a test user operation - let user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0x".to_string(), - call_data: "0xabcdef".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: None, - }; - - let chain_id = 31337u64; // Local test chain - let entry_point_address = Address::from([0u8; 20]); - - let mut combined_hash_data = Vec::new(); - let packed_user_op = convert_to_packed_user_op(user_op.clone()).unwrap(); - let user_op_hash = - calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); - combined_hash_data.extend_from_slice(&user_op_hash.0); - let combined_hash = keccak256(&combined_hash_data); - - // Sign the combined hash with our private key - let signature = match secp256k1_ecdsa_sign(&private_key, &combined_hash.0) { - Ok(sig) => sig, - Err(_) => panic!("Failed to sign with valid private key"), - }; - - // Derive the expected public key from the signature and hash - let expected_pubkey = match secp256k1_ecdsa_recover_compressed(&signature, &combined_hash.0) - { - Ok(pk) => pk, - Err(_) => panic!("Failed to recover pubkey from valid signature"), - }; - - // Convert signature to hex string with 0x prefix - let signature_hex = format!("0x{}", hex::encode(signature)); - - // Test the verification function - let result = verify_wildmeta_backend_signature_wrapper( - &signature_hex, - &[user_op], - chain_id, - entry_point_address, - &expected_pubkey, - ); - - assert!(result.is_ok(), "Valid signature verification should succeed"); - } - - #[test] - fn test_wildmeta_backend_multiple_operations_signature_verification() { - use aa_contracts_client::calculate_user_operation_hash; - use alloy::primitives::{keccak256, Address}; - use executor_core::types::SerializablePackedUserOperation; - use executor_crypto::secp256k1::{ - secp256k1_ecdsa_recover_compressed, secp256k1_ecdsa_sign, - }; - - let private_key: [u8; 32] = [ - 0x47, 0xf7, 0x8f, 0x59, 0x81, 0x2d, 0x6d, 0x1f, 0x2c, 0x8a, 0x65, 0x04, 0x19, 0x0d, - 0x63, 0x7f, 0x34, 0x6c, 0x4b, 0x6f, 0x7d, 0x20, 0x45, 0x32, 0x15, 0x68, 0x91, 0x73, - 0xa2, 0xb8, 0xc9, 0xe4, - ]; - - // Create multiple test user operations - let user_op1 = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0x".to_string(), - call_data: "0xabcdef".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: None, - }; - - let user_op2 = SerializablePackedUserOperation { - sender: "0x9876543210987654321098765432109876543210".to_string(), - nonce: 43, - init_code: "0x".to_string(), - call_data: "0x123456".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 22000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: None, - }; - - let user_operations = vec![user_op1.clone(), user_op2.clone()]; - let chain_id = 31337u64; - let entry_point_address = Address::from([0u8; 20]); - - // Calculate combined hash for all operations (mimic the new implementation) - let mut combined_hash_data = Vec::new(); - for user_op in &user_operations { - let packed_user_op = convert_to_packed_user_op(user_op.clone()).unwrap(); - let user_op_hash = - calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); - combined_hash_data.extend_from_slice(&user_op_hash.0); - } - let combined_hash = keccak256(&combined_hash_data); - - // Sign the combined hash - let signature = match secp256k1_ecdsa_sign(&private_key, &combined_hash.0) { - Ok(sig) => sig, - Err(_) => panic!("Failed to sign"), - }; - let expected_pubkey = match secp256k1_ecdsa_recover_compressed(&signature, &combined_hash.0) - { - Ok(pk) => pk, - Err(_) => panic!("Failed to recover pubkey"), - }; - let signature_hex = format!("0x{}", hex::encode(signature)); - - // Test with multiple operations - let result = verify_wildmeta_backend_signature_wrapper( - &signature_hex, - &user_operations, - chain_id, - entry_point_address, - &expected_pubkey, - ); - - assert!(result.is_ok(), "Multiple operations signature verification should succeed"); - - // Test that single operation would fail with same signature (different hash) - let single_op_result = verify_wildmeta_backend_signature_wrapper( - &signature_hex, - &[user_op1], - chain_id, - entry_point_address, - &expected_pubkey, - ); - - assert!(single_op_result.is_err(), "Single operation should fail with multi-op signature"); - } - - #[test] - fn test_wildmeta_backend_invalid_signature_wrong_key() { - use aa_contracts_client::calculate_user_operation_hash; - use alloy::primitives::{keccak256, Address}; - use executor_core::types::SerializablePackedUserOperation; - use executor_crypto::secp256k1::secp256k1_ecdsa_sign; - - // Create a test private key - let private_key: [u8; 32] = [ - 0x47, 0xf7, 0x8f, 0x59, 0x81, 0x2d, 0x6d, 0x1f, 0x2c, 0x8a, 0x65, 0x04, 0x19, 0x0d, - 0x63, 0x7f, 0x34, 0x6c, 0x4b, 0x6f, 0x7d, 0x20, 0x45, 0x32, 0x15, 0x68, 0x91, 0x73, - 0xa2, 0xb8, 0xc9, 0xe4, - ]; - - // Wrong expected public key (different from the actual signature) - let wrong_expected_pubkey: [u8; 33] = [ - 0x03, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, - 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfb, 0xa3, 0x72, 0xdd, 0x89, 0x6e, 0x17, 0xc8, 0x43, - 0x79, 0x1b, 0x19, 0x5f, 0x8d, - ]; - - // Create a test user operation - let user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0x".to_string(), - call_data: "0xabcdef".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: None, - }; - - let chain_id = 31337u64; - let entry_point_address = Address::from([0u8; 20]); - - // Calculate combined hash (mimic the new implementation) - let mut combined_hash_data = Vec::new(); - let packed_user_op = convert_to_packed_user_op(user_op.clone()).unwrap(); - let user_op_hash = - calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); - combined_hash_data.extend_from_slice(&user_op_hash.0); - let combined_hash = keccak256(&combined_hash_data); - - // Sign the combined hash with our private key - let signature = match secp256k1_ecdsa_sign(&private_key, &combined_hash.0) { - Ok(sig) => sig, - Err(_) => panic!("Failed to sign with valid private key"), - }; - let signature_hex = format!("0x{}", hex::encode(signature)); - - // Test with wrong expected public key - should fail - let result = verify_wildmeta_backend_signature_wrapper( - &signature_hex, - &[user_op], - chain_id, - entry_point_address, - &wrong_expected_pubkey, - ); - - assert!(result.is_err(), "Signature verification with wrong public key should fail"); - } #[test] fn test_validate_arbitrum_usdc_transfer_valid() { - // Valid USDC transfer calldata: transfer(address recipient, uint256 amount) - // Method signature: 0xa9059cbb // Recipient: ARB_TO_HYPER_BRIDGE_ADDRESS (padded to 32 bytes) // Amount: 1000000 (1 USDC with 6 decimals, padded to 32 bytes) let call_data = format!( diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/test_protected_method.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/test_protected_method.rs index c8437fd397..bb2876b299 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/test_protected_method.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/test_protected_method.rs @@ -1,7 +1,9 @@ -use crate::methods::omni::common::check_auth; use crate::server::RpcContext; +use crate::utils::omni::extract_omni_account; use executor_core::intent_executor::IntentExecutor; -use jsonrpsee::{types::ErrorObject, RpcModule}; +use executor_primitives::utils::hex::hex_encode; +use jsonrpsee::types::ErrorObjectOwned; +use jsonrpsee::RpcModule; #[cfg(test)] pub fn register_test_protected_method< @@ -11,8 +13,8 @@ pub fn register_test_protected_method< ) { module .register_method("omni_testProtectedMethod", |_, _, ext| { - if let Ok(omni_account) = check_auth(ext) { - return Ok::(omni_account); + if let Ok(omni_account) = extract_omni_account(ext) { + return Ok::(hex_encode(omni_account.as_ref())); } panic!("RpcExtensions not found in request extensions"); }) diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/transfer_widthdraw.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/transfer_widthdraw.rs index 38d661e882..82fe42b23c 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/transfer_widthdraw.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/transfer_widthdraw.rs @@ -1,13 +1,11 @@ -use super::common::check_omni_api_response; use crate::{ detailed_error::DetailedError, - error_code::*, - methods::omni::{common::check_auth, PumpxRpcError}, + methods::omni::check_backend_response, server::RpcContext, - utils::omni::to_omni_account, + utils::omni::extract_omni_account, utils::pumpx::verify_google_code, - validation_helpers::{ - validate_amount, validate_chain_id, validate_ethereum_address, validate_token_address, + utils::validation::{ + parse_rpc_params, validate_amount, validate_chain_id, validate_evm_address, validate_wallet_index, }, Deserialize, @@ -45,57 +43,25 @@ pub fn register_transfer_withdraw< ) { module .register_async_method("omni_transferWithdraw", |params, ctx, ext| async move { - let oa_str = check_auth(&ext).map_err(|e| { - error!("Authentication check failed: {:?}", e); - PumpxRpcError::from(DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication failed" - ) - .with_suggestion("Please provide valid authentication credentials")) - })?; + debug!("Received omni_transferWithdraw, params: {:?}", params); - let params = params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse request parameters" - ) - .with_reason(format!("Invalid JSON structure: {}", e))) - })?; + let params = parse_rpc_params::(params)?; + let omni_account = extract_omni_account(&ext)?; - debug!("Received omni_transferWithdraw, chain_id: {}, wallet_index: {}, recipient_address: {}, token_ca: {}, amount: {}", - params.chain_id, params.wallet_index, params.recipient_address, params.token_ca, params.amount); - - validate_chain_id(params.chain_id, Some("evm")).map_err(PumpxRpcError::from)?; - - validate_wallet_index(params.wallet_index).map_err(PumpxRpcError::from)?; - - validate_ethereum_address(¶ms.recipient_address, "recipient_address") - .map_err(PumpxRpcError::from)?; - - validate_token_address(¶ms.token_ca, "token_ca") - .map_err(PumpxRpcError::from)?; - - validate_amount(¶ms.amount, "amount") - .map_err(PumpxRpcError::from)?; - - let omni_account = to_omni_account(&oa_str).map_err(|_| { - PumpxRpcError::from(DetailedError::new( - PARSE_ERROR_CODE, - "Failed to parse omni account", - )) - })?; + validate_chain_id(params.chain_id)?; + validate_wallet_index(params.wallet_index)?; + validate_evm_address(¶ms.recipient_address, "recipient_address")?; + validate_evm_address(¶ms.token_ca, "token_ca")?; + validate_amount(¶ms.amount, "amount")?; // Inline handle_pumpx_transfer_withdraw logic // 1. Verify we have a valid Pumpx "access" token for the user let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); - let Ok(Some(access_token)) = storage.get(&(omni_account, AUTH_TOKEN_ACCESS_TYPE)) + let Ok(Some(access_token)) = + storage.get(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE)) else { error!("Failed to get access_token within TransferWidthdraw"); - return Err(PumpxRpcError::from(DetailedError::new( - INTERNAL_ERROR_CODE, - "Internal error" - ).with_reason("Failed to get access token"))); + return Err(DetailedError::storage_service_error("get access token").to_rpc_error()); }; // 2. Verify google code @@ -108,11 +74,9 @@ pub fn register_transfer_withdraw< .await; if !verify_success { - error!("Failed to verify google code within TransferWidthdraw"); - return Err(PumpxRpcError::from(DetailedError::new( - PUMPX_API_GOOGLE_CODE_VERIFICATION_FAILED_CODE, - "Google code verification failed" - ).with_suggestion("Please check your Google verification code and try again"))); + let msg = "Failed to verify google code within TransferWidthdraw"; + error!(msg); + return Err(DetailedError::internal_error(msg).to_rpc_error()); } // 3. Create a transfer tx and send to backend @@ -126,16 +90,17 @@ pub fn register_transfer_withdraw< }; debug!("Calling pumpx create_transfer_tx, body {:?}", body); - let response = ctx.pumpx_api.create_transfer_tx(&access_token, body, params.lang.clone()).await + let response = ctx + .pumpx_api + .create_transfer_tx(&access_token, body, params.lang.clone()) + .await .map_err(|e| { - error!("Failed to create transfer tx: {}", e); - PumpxRpcError::from(DetailedError::new( - PUMPX_API_CREATE_TRANSFER_TX_FAILED_CODE, - "Failed to create transfer transaction" - ).with_suggestion("Please check your transfer parameters and try again")) + error!("Failed to create transfer tx: {:?}", e); + DetailedError::pumpx_service_error("create_transfer_tx", format!("{:?}", e)) + .to_rpc_error() })?; - check_omni_api_response(response.clone(), "Transfer withdraw".into())?; + check_backend_response(&response, "transfer_withdraw")?; Ok(TransferWithdrawResponse { backend_response: response }) }) .expect("Failed to register omni_transferWithdraw method"); diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/user_login.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/user_login.rs index 6d3d19786f..090a400dba 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/user_login.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/user_login.rs @@ -1,6 +1,8 @@ -use super::common::check_omni_api_response; +use super::check_backend_response; use crate::{ - error_code::*, server::RpcContext, verify_auth::verify_auth, Deserialize, ErrorCode, Serialize, + detailed_error::DetailedError, error_code::*, server::RpcContext, + utils::validation::parse_rpc_params, verify_auth::verify_auth, Deserialize, ErrorCode, + Serialize, }; use chrono::{Days, Utc}; @@ -49,10 +51,7 @@ pub fn register_user_login().map_err(|e| { - error!("Failed to parse params: {:?}", e); - ErrorCode::ParseError - })?; + let params = parse_rpc_params::(params)?; let auth = OmniAuth::try_from(params.clone()).map_err(|e| { error!("Failed to convert params to OmniAuth: {:?}", e); ErrorCode::ParseError @@ -93,24 +92,26 @@ pub fn register_user_login(UserLoginResponse { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/verify_email_verification_code_test.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/verify_email_verification_code_test.rs index a5f61923f3..207439136e 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/verify_email_verification_code_test.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/verify_email_verification_code_test.rs @@ -1,7 +1,8 @@ use crate::detailed_error::DetailedError; -use crate::error_code::{AUTH_VERIFICATION_FAILED_CODE, INVALID_PARAMS_CODE, PARSE_ERROR_CODE}; -use crate::server::RpcContext; +use crate::error_code::AUTH_VERIFICATION_FAILED_CODE; +use crate::utils::validation::parse_rpc_params; use crate::verify_auth::verify_email_authentication; +use crate::RpcContext; use executor_core::intent_executor::IntentExecutor; use executor_primitives::{UserAuth, UserId}; use jsonrpsee::{types::ErrorObject, RpcModule}; @@ -23,22 +24,15 @@ pub fn register_verify_email_verification_code_test< .register_async_method( "omni_verifyEmailVerificationCodeTest", |params, ctx, _ext| async move { - let params = - params.parse::().map_err(|e| { - error!("Failed to parse params: {:?}", e); - DetailedError::new(PARSE_ERROR_CODE, "Parse error") - .with_reason("Invalid JSON format or missing required fields") - .to_error_object() - })?; + let params = parse_rpc_params::(params)?; let email = match ¶ms.user_id { UserId::Email(email) => email.clone(), _ => { error!("Invalid user_id type: expected Email, got {:?}", params.user_id); - return Err(DetailedError::new(INVALID_PARAMS_CODE, "Invalid parameter") - .with_field("user_id") - .with_reason("user_id must be of type 'email'") - .to_error_object()); + return Err( + DetailedError::invalid_params("user_id", "expect email").to_rpc_error() + ); }, }; @@ -46,10 +40,9 @@ pub fn register_verify_email_verification_code_test< UserAuth::Email(code) => code.clone(), _ => { error!("Invalid auth type: expected Email, got {:?}", params.auth); - return Err(DetailedError::new(INVALID_PARAMS_CODE, "Invalid parameter") - .with_field("auth") - .with_reason("auth must be of type 'email' with verification code") - .to_error_object()); + return Err( + DetailedError::invalid_params("auth", "expect email").to_rpc_error() + ); }, }; @@ -61,7 +54,7 @@ pub fn register_verify_email_verification_code_test< ); DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication failed") .with_reason(format!("Email verification failed: {}", e)) - .to_error_object() + .to_rpc_error() })?; Ok::<(), ErrorObject>(()) diff --git a/tee-worker/omni-executor/rpc-server/src/server.rs b/tee-worker/omni-executor/rpc-server/src/server.rs index 3a1c8019c5..0fae00552b 100644 --- a/tee-worker/omni-executor/rpc-server/src/server.rs +++ b/tee-worker/omni-executor/rpc-server/src/server.rs @@ -21,7 +21,7 @@ use std::{env, net::SocketAddr, sync::Arc}; use tracing::info; use wildmeta_api::WildmetaApi; -pub(crate) struct RpcContext { +pub struct RpcContext { pub shielding_key: ShieldingKey, pub storage_db: Arc, pub mailer_factory: Arc, diff --git a/tee-worker/omni-executor/rpc-server/src/utils/auth.rs b/tee-worker/omni-executor/rpc-server/src/utils/auth.rs new file mode 100644 index 0000000000..35af8dab0f --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/auth.rs @@ -0,0 +1,532 @@ +use crate::utils::user_op::convert_to_packed_user_op; +use crate::RpcResult; +use crate::{error_code::AUTH_VERIFICATION_FAILED_CODE, ErrorCode}; +use aa_contracts_client::calculate_user_operation_hash; +use alloy::primitives::Address; +use executor_core::types::SerializablePackedUserOperation; +use executor_crypto::ecdsa; +use executor_primitives::{ + signature::{EthereumSignature, HeimaMultiSignature}, + utils::hex::decode_hex, + ChainId, +}; +use executor_storage::{Storage, WildmetaTimestampStorage}; +use heima_primitives::{Address20, Identity}; +use jsonrpsee::types::ErrorObject; +use std::sync::Arc; +use tracing::error; + +/// Verify WildMeta signature +pub fn verify_wildmeta_signature( + agent_address: &str, + business_json: &str, + signature: &str, +) -> RpcResult<()> { + let message = business_json.as_bytes(); + + let signature_bytes = decode_hex(signature).map_err(|e| { + error!("Failed to decode signature: {:?}", e); + ErrorObject::from(ErrorCode::ParseError) + })?; + let ethereum_signature = + EthereumSignature::try_from(signature_bytes.as_slice()).map_err(|e| { + error!("Failed to convert signature to EthereumSignature: {:?}", e); + ErrorObject::from(ErrorCode::ParseError) + })?; + let heima_sig = HeimaMultiSignature::Ethereum(ethereum_signature); + + let agent_address_bytes = decode_hex(agent_address).map_err(|e| { + error!("Failed to decode signature: {:?}", e); + ErrorObject::from(ErrorCode::ParseError) + })?; + let agent_address = Address20::try_from(agent_address_bytes.as_slice()).map_err(|_| { + error!("Failed to parse agent address"); + ErrorObject::from(ErrorCode::ParseError) + })?; + + let agent_identity = Identity::Evm(agent_address); + + if !heima_sig.verify(message, &agent_identity) { + error!("Signature verification failed"); + return Err(ErrorObject::from(ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE))); + } + + Ok(()) +} + +/// Verify WildmetaBackend signature against user operation hash +pub fn verify_wildmeta_backend_signature( + signature: &str, + user_operations: &[SerializablePackedUserOperation], + chain_id: ChainId, + entry_point_address: Address, + expected_pubkey: &[u8; 33], +) -> Result<(), ErrorObject<'static>> { + if user_operations.is_empty() { + error!("No user operations provided for signature verification"); + return Err(ErrorObject::from(ErrorCode::ParseError)); + } + + // Decode signature from hex + let signature_bytes = decode_hex(signature).map_err(|e| { + error!("Failed to decode signature: {:?}", e); + ErrorObject::from(ErrorCode::ParseError) + })?; + + if signature_bytes.len() != 65 { + error!("Invalid signature length: expected 65 bytes, got {}", signature_bytes.len()); + return Err(ErrorObject::from(ErrorCode::ParseError)); + } + + let signature_array: [u8; 65] = signature_bytes + .try_into() + .map_err(|_| ErrorObject::from(ErrorCode::ParseError))?; + + // Convert all user operations to PackedUserOperations and calculate their combined hash + let mut combined_hash_data = Vec::new(); + + for user_op in user_operations { + let packed_user_op = convert_to_packed_user_op(user_op.clone()).map_err(|e| { + error!("Failed to convert user operation: {}", e); + ErrorObject::from(ErrorCode::ParseError) + })?; + + let user_op_hash = + calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); + combined_hash_data.extend_from_slice(&user_op_hash.0); + } + + // Hash the combined data using keccak256 to create a single 32-byte hash + use alloy::primitives::keccak256; + let user_op_hash = keccak256(&combined_hash_data); + + // Convert user op hash to 32-byte array + let user_op_hash_array: [u8; 32] = user_op_hash.0; + + // Convert expected public key to ecdsa::Public + let public_key = ecdsa::Public::from_raw(*expected_pubkey); + + let signature = ecdsa::Signature::from_raw(signature_array); + + // Verify signature directly using verify_prehashed + if !ecdsa::Pair::verify_prehashed(&signature, &user_op_hash_array, &public_key) { + error!("Signature verification failed"); + return Err(ErrorObject::from(ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE))); + } + + Ok(()) +} + +/// Verify and update payload timestamp to prevent replay attacks +pub fn verify_payload_timestamp( + storage: &Arc, + main_address: &str, + new_timestamp: u64, +) -> Result<(), ErrorObject<'static>> { + let last_timestamp = storage + .get(&main_address.to_string()) + .map_err(|_| { + error!("Failed to get last timestamp"); + ErrorObject::from(ErrorCode::InternalError) + })? + .unwrap_or(0); + + if new_timestamp <= last_timestamp { + error!("Invalid payload timestamp: {} <= {}", new_timestamp, last_timestamp); + return Err(ErrorObject::from(ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE))); + } + + storage.insert(&main_address.to_string(), new_timestamp).map_err(|_| { + error!("Failed to store timestamp"); + ErrorObject::from(ErrorCode::InternalError) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use executor_storage::StorageDB; + use tempfile::tempdir; + + #[test] + fn test_verify_wildmeta_signature_with_real_data() { + let business_json = r#"{"action":"trade","amount":1.5,"customField1":"buy","customField2":"market","leverage":10,"metadata":{"features":{"darkMode":true,"notifications":false},"userAgent":"mobile-app","version":"1.0.0"},"positions":[{"entryPrice":50000,"metadata":{"openTime":1640995200,"strategy":"momentum"},"side":"long","size":1.5,"symbol":"BTC/USD"},{"entryPrice":3000,"metadata":{"openTime":1640995300,"strategy":"reversal"},"side":"short","size":2,"symbol":"ETH/USD"}],"price":50000,"riskManagement":{"maxLeverage":20,"stopLoss":{"enabled":true,"percentage":0.05},"takeProfit":{"enabled":true,"percentage":0.1}},"slippage":0.01,"symbol":"BTC/USD","timestamp":1752573555}"#; + let signature = "0x46c737250d61b60cbf0f46a6755e59815844a2f7cdb9dc16bf867b57bfed3526424343a237c15eef9089d571d1f60fd0bd7f91d5888c649216a7df147b386a681c"; + let agent_address = "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd"; + + let result = verify_wildmeta_signature(agent_address, business_json, signature); + assert!(result.is_ok(), "Signature verification should succeed"); + } + + #[test] + fn test_verify_wildmeta_signature_invalid_signature() { + let business_json = r#"{"action":"trade","timestamp":1752573555}"#; + let signature = "0x020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + let agent_address = "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd"; + + let result = verify_wildmeta_signature(agent_address, business_json, signature); + assert!(result.is_err(), "Should fail with invalid signature"); + } + + #[test] + fn test_verify_wildmeta_signature_wrong_signer() { + let business_json = r#"{"action":"trade","amount":1.5,"customField1":"buy","customField2":"market","leverage":10,"metadata":{"features":{"darkMode":true,"notifications":false},"userAgent":"mobile-app","version":"1.0.0"},"positions":[{"entryPrice":50000,"metadata":{"openTime":1640995200,"strategy":"momentum"},"side":"long","size":1.5,"symbol":"BTC/USD"},{"entryPrice":3000,"metadata":{"openTime":1640995300,"strategy":"reversal"},"side":"short","size":2,"symbol":"ETH/USD"}],"price":50000,"riskManagement":{"maxLeverage":20,"stopLoss":{"enabled":true,"percentage":0.05},"takeProfit":{"enabled":true,"percentage":0.1}},"slippage":0.01,"symbol":"BTC/USD","timestamp":1752573555}"#; + + let signature = "0x46c737250d61b60cbf0f46a6755e59815844a2f7cdb9dc16bf867b57bfed3526424343a237c15eef9089d571d1f60fd0bd7f91d5888c649216a7df147b386a681c"; + // Use a different address than the actual signer + let wrong_agent_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; + + let result = verify_wildmeta_signature(wrong_agent_address, business_json, signature); + assert!(result.is_err(), "Should fail with wrong signer address"); + } + + #[test] + fn test_verify_wildmeta_signature_invalid_hex() { + let business_json = r#"{"timestamp":1752573555}"#; + let signature = "invalid_hex"; + let agent_address = "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd"; + + let result = verify_wildmeta_signature(agent_address, business_json, signature); + assert!(result.is_err(), "Should fail with invalid hex signature"); + } + + #[test] + fn test_verify_payload_timestamp_success() { + let tmp_dir = tempdir().unwrap(); + let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); + let storage = Arc::new(WildmetaTimestampStorage::new(db)); + + let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; + + // First timestamp should succeed + let result = verify_payload_timestamp(&storage, main_address, 1000); + assert!(result.is_ok(), "First timestamp should succeed"); + + // Higher timestamp should succeed + let result = verify_payload_timestamp(&storage, main_address, 2000); + assert!(result.is_ok(), "Higher timestamp should succeed"); + } + + #[test] + fn test_verify_payload_timestamp_fails_with_old_timestamp() { + let tmp_dir = tempdir().unwrap(); + let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); + let storage = Arc::new(WildmetaTimestampStorage::new(db)); + + let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; + + // Store initial timestamp + let result = verify_payload_timestamp(&storage, main_address, 1000); + assert!(result.is_ok()); + + // Same timestamp should fail + let result = verify_payload_timestamp(&storage, main_address, 1000); + assert!(result.is_err(), "Same timestamp should fail"); + + // Lower timestamp should fail + let result = verify_payload_timestamp(&storage, main_address, 500); + assert!(result.is_err(), "Lower timestamp should fail"); + } + + #[test] + fn test_verify_payload_timestamp_first_time() { + let tmp_dir = tempdir().unwrap(); + let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); + let storage = Arc::new(WildmetaTimestampStorage::new(db)); + + let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; + + // Any timestamp should succeed for first time + let result = verify_payload_timestamp(&storage, main_address, 1); + assert!(result.is_ok(), "First timestamp should succeed even if it's 1"); + } + + #[test] + fn test_verify_payload_timestamp_persistence() { + let tmp_dir = tempdir().unwrap(); + let db = Arc::new(StorageDB::open_default(tmp_dir.path()).unwrap()); + let storage = Arc::new(WildmetaTimestampStorage::new(db)); + + let main_address = "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"; + + // Store timestamp + verify_payload_timestamp(&storage, main_address, 1000).unwrap(); + + // Verify it's persisted by checking that lower timestamp fails + let result = verify_payload_timestamp(&storage, main_address, 999); + assert!(result.is_err(), "Timestamp should be persisted"); + + // Verify exact stored value fails + let result = verify_payload_timestamp(&storage, main_address, 1000); + assert!(result.is_err(), "Exact stored timestamp should fail"); + + // Higher should succeed + let result = verify_payload_timestamp(&storage, main_address, 1001); + assert!(result.is_ok(), "Higher timestamp should succeed"); + } + + #[test] + fn test_verify_wildmeta_backend_signature_invalid_signature() { + use executor_core::types::SerializablePackedUserOperation; + + // Create a test user operation + let user_op = SerializablePackedUserOperation { + sender: "0x1234567890123456789012345678901234567890".to_string(), + nonce: 42, + init_code: "0x".to_string(), + call_data: "0x".to_string(), + account_gas_limits: + "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), + pre_verification_gas: 21000, + gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" + .to_string(), + paymaster_and_data: "0x".to_string(), + signature: None, + }; + + let invalid_signature = "0x020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + let chain_id = 1; + let entry_point_address = Address::from([0u8; 20]); + let expected_pubkey = [0u8; 33]; + + let result = verify_wildmeta_backend_signature( + invalid_signature, + &[user_op], + chain_id, + entry_point_address, + &expected_pubkey, + ); + + assert!(result.is_err(), "Should fail with invalid signature"); + } + + #[test] + fn test_wildmeta_backend_valid_signature_verification() { + use aa_contracts_client::calculate_user_operation_hash; + use alloy::primitives::{keccak256, Address}; + use executor_core::types::SerializablePackedUserOperation; + use executor_crypto::secp256k1::{ + secp256k1_ecdsa_recover_compressed, secp256k1_ecdsa_sign, + }; + + // Create a test private key (32 bytes) + let private_key: [u8; 32] = [ + 0x47, 0xf7, 0x8f, 0x59, 0x81, 0x2d, 0x6d, 0x1f, 0x2c, 0x8a, 0x65, 0x04, 0x19, 0x0d, + 0x63, 0x7f, 0x34, 0x6c, 0x4b, 0x6f, 0x7d, 0x20, 0x45, 0x32, 0x15, 0x68, 0x91, 0x73, + 0xa2, 0xb8, 0xc9, 0xe4, + ]; + + // Create a test user operation + let user_op = SerializablePackedUserOperation { + sender: "0x1234567890123456789012345678901234567890".to_string(), + nonce: 42, + init_code: "0x".to_string(), + call_data: "0xabcdef".to_string(), + account_gas_limits: + "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), + pre_verification_gas: 21000, + gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" + .to_string(), + paymaster_and_data: "0x".to_string(), + signature: None, + }; + + let chain_id = 31337u64; // Local test chain + let entry_point_address = Address::from([0u8; 20]); + + let mut combined_hash_data = Vec::new(); + let packed_user_op = convert_to_packed_user_op(user_op.clone()).unwrap(); + let user_op_hash = + calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); + combined_hash_data.extend_from_slice(&user_op_hash.0); + let combined_hash = keccak256(&combined_hash_data); + + // Sign the combined hash with our private key + let signature = match secp256k1_ecdsa_sign(&private_key, &combined_hash.0) { + Ok(sig) => sig, + Err(_) => panic!("Failed to sign with valid private key"), + }; + + // Derive the expected public key from the signature and hash + let expected_pubkey = match secp256k1_ecdsa_recover_compressed(&signature, &combined_hash.0) + { + Ok(pk) => pk, + Err(_) => panic!("Failed to recover pubkey from valid signature"), + }; + + // Convert signature to hex string with 0x prefix + let signature_hex = format!("0x{}", hex::encode(signature)); + + // Test the verification function + let result = verify_wildmeta_backend_signature( + &signature_hex, + &[user_op], + chain_id, + entry_point_address, + &expected_pubkey, + ); + + assert!(result.is_ok(), "Valid signature verification should succeed"); + } + + #[test] + fn test_wildmeta_backend_multiple_operations_signature_verification() { + use aa_contracts_client::calculate_user_operation_hash; + use alloy::primitives::{keccak256, Address}; + use executor_core::types::SerializablePackedUserOperation; + use executor_crypto::secp256k1::{ + secp256k1_ecdsa_recover_compressed, secp256k1_ecdsa_sign, + }; + + let private_key: [u8; 32] = [ + 0x47, 0xf7, 0x8f, 0x59, 0x81, 0x2d, 0x6d, 0x1f, 0x2c, 0x8a, 0x65, 0x04, 0x19, 0x0d, + 0x63, 0x7f, 0x34, 0x6c, 0x4b, 0x6f, 0x7d, 0x20, 0x45, 0x32, 0x15, 0x68, 0x91, 0x73, + 0xa2, 0xb8, 0xc9, 0xe4, + ]; + + // Create multiple test user operations + let user_op1 = SerializablePackedUserOperation { + sender: "0x1234567890123456789012345678901234567890".to_string(), + nonce: 42, + init_code: "0x".to_string(), + call_data: "0xabcdef".to_string(), + account_gas_limits: + "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), + pre_verification_gas: 21000, + gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" + .to_string(), + paymaster_and_data: "0x".to_string(), + signature: None, + }; + + let user_op2 = SerializablePackedUserOperation { + sender: "0x9876543210987654321098765432109876543210".to_string(), + nonce: 43, + init_code: "0x".to_string(), + call_data: "0x123456".to_string(), + account_gas_limits: + "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), + pre_verification_gas: 22000, + gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" + .to_string(), + paymaster_and_data: "0x".to_string(), + signature: None, + }; + + let user_operations = vec![user_op1.clone(), user_op2.clone()]; + let chain_id = 31337u64; + let entry_point_address = Address::from([0u8; 20]); + + // Calculate combined hash for all operations + let mut combined_hash_data = Vec::new(); + for user_op in &user_operations { + let packed_user_op = convert_to_packed_user_op(user_op.clone()).unwrap(); + let user_op_hash = + calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); + combined_hash_data.extend_from_slice(&user_op_hash.0); + } + let combined_hash = keccak256(&combined_hash_data); + + // Sign the combined hash + let signature = match secp256k1_ecdsa_sign(&private_key, &combined_hash.0) { + Ok(sig) => sig, + Err(_) => panic!("Failed to sign"), + }; + let expected_pubkey = match secp256k1_ecdsa_recover_compressed(&signature, &combined_hash.0) + { + Ok(pk) => pk, + Err(_) => panic!("Failed to recover pubkey"), + }; + let signature_hex = format!("0x{}", hex::encode(signature)); + + // Test with multiple operations + let result = verify_wildmeta_backend_signature( + &signature_hex, + &user_operations, + chain_id, + entry_point_address, + &expected_pubkey, + ); + + assert!(result.is_ok(), "Multiple operations signature verification should succeed"); + + // Test that single operation would fail with same signature (different hash) + let single_op_result = verify_wildmeta_backend_signature( + &signature_hex, + &[user_op1], + chain_id, + entry_point_address, + &expected_pubkey, + ); + + assert!(single_op_result.is_err(), "Single operation should fail with multi-op signature"); + } + + #[test] + fn test_wildmeta_backend_invalid_signature_wrong_key() { + use aa_contracts_client::calculate_user_operation_hash; + use alloy::primitives::{keccak256, Address}; + use executor_core::types::SerializablePackedUserOperation; + use executor_crypto::secp256k1::secp256k1_ecdsa_sign; + + // Create a test private key + let private_key: [u8; 32] = [ + 0x47, 0xf7, 0x8f, 0x59, 0x81, 0x2d, 0x6d, 0x1f, 0x2c, 0x8a, 0x65, 0x04, 0x19, 0x0d, + 0x63, 0x7f, 0x34, 0x6c, 0x4b, 0x6f, 0x7d, 0x20, 0x45, 0x32, 0x15, 0x68, 0x91, 0x73, + 0xa2, 0xb8, 0xc9, 0xe4, + ]; + + // Wrong expected public key (different from the actual signature) + let wrong_expected_pubkey: [u8; 33] = [ + 0x03, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, + 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfb, 0xa3, 0x72, 0xdd, 0x89, 0x6e, 0x17, 0xc8, 0x43, + 0x79, 0x1b, 0x19, 0x5f, 0x8d, + ]; + + // Create a test user operation + let user_op = SerializablePackedUserOperation { + sender: "0x1234567890123456789012345678901234567890".to_string(), + nonce: 42, + init_code: "0x".to_string(), + call_data: "0xabcdef".to_string(), + account_gas_limits: + "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), + pre_verification_gas: 21000, + gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" + .to_string(), + paymaster_and_data: "0x".to_string(), + signature: None, + }; + + let chain_id = 31337u64; + let entry_point_address = Address::from([0u8; 20]); + + // Calculate combined hash + let mut combined_hash_data = Vec::new(); + let packed_user_op = convert_to_packed_user_op(user_op.clone()).unwrap(); + let user_op_hash = + calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); + combined_hash_data.extend_from_slice(&user_op_hash.0); + let combined_hash = keccak256(&combined_hash_data); + + // Sign the combined hash with our private key + let signature = match secp256k1_ecdsa_sign(&private_key, &combined_hash.0) { + Ok(sig) => sig, + Err(_) => panic!("Failed to sign with valid private key"), + }; + let signature_hex = format!("0x{}", hex::encode(signature)); + + // Test with wrong expected public key - should fail + let result = verify_wildmeta_backend_signature( + &signature_hex, + &[user_op], + chain_id, + entry_point_address, + &wrong_expected_pubkey, + ); + + assert!(result.is_err(), "Signature verification with wrong public key should fail"); + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/utils/mod.rs b/tee-worker/omni-executor/rpc-server/src/utils/mod.rs index 2ca7755243..97147b064a 100644 --- a/tee-worker/omni-executor/rpc-server/src/utils/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/utils/mod.rs @@ -1,6 +1,8 @@ +pub mod auth; pub mod gas_estimation; pub mod omni; pub mod paymaster; pub mod pumpx; pub mod types; pub mod user_op; +pub mod validation; diff --git a/tee-worker/omni-executor/rpc-server/src/utils/omni.rs b/tee-worker/omni-executor/rpc-server/src/utils/omni.rs index 09e6d81a79..c0f4317397 100644 --- a/tee-worker/omni-executor/rpc-server/src/utils/omni.rs +++ b/tee-worker/omni-executor/rpc-server/src/utils/omni.rs @@ -1,6 +1,24 @@ +use crate::error_code::INVALID_RPC_EXTENSION; +use crate::middlewares::RpcExtensions; use executor_primitives::utils::hex::decode_hex; use executor_primitives::AccountId; +use jsonrpsee::core::RpcResult; +use jsonrpsee::Extensions; -pub fn to_omni_account(s: &str) -> Result { - decode_hex(s).map_err(|_| ()).and_then(|b| AccountId::try_from(b.as_slice())) +use crate::detailed_error::DetailedError; + +pub fn to_omni_account(s: &str) -> RpcResult { + decode_hex(s) + .map_err(|_| ()) + .and_then(|b| AccountId::try_from(b.as_slice())) + .map_err(|_| DetailedError::parse_error("Failed to parse omni_account").to_rpc_error()) +} + +/// This is used to extract the sender from the extension (see rpc_middleware.rs), +/// and then convert it to a valid omni_account +pub fn extract_omni_account(ext: &Extensions) -> RpcResult { + ext.get::() + .map(|e| e.sender.clone()) + .ok_or(DetailedError::new(INVALID_RPC_EXTENSION, "Invalid rpc extension").to_rpc_error()) + .and_then(|s| to_omni_account(&s)) } diff --git a/tee-worker/omni-executor/rpc-server/src/utils/types.rs b/tee-worker/omni-executor/rpc-server/src/utils/types.rs index 04844bd48e..f4f46d4090 100644 --- a/tee-worker/omni-executor/rpc-server/src/utils/types.rs +++ b/tee-worker/omni-executor/rpc-server/src/utils/types.rs @@ -1,5 +1,8 @@ +use crate::detailed_error::DetailedError; +use crate::RpcResult; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; +use tracing::error; /// Information about estimated token cost for ERC20 paymaster operations #[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq)] @@ -26,3 +29,47 @@ pub struct GasEstimateResponse { pub max_priority_fee_per_gas: u128, pub estimated_token_cost: Option, } + +pub trait RpcResultExt { + fn map_err_internal(self, context: &str) -> RpcResult; + fn map_err_parse(self, context: &str) -> RpcResult; +} + +pub trait RpcOptionExt { + fn ok_or_internal(self, msg: &str) -> RpcResult; + fn ok_or_parse(self, msg: &str) -> RpcResult; +} + +impl RpcResultExt for Result { + fn map_err_internal(self, context: &str) -> RpcResult { + self.map_err(|e| { + let msg = format!("{}: {:?}", context, e); + error!(msg); + DetailedError::internal_error(&msg).to_rpc_error() + }) + } + + fn map_err_parse(self, context: &str) -> RpcResult { + self.map_err(|e| { + let msg = format!("{}: {:?}", context, e); + error!(msg); + DetailedError::parse_error(&msg).to_rpc_error() + }) + } +} + +impl RpcOptionExt for Option { + fn ok_or_internal(self, msg: &str) -> RpcResult { + self.ok_or_else(|| { + error!(msg); + DetailedError::internal_error(msg).to_rpc_error() + }) + } + + fn ok_or_parse(self, msg: &str) -> RpcResult { + self.ok_or_else(|| { + error!(msg); + DetailedError::parse_error(msg).to_rpc_error() + }) + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/utils/user_op.rs b/tee-worker/omni-executor/rpc-server/src/utils/user_op.rs index 6eb2cf2ab1..c082effa90 100644 --- a/tee-worker/omni-executor/rpc-server/src/utils/user_op.rs +++ b/tee-worker/omni-executor/rpc-server/src/utils/user_op.rs @@ -15,23 +15,22 @@ // along with Litentry. If not, see . use crate::detailed_error::DetailedError; -use crate::error_code::{ - INTERNAL_ERROR_CODE, INVALID_CHAIN_ID_CODE, INVALID_USER_OPERATION_CODE, - SIGNATURE_SERVICE_UNAVAILABLE_CODE, -}; -use crate::methods::PumpxRpcError; use crate::server::RpcContext; use crate::utils::paymaster::{ extract_paymaster_address, is_whitelisted_paymaster, parse_whitelisted_paymasters, process_erc20_paymaster_data, }; -use alloy::primitives::{Address, Bytes, FixedBytes, U256}; +use crate::utils::types::RpcResultExt; +use crate::RpcResult; +use aa_contracts_client::calculate_user_operation_hash; +use alloy::primitives::{hex, Address, Bytes, FixedBytes, U256}; use binance_api::BinancePaymasterApi; use executor_core::intent_executor::IntentExecutor; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::utils::hex::decode_hex; -use executor_primitives::AccountId; +use executor_primitives::{AccountId, ChainId}; use hyperliquid::*; +use jsonrpsee::types::ErrorObjectOwned; use signer_client::ChainType; use std::sync::Arc; use tracing::{debug, error, info}; @@ -146,7 +145,7 @@ pub fn convert_to_packed_user_op( /// Helper function to submit a CoreWriter userOp #[allow(clippy::too_many_arguments)] #[allow(dead_code)] -pub(crate) async fn submit_corewriter_userop< +pub(crate) async fn submit_corewriter_user_ops< CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( ctx: Arc>, @@ -155,15 +154,12 @@ pub(crate) async fn submit_corewriter_userop< chain_id: u64, wallet_index: u32, call_data: String, -) -> Result, PumpxRpcError> { +) -> Result, ErrorObjectOwned> { let smart_wallet_address = &skeleton_user_op.sender; let entry_point_client = ctx.entry_point_clients.get(&chain_id).ok_or_else(|| { error!("No EntryPoint client configured for chain_id: {}", chain_id); - PumpxRpcError::from( - DetailedError::new(INVALID_CHAIN_ID_CODE, "Chain not supported") - .with_reason(format!("Chain ID {} is not supported", chain_id)), - ) + DetailedError::invalid_chain_id(chain_id).to_rpc_error() })?; let nonce = skeleton_user_op.nonce; @@ -185,14 +181,10 @@ pub(crate) async fn submit_corewriter_userop< ) } else { info!("Calculating gas fees"); - let (max_fee_per_gas, max_priority_fee_per_gas) = - entry_point_client.calculate_gas_fees_with_buffer(20).await.map_err(|e| { - error!("Failed to calculate gas fees: {:?}", e); - PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to calculate gas fees"), - ) - })?; + let (max_fee_per_gas, max_priority_fee_per_gas) = entry_point_client + .calculate_gas_fees_with_buffer(20) + .await + .map_err_internal("Failed to calculate gas fees")?; ( pack_gas_fees(max_fee_per_gas.to::(), max_priority_fee_per_gas.to::()), format!("0x{}", hex::encode(pack_account_gas_limits(1_000_000, 2_000_000).as_slice())), @@ -218,177 +210,189 @@ pub(crate) async fn submit_corewriter_userop< } else { encode_simple_paymaster() }, - signature: None, // Will be signed below + signature: None, // Will be signed by submit_user_ops }; - // Now inline the submit user op logic - info!("Processing SubmitUserOp for 1 UserOperation on chain_id: {}", chain_id); + // Use the common submission logic + submit_user_ops(&ctx, vec![user_op], chain_id, wallet_index, omni_account).await +} + +/// Common user operation submission logic shared between test and auth endpoints +/// This function handles the complete flow of processing, signing, and submitting user operations +pub async fn submit_user_ops( + ctx: &RpcContext, + user_operations: Vec, + chain_id: ChainId, + wallet_index: u32, + omni_account: &executor_primitives::AccountId, +) -> RpcResult> { + // Inlined handler logic from handle_submit_user_op + info!( + "Processing SubmitUserOp for {} UserOps on chain_id: {}", + user_operations.len(), + chain_id + ); + + // Get EntryPoint client for this chain (needed for both signing and submission) + let entry_point_client = ctx.entry_point_clients.get(&chain_id).ok_or_else(|| { + error!("No EntryPoint client configured for chain_id: {}", chain_id); + DetailedError::invalid_chain_id(chain_id).to_rpc_error() + })?; + // Parse whitelisted paymasters once let whitelisted_paymaster = parse_whitelisted_paymasters(); - // Convert SerializablePackedUserOperation to PackedUserOperation - let mut packed_user_op = convert_to_packed_user_op(user_op.clone()).map_err(|e| { - error!("Failed to convert UserOperation: {}", e); - PumpxRpcError::from( - DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid user operation") - .with_reason(format!("Invalid user operation: {}", e)), - ) - })?; + let mut user_ops = Vec::new(); + + for (index, serializable_user_op) in user_operations.iter().enumerate() { + let mut packed_user_op = + convert_to_packed_user_op(serializable_user_op.clone()).map_err(|e| { + error!("Failed to convert UserOp[{}]: {}", index, e); + DetailedError::invalid_user_op(&format!( + "Failed to convert UserOp[{}]: {}", + index, e + )) + .to_rpc_error() + })?; - // Check userOp signature status and validate paymaster usage - if packed_user_op.signature.is_empty() { - // UNSIGNED userOp: If paymaster specified, must be whitelisted - if !packed_user_op.paymasterAndData.is_empty() { - if let Some(paymaster_address) = - extract_paymaster_address(&packed_user_op.paymasterAndData) - { - if !is_whitelisted_paymaster(&paymaster_address, &whitelisted_paymaster) { - error!( - "UserOperation uses non-whitelisted paymaster {}. Only whitelisted paymasters are allowed for unsigned userOps.", - paymaster_address - ); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid user operation") - .with_reason(format!( - "UserOperation uses non-whitelisted paymaster {}", - paymaster_address - )), - )); + // Check userOp signature status and validate paymaster usage + if packed_user_op.signature.is_empty() { + // UNSIGNED userOp: If paymaster specified, must be whitelisted + if !packed_user_op.paymasterAndData.is_empty() { + if let Some(paymaster_address) = + extract_paymaster_address(&packed_user_op.paymasterAndData) + { + if !is_whitelisted_paymaster(&paymaster_address, &whitelisted_paymaster) { + error!( + "UserOp {} uses non-whitelisted paymaster {}. Only whitelisted paymasters are allowed for unsigned userOps.", + index, paymaster_address + ); + return Err(DetailedError::invalid_user_op(&format!( + "UserOp[{}] uses non-whitelisted paymaster {}", + index, paymaster_address + )) + .to_rpc_error()); + } } - } - match process_erc20_paymaster_data( - ctx.binance_api_client.as_ref() as &dyn BinancePaymasterApi, - &packed_user_op.paymasterAndData, - chain_id, - ) - .await - { - Ok(Some(updated_paymaster_data)) => { - packed_user_op.paymasterAndData = updated_paymaster_data; - info!("Updated ERC20 paymaster data for UserOperation"); - }, - Ok(None) => { - debug!("UserOperation does not use ERC20 paymaster"); - }, - Err(e) => { - error!("Failed to process ERC20 paymaster data for UserOperation: {}", e); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid user operation") - .with_reason(format!("ERC20 paymaster processing failed: {}", e)), - )); - }, + match process_erc20_paymaster_data( + ctx.binance_api_client.as_ref() as &dyn BinancePaymasterApi, + &packed_user_op.paymasterAndData, + chain_id, + ) + .await + { + Ok(Some(updated_paymaster_data)) => { + packed_user_op.paymasterAndData = updated_paymaster_data; + info!("Updated ERC20 paymaster data for UserOp[{}]", index); + }, + Ok(None) => { + // Not an ERC20 paymaster, continue as normal + debug!("UserOp[{}] does not use ERC20 paymaster", index); + }, + Err(e) => { + error!( + "Failed to process ERC20 paymaster data for UserOp[{}]: {}", + index, e + ); + return Err(DetailedError::invalid_user_op(&format!( + "ERC20 paymaster processing failed for UserOp[{}]: {}", + index, e + )) + .to_rpc_error()); + }, + } } - } - info!("Requesting signature from pumpx signer for UserOperation"); - - info!( - "UserOp details - Sender: {}, Nonce: {}, InitCode length: {}, CallData length: {}", - packed_user_op.sender, - packed_user_op.nonce, - packed_user_op.initCode.len(), - packed_user_op.callData.len() - ); - - let entry_point_address = entry_point_client.entry_point_address(); - - let user_op_hash_bytes = aa_contracts_client::calculate_user_operation_hash( - &packed_user_op, - entry_point_address, - chain_id, - ); - let message_to_sign = user_op_hash_bytes.to_vec(); - - info!( - "Signing UserOp hash: 0x{}, EntryPoint: {}, ChainID: {}", - hex::encode(user_op_hash_bytes), - entry_point_address, - chain_id - ); - - // Request signature from pumpx signer for EVM chain - let signature_result = ctx - .signer_client - .request_signature( - ChainType::Evm, - wallet_index, - omni_account.clone().into(), - message_to_sign, - ) - .await; - - let signature = match signature_result { - Ok(sig) => substrate_to_ethereum_signature(&sig) - .map_err(|e| { - error!("Failed to convert signature: {}", e); - PumpxRpcError::from( - DetailedError::new( - SIGNATURE_SERVICE_UNAVAILABLE_CODE, - "Signature service unavailable", - ) - .with_suggestion("Please try again later"), - ) - })? - .to_vec(), - Err(_) => { - error!("Failed to sign user operation"); - return Err(PumpxRpcError::from( - DetailedError::new( - SIGNATURE_SERVICE_UNAVAILABLE_CODE, - "Signature service unavailable", - ) - .with_suggestion("Please try again later"), - )); - }, - }; + info!("Requesting signature from pumpx signer for UserOp[{}]", index); - // Prepend 0x01 byte to indicate Root signature type - let mut signature_with_prefix: Vec = vec![0x01]; - signature_with_prefix.extend_from_slice(&signature); - packed_user_op.signature = Bytes::from(signature_with_prefix); - info!("UserOperation signed successfully"); - } else { - // SIGNED userOp: Only allowed if no paymaster specified - if !packed_user_op.paymasterAndData.is_empty() { - error!( - "UserOperation is signed but has paymaster data. Signed userOps are only allowed without paymaster." + // Log UserOp details for debugging + info!( + "UserOp details: sender={}, nonce={}, initCode len={}, callData len={}", + packed_user_op.sender, + packed_user_op.nonce, + packed_user_op.initCode.len(), + packed_user_op.callData.len() + ); + + let entry_point_address = entry_point_client.entry_point_address(); + + let user_op_hash_bytes = + calculate_user_operation_hash(&packed_user_op, entry_point_address, chain_id); + let message_to_sign = user_op_hash_bytes.to_vec(); + + info!( + "Signing UserOp hash: 0x{}, EntryPoint: {}, ChainID: {}", + hex::encode(user_op_hash_bytes), + entry_point_address, + chain_id ); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid user operation") - .with_reason("UserOperation is signed but specifies a paymaster"), - )); + + // Request signature from pumpx signer for EVM chain + let sig = ctx + .signer_client + .request_signature( + ChainType::Evm, + wallet_index, + omni_account.clone().into(), + message_to_sign, + ) + .await + .map_err(|_| DetailedError::signer_service_error().to_rpc_error())?; + + let signature = substrate_to_ethereum_signature(&sig) + .map_err_internal("Failed to convert signature")? + .to_vec(); + + // Prepend 0x01 byte to indicate Root signature type (according to UserOpSigner enum) + let mut signature_with_prefix: Vec = vec![0x01]; + signature_with_prefix.extend_from_slice(&signature); + packed_user_op.signature = Bytes::from(signature_with_prefix); + info!("UserOp[{}] signed successfully", index); + } else { + // SIGNED userOp: Only allowed if no paymaster specified + if !packed_user_op.paymasterAndData.is_empty() { + error!( + "UserOp[{}] is signed but has paymaster data, signed userOps are only allowed without paymaster", + index + ); + return Err(DetailedError::invalid_user_op(&format!( + "UserOp[{}] is signed but specifies a paymaster", + index + )) + .to_rpc_error()); + } + info!("UserOp[{}] is signed with no paymaster, processing", index); } - info!("UserOperation is signed with no paymaster, processing"); - } - // Convert to aa_contracts_client::PackedUserOperation for EntryPoint call - let aa_user_op = aa_contracts_client::PackedUserOperation { - sender: packed_user_op.sender, - nonce: packed_user_op.nonce, - initCode: packed_user_op.initCode.clone(), - callData: packed_user_op.callData.clone(), - accountGasLimits: packed_user_op.accountGasLimits, - preVerificationGas: packed_user_op.preVerificationGas, - gasFees: packed_user_op.gasFees, - paymasterAndData: packed_user_op.paymasterAndData.clone(), - signature: packed_user_op.signature.clone(), - }; + // Convert to aa_contracts_client::PackedUserOperation for EntryPoint call + let aa_user_op = aa_contracts_client::PackedUserOperation { + sender: packed_user_op.sender, + nonce: packed_user_op.nonce, + initCode: packed_user_op.initCode.clone(), + callData: packed_user_op.callData.clone(), + accountGasLimits: packed_user_op.accountGasLimits, + preVerificationGas: packed_user_op.preVerificationGas, + gasFees: packed_user_op.gasFees, + paymasterAndData: packed_user_op.paymasterAndData.clone(), + signature: packed_user_op.signature.clone(), + }; + user_ops.push(aa_user_op); + } // Get beneficiary address from the EntryPoint client's wallet - let beneficiary = entry_point_client.get_wallet_address().await.map_err(|_| { - let err_msg = "Failed to get wallet address from EntryPoint client".to_string(); - error!("{}", err_msg); - PumpxRpcError::from_code_and_message(INTERNAL_ERROR_CODE, err_msg) - })?; - - // Run simulation for UserOperation before submission - info!("Running simulation for UserOperation"); - match entry_point_client.simulate_handle_ops(&[aa_user_op.clone()], beneficiary).await { + let beneficiary = entry_point_client + .get_wallet_address() + .await + .map_err_internal("Failed to get wallet address from EntryPoint client")?; + + // Run batch simulation for all UserOperations before submission + info!("Running batch simulation for {} UserOps", user_ops.len()); + match entry_point_client.simulate_handle_ops(&user_ops, beneficiary).await { Ok(simulation_results) => { for (index, result) in simulation_results.iter().enumerate() { info!( - "UserOperation {} simulation successful. PreOpGas: {}, Paid: {}, AccountValidation: {}, PaymasterValidation: {}", + "UserOp[{}] simulation successful, preOpGas={}, paid={}, accountValidationData={}, paymasterValidationData={}", index, result.preOpGas, result.paid, @@ -396,30 +400,21 @@ pub(crate) async fn submit_corewriter_userop< result.paymasterValidationData ); } - info!("UserOperation passed simulation checks"); + info!("All {} UserOps passed batch simulation checks", user_ops.len()); }, Err(e) => { - let err_msg = format!("UserOperation simulation failed: {}", e); - error!("{}", err_msg); - return Err(PumpxRpcError::from( - DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid user operation") - .with_reason(err_msg), - )); + let err_msg: String = format!("Batch UserOp simulation failed: {}", e); + error!("{}", err_msg.clone()); + return Err(DetailedError::invalid_user_op(&err_msg).to_rpc_error()); }, } - // Submit UserOperation via EntryPoint.handleOps() with retry logic + // Submit all UserOperations via EntryPoint.handleOps() with retry logic let transaction_hash = - match entry_point_client.handle_ops_with_retry(&[aa_user_op], beneficiary).await { - Ok(tx_hash) => Some(tx_hash), - Err(_) => { - let err_msg = - "Failed to submit UserOperation to EntryPoint via handleOps after retries" - .to_string(); - error!("{}", err_msg); - return Err(PumpxRpcError::from_code_and_message(INTERNAL_ERROR_CODE, err_msg)); - }, - }; + entry_point_client + .handle_ops_with_retry(&user_ops, beneficiary) + .await + .map_err_internal("Failed to submit UserOps to EntryPoint after retries")?; - Ok(transaction_hash) + Ok(Some(transaction_hash)) } diff --git a/tee-worker/omni-executor/rpc-server/src/utils/validation.rs b/tee-worker/omni-executor/rpc-server/src/utils/validation.rs new file mode 100644 index 0000000000..dd29fb95a2 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/validation.rs @@ -0,0 +1,270 @@ +use crate::config::{MAX_WALLET_INDEX, SUPPORTED_EVM_CHAINS}; +use crate::detailed_error::DetailedError; +use crate::RpcResult; +use alloy::primitives::Address; +use email_address::EmailAddress; +use std::str::FromStr; +use tracing::error; + +/// Generic function to parse a string into a numeric type (f64, u128, etc.) +/// Returns RpcResult with field name in error message for better debugging +/// +/// # Example +/// ```ignore +/// let value: f64 = parse_as(&some_string, "collateral_size")?; +/// let count: u128 = parse_as(&count_string, "hedge_open_cloid")?; +/// ``` +pub fn parse_as(value: &str, field_name: &str) -> RpcResult +where + T: FromStr, + T::Err: std::fmt::Display, +{ + value.parse::().map_err(|e| { + DetailedError::parse_error(&format!("Failed to parse {}: {}", field_name, e)) + .with_field(field_name) + .with_received(value.to_string()) + .to_rpc_error() + }) +} + +/// Generic function to parse JSON-RPC params into a typed struct +/// Returns RpcResult with consistent error handling +/// +/// # Example +/// ```ignore +/// let params = parse_rpc_params::(params)?; +/// ``` +pub fn parse_rpc_params( + params: jsonrpsee::types::Params, +) -> RpcResult { + params.parse::().map_err(|e| { + error!("Failed to parse RPC params: {:?}", e); + DetailedError::parse_error("Invalid JSON format or missing required fields").to_rpc_error() + }) +} + +pub fn validate_chain_id(chain_id: u32) -> RpcResult<()> { + if !SUPPORTED_EVM_CHAINS.contains(&chain_id) { + return Err(DetailedError::invalid_chain_id(chain_id as u64).to_rpc_error()); + } + Ok(()) +} + +pub fn validate_wallet_index(index: u32) -> RpcResult<()> { + if index > MAX_WALLET_INDEX { + return Err(DetailedError::invalid_params( + "wallet_index", + &format!("exceed {}", MAX_WALLET_INDEX), + ) + .to_rpc_error()); + } + Ok(()) +} + +pub fn validate_evm_address(address: &str, field: &str) -> RpcResult
{ + Address::from_str(address).map_err(|e| { + DetailedError::invalid_params(field, &format!("invalid evm address: {:?}", e)) + .to_rpc_error() + }) +} + +pub fn validate_amount(amount: &str, field: &str) -> RpcResult { + let amount = parse_as::(amount, field)?; + + if amount == 0 { + return Err(DetailedError::invalid_params(field, "expect non-zero").to_rpc_error()); + } + + Ok(amount) +} + +pub fn validate_email(email: &str) -> RpcResult<()> { + if !EmailAddress::is_valid(email) { + return Err(DetailedError::invalid_params( + "email", + &format!("invalid email address: {}", email), + ) + .to_rpc_error()); + } + + // Additionally require a TLD (at least one dot after @) + if let Some(at_pos) = email.find('@') { + let domain = &email[at_pos + 1..]; + if !domain.contains('.') { + return Err(DetailedError::invalid_params( + "email", + &format!("expect TLD in email address: {}", email), + ) + .to_rpc_error()); + } + } + + Ok(()) +} + +pub fn validate_user_operations( + operations: &[executor_core::types::SerializablePackedUserOperation], +) -> RpcResult<()> { + if operations.is_empty() { + return Err( + DetailedError::invalid_params("user_operations", "expect non-empty").to_rpc_error() + ); + } + + // Validate each operation's sender address + for (index, op) in operations.iter().enumerate() { + validate_evm_address(&op.sender, &format!("user_operations[{}].sender", index))?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_chain_id_evm_valid() { + // Valid EVM chain IDs - Mainnets + assert!(validate_chain_id(1).is_ok()); + assert!(validate_chain_id(137).is_ok()); + assert!(validate_chain_id(42161).is_ok()); + assert!(validate_chain_id(10).is_ok()); + assert!(validate_chain_id(8453).is_ok()); + assert!(validate_chain_id(56).is_ok()); + + // Valid EVM chain IDs - Testnets + assert!(validate_chain_id(11155111).is_ok()); + assert!(validate_chain_id(421614).is_ok()); + assert!(validate_chain_id(11155420).is_ok()); + assert!(validate_chain_id(80001).is_ok()); + assert!(validate_chain_id(84532).is_ok()); + assert!(validate_chain_id(97).is_ok()); + assert!(validate_chain_id(31337).is_ok()); + } + + #[test] + fn test_validate_chain_id_evm_invalid() { + // Invalid EVM chain IDs + assert!(validate_chain_id(5).is_err()); + assert!(validate_chain_id(421613).is_err()); + assert!(validate_chain_id(84531).is_err()); + } + + #[test] + fn test_validate_wallet_index_valid() { + assert!(validate_wallet_index(0).is_ok()); + assert!(validate_wallet_index(50).is_ok()); + assert!(validate_wallet_index(100).is_ok()); + } + + #[test] + fn test_validate_wallet_index_invalid() { + assert!(validate_wallet_index(101).is_err()); + assert!(validate_wallet_index(1000).is_err()); + } + + #[test] + fn test_validate_evm_address_valid() { + let addresses = vec![ + "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb9", + "0x0000000000000000000000000000000000000000", + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ]; + + for addr in addresses { + assert!(validate_evm_address(addr, "test").is_ok()); + } + } + + #[test] + fn test_validate_evm_address_invalid() { + let invalid_addresses = vec![ + "not_an_address", + "0x", + "0xGGGG", // Invalid hex + "", + ]; + + for addr in invalid_addresses { + assert!(validate_evm_address(addr, "test").is_err()); + } + } + + #[test] + fn test_validate_amount_valid() { + assert_eq!(validate_amount("100", "test").unwrap(), 100u128); + assert_eq!(validate_amount("999999999", "test").unwrap(), 999999999u128); + } + + #[test] + fn test_validate_amount_invalid() { + assert!(validate_amount("", "test").is_err()); // Empty + assert!(validate_amount("0", "test").is_err()); // Zero + assert!(validate_amount("abc", "test").is_err()); // Non-numeric + assert!(validate_amount("-100", "test").is_err()); // Negative + } + + #[test] + fn test_validate_email_valid() { + let valid_emails = vec![ + "test@example.com", + "user.name@example.co.uk", + "test+tag@example.org", + "test_123@test-domain.com", + ]; + + for email in valid_emails { + assert!(validate_email(email).is_ok(), "Failed for: {}", email); + } + } + + #[test] + fn test_validate_email_invalid() { + let invalid_emails = + vec!["notanemail", "@example.com", "test@", "test@.com", "test@example", ""]; + + for email in invalid_emails { + assert!(validate_email(email).is_err(), "Should fail for: {}", email); + } + } + + #[test] + fn test_parse_as_f64() { + // Valid f64 parsing + let result: f64 = parse_as("123.456", "test_field").unwrap(); + assert_eq!(result, 123.456); + + let result: f64 = parse_as("0.001", "test_field").unwrap(); + assert_eq!(result, 0.001); + + // Invalid f64 parsing + let result: Result = parse_as("not_a_number", "test_field"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_as_u128() { + // Valid u128 parsing + let result: u128 = parse_as("123456789", "test_field").unwrap(); + assert_eq!(result, 123456789u128); + + // Invalid u128 parsing + let result: Result = parse_as("123.456", "test_field"); + assert!(result.is_err()); + + let result: Result = parse_as("-100", "test_field"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_as_u32() { + // Valid u32 parsing + let result: u32 = parse_as("42161", "chain_id").unwrap(); + assert_eq!(result, 42161u32); + + // Invalid u32 parsing (overflow) + let result: Result = parse_as("999999999999", "chain_id"); + assert!(result.is_err()); + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/validation_helpers.rs b/tee-worker/omni-executor/rpc-server/src/validation_helpers.rs deleted file mode 100644 index d5c6ba4c26..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/validation_helpers.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::config::{MAX_WALLET_INDEX, SUPPORTED_EVM_CHAINS}; -use crate::detailed_error::DetailedError; -use alloy::primitives::Address; -use email_address::EmailAddress; -use std::str::FromStr; - -pub fn validate_chain_id( - chain_id: u32, - chain_type: Option<&str>, -) -> Result<(), Box> { - let is_valid = match chain_type { - Some("evm") => SUPPORTED_EVM_CHAINS.contains(&chain_id), - _ => SUPPORTED_EVM_CHAINS.contains(&chain_id), - }; - - if !is_valid { - let supported_chains: Vec = SUPPORTED_EVM_CHAINS.iter().map(|&c| c as u64).collect(); - return Err(Box::new(DetailedError::invalid_chain_id(chain_id as u64, &supported_chains))); - } - Ok(()) -} - -pub fn validate_wallet_index(index: u32) -> Result<(), Box> { - if index > MAX_WALLET_INDEX { - return Err(Box::new(DetailedError::invalid_wallet_index(index, MAX_WALLET_INDEX))); - } - Ok(()) -} - -pub fn validate_ethereum_address( - address: &str, - field_name: &str, -) -> Result> { - Address::from_str(address).map_err(|e| { - Box::new( - DetailedError::invalid_address_format( - field_name, - address, - "0x-prefixed 20-byte Ethereum address (40 hex chars)", - ) - .with_reason(format!("Parse error: {}", e)), - ) - }) -} - -pub fn validate_amount(amount_str: &str, field_name: &str) -> Result> { - // Check if empty - if amount_str.is_empty() { - return Err(Box::new(DetailedError::invalid_amount( - field_name, - amount_str, - "Amount cannot be empty", - ))); - } - - // Parse as u128 - let amount = amount_str.parse::().map_err(|e| { - Box::new(DetailedError::invalid_amount( - field_name, - amount_str, - &format!("Failed to parse amount: {}", e), - )) - })?; - - // Check if zero - if amount == 0 { - return Err(Box::new(DetailedError::invalid_amount( - field_name, - amount_str, - "Amount must be greater than zero", - ))); - } - - Ok(amount) -} - -pub fn validate_token_address( - address: &str, - field_name: &str, -) -> Result> { - // For native token transfers, address might be "0x0" or similar - if address == "0x0" || address == "0x0000000000000000000000000000000000000000" { - return Ok(Address::ZERO); - } - - validate_ethereum_address(address, field_name).map_err(|_e| { - Box::new( - DetailedError::new( - crate::error_code::INVALID_TOKEN_ADDRESS_CODE, - "Invalid token contract address", - ) - .with_field(field_name) - .with_received(address.to_string()) - .with_expected("Valid ERC20 token contract address or 0x0 for native token") - .with_suggestion("Ensure the token address is correct for the selected chain"), - ) - }) -} - -pub fn validate_email(email: &str) -> Result<(), Box> { - if !EmailAddress::is_valid(email) { - return Err(Box::new( - DetailedError::new( - crate::error_code::INVALID_EMAIL_FORMAT_CODE, - "Invalid email format", - ) - .with_field("email") - .with_received(email.to_string()) - .with_expected("Valid email address (e.g., user@example.com)") - .with_suggestion("Please provide a valid email address"), - )); - } - - // Additionally require a TLD (at least one dot after @) - if let Some(at_pos) = email.find('@') { - let domain = &email[at_pos + 1..]; - if !domain.contains('.') { - return Err(Box::new( - DetailedError::new( - crate::error_code::INVALID_EMAIL_FORMAT_CODE, - "Invalid email format", - ) - .with_field("email") - .with_received(email.to_string()) - .with_expected("Email address with a valid domain (e.g., user@example.com)") - .with_suggestion("Email domain must include a top-level domain (TLD)"), - )); - } - } - - Ok(()) -} - -pub fn validate_user_operations( - operations: &[executor_core::types::SerializablePackedUserOperation], -) -> Result<(), Box> { - if operations.is_empty() { - return Err(Box::new( - DetailedError::new( - crate::error_code::MISSING_REQUIRED_FIELD_CODE, - "User operations cannot be empty", - ) - .with_field("user_operations") - .with_expected("At least one user operation") - .with_suggestion("Provide at least one user operation to submit"), - )); - } - - // Validate each operation's sender address - for (index, op) in operations.iter().enumerate() { - validate_ethereum_address(&op.sender, &format!("user_operations[{}].sender", index))?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_chain_id_evm_valid() { - // Valid EVM chain IDs - Mainnets - assert!(validate_chain_id(1, Some("evm")).is_ok()); - assert!(validate_chain_id(137, Some("evm")).is_ok()); - assert!(validate_chain_id(42161, Some("evm")).is_ok()); - assert!(validate_chain_id(10, Some("evm")).is_ok()); - assert!(validate_chain_id(8453, Some("evm")).is_ok()); - assert!(validate_chain_id(56, Some("evm")).is_ok()); - - // Valid EVM chain IDs - Testnets - assert!(validate_chain_id(11155111, Some("evm")).is_ok()); - assert!(validate_chain_id(421614, Some("evm")).is_ok()); - assert!(validate_chain_id(11155420, Some("evm")).is_ok()); - assert!(validate_chain_id(80001, Some("evm")).is_ok()); - assert!(validate_chain_id(84532, Some("evm")).is_ok()); - assert!(validate_chain_id(97, Some("evm")).is_ok()); - assert!(validate_chain_id(31337, Some("evm")).is_ok()); - } - - #[test] - fn test_validate_chain_id_evm_invalid() { - // Invalid EVM chain IDs - assert!(validate_chain_id(5, Some("evm")).is_err()); - assert!(validate_chain_id(421613, Some("evm")).is_err()); - assert!(validate_chain_id(84531, Some("evm")).is_err()); - } - - #[test] - fn test_validate_chain_id_any_type() { - // Any type should check EVM chains - assert!(validate_chain_id(1, None).is_ok()); // Ethereum Mainnet - assert!(validate_chain_id(11155111, None).is_ok()); // Sepolia - assert!(validate_chain_id(31337, None).is_ok()); // Local Anvil - assert!(validate_chain_id(999, None).is_ok()); // HyperEVM - assert!(validate_chain_id(5, None).is_err()); - } - - #[test] - fn test_validate_chain_id_no_memory_leak() { - // This test ensures the memory leak fix is working - // Previously this would leak memory on every call with None chain_type - for _ in 0..100 { - let _ = validate_chain_id(999, None); - } - // If memory leak was present, this would cause issues in valgrind/miri - } - - #[test] - fn test_validate_wallet_index_valid() { - assert!(validate_wallet_index(0).is_ok()); - assert!(validate_wallet_index(50).is_ok()); - assert!(validate_wallet_index(100).is_ok()); - } - - #[test] - fn test_validate_wallet_index_invalid() { - assert!(validate_wallet_index(101).is_err()); - assert!(validate_wallet_index(1000).is_err()); - } - - #[test] - fn test_validate_ethereum_address_valid() { - let addresses = vec![ - "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb9", - "0x0000000000000000000000000000000000000000", - "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - ]; - - for addr in addresses { - assert!(validate_ethereum_address(addr, "test").is_ok()); - } - } - - #[test] - fn test_validate_ethereum_address_invalid() { - let invalid_addresses = vec![ - "not_an_address", - "0x", - "0xGGGG", // Invalid hex - "", - ]; - - for addr in invalid_addresses { - assert!(validate_ethereum_address(addr, "test").is_err()); - } - } - - #[test] - fn test_validate_amount_valid() { - assert_eq!(validate_amount("100", "test").unwrap(), 100u128); - assert_eq!(validate_amount("999999999", "test").unwrap(), 999999999u128); - } - - #[test] - fn test_validate_amount_invalid() { - assert!(validate_amount("", "test").is_err()); // Empty - assert!(validate_amount("0", "test").is_err()); // Zero - assert!(validate_amount("abc", "test").is_err()); // Non-numeric - assert!(validate_amount("-100", "test").is_err()); // Negative - } - - #[test] - fn test_validate_token_address_native() { - // Native token addresses - assert_eq!(validate_token_address("0x0", "test").unwrap(), Address::ZERO); - assert_eq!( - validate_token_address("0x0000000000000000000000000000000000000000", "test").unwrap(), - Address::ZERO - ); - } - - #[test] - fn test_validate_token_address_erc20() { - let erc20_addr = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb9"; - assert!(validate_token_address(erc20_addr, "test").is_ok()); - } - - #[test] - fn test_validate_email_valid() { - let valid_emails = vec![ - "test@example.com", - "user.name@example.co.uk", - "test+tag@example.org", - "test_123@test-domain.com", - ]; - - for email in valid_emails { - assert!(validate_email(email).is_ok(), "Failed for: {}", email); - } - } - - #[test] - fn test_validate_email_invalid() { - let invalid_emails = - vec!["notanemail", "@example.com", "test@", "test@.com", "test@example", ""]; - - for email in invalid_emails { - assert!(validate_email(email).is_err(), "Should fail for: {}", email); - } - } -} diff --git a/tee-worker/omni-executor/ts-tests/jsonrpc-mock-tests/hyperliquid_signature_data.test.ts b/tee-worker/omni-executor/ts-tests/jsonrpc-mock-tests/hyperliquid_signature_data.test.ts index 773bb2481e..afecaa0f59 100644 --- a/tee-worker/omni-executor/ts-tests/jsonrpc-mock-tests/hyperliquid_signature_data.test.ts +++ b/tee-worker/omni-executor/ts-tests/jsonrpc-mock-tests/hyperliquid_signature_data.test.ts @@ -37,7 +37,7 @@ describe('Hyperliquid Signature Data Tests', function () { expect(result).to.have.property('hyperliquid_signature_data'); expect(result.main_address).to.be.a('string'); expect(result.main_address).to.match(/^0x[a-fA-F0-9]{40}$/); - + const data = result.hyperliquid_signature_data; expect(data).to.have.property('action'); expect(data).to.have.property('nonce'); @@ -45,7 +45,7 @@ describe('Hyperliquid Signature Data Tests', function () { expect(data.nonce).to.be.a('number'); expect(data.nonce).to.be.greaterThan(0); expect(data.nonce).to.be.lessThan(Date.now() + 60000); - + validateEIP712Signature(data.signature); } @@ -683,7 +683,7 @@ describe('Hyperliquid Signature Data Tests', function () { await omniApi.getHyperliquidSignatureData(params); expect.fail('Expected method to throw an error for invalid agent_address'); } catch (error: any) { - expect(error).to.have.property('message', 'Invalid address format'); + expect(error).to.have.property('message', 'Invalid params'); } }); @@ -720,7 +720,7 @@ describe('Hyperliquid Signature Data Tests', function () { await omniApi.getHyperliquidSignatureData(params); expect.fail('Expected method to throw an error for invalid destination'); } catch (error: any) { - expect(error).to.have.property('message', 'Invalid address format'); + expect(error).to.have.property('message', 'Invalid params'); } }); @@ -757,7 +757,7 @@ describe('Hyperliquid Signature Data Tests', function () { await omniApi.getHyperliquidSignatureData(params); expect.fail('Expected method to throw an error for invalid builder address'); } catch (error: any) { - expect(error).to.have.property('message', 'Invalid address format'); + expect(error).to.have.property('message', 'Invalid params'); } }); });