diff --git a/tee-worker/omni-executor/Cargo.lock b/tee-worker/omni-executor/Cargo.lock index 54e5472deb..f9e3bd11f3 100644 --- a/tee-worker/omni-executor/Cargo.lock +++ b/tee-worker/omni-executor/Cargo.lock @@ -4013,7 +4013,6 @@ dependencies = [ "clap", "config-loader", "cross-chain-intent-executor", - "ethereum-intent-executor", "ethereum-rpc", "ethers", "executor-core", @@ -4026,7 +4025,6 @@ dependencies = [ "jsonrpsee", "metrics-exporter-prometheus", "mock-server", - "native-task-handler", "pumpx", "rand 0.8.5", "rpc-server", @@ -4038,7 +4036,6 @@ dependencies = [ "sha2 0.10.9", "signer-client", "solana", - "solana-intent-executor", "tokio", "tracing", "tracing-subscriber", @@ -5224,7 +5221,7 @@ dependencies = [ "hyper 1.7.0", "hyper-util", "log", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -5665,9 +5662,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -5830,7 +5827,7 @@ dependencies = [ "http 1.3.1", "jsonrpsee-core", "pin-project", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-pki-types", "rustls-platform-verifier 0.5.3", "soketto", @@ -5883,7 +5880,7 @@ dependencies = [ "hyper-util", "jsonrpsee-core", "jsonrpsee-types", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-platform-verifier 0.5.3", "serde", "serde_json", @@ -6572,35 +6569,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "native-task-handler" -version = "0.1.0" -dependencies = [ - "aa-contracts-client", - "alloy", - "async-trait", - "binance-api", - "chrono", - "ethereum-rpc", - "executor-core", - "executor-crypto", - "executor-primitives", - "executor-storage", - "heima-authentication", - "heima-identity-verification", - "hex", - "hyperliquid", - "parity-scale-codec", - "pumpx", - "rand 0.8.5", - "serde", - "signer-client", - "sp-core 35.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio", - "tokio-test", - "tracing", -] - [[package]] name = "native-tls" version = "0.2.14" @@ -6934,9 +6902,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -7762,7 +7730,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.33", + "rustls 0.23.34", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -7783,7 +7751,7 @@ dependencies = [ "rand 0.9.2", "ring 0.17.14", "rustc-hash", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-pki-types", "rustls-platform-verifier 0.6.1", "slab", @@ -8131,7 +8099,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-pki-types", "serde", "serde_json", @@ -8350,9 +8318,9 @@ dependencies = [ "heima-utils", "hex", "http 1.3.1", + "hyperliquid", "hyperliquid_rust_sdk", "jsonrpsee", - "native-task-handler", "oauth-providers", "parity-scale-codec", "pumpx", @@ -8514,9 +8482,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.33" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "aws-lc-rs", "log", @@ -8570,7 +8538,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.7", @@ -8591,7 +8559,7 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.33", + "rustls 0.23.34", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki 0.103.7", @@ -10634,7 +10602,7 @@ dependencies = [ "log", "quinn", "quinn-proto", - "rustls 0.23.33", + "rustls 0.23.34", "solana-connection-cache", "solana-keypair", "solana-measure", @@ -11170,7 +11138,7 @@ dependencies = [ "quinn", "quinn-proto", "rand 0.8.5", - "rustls 0.23.33", + "rustls 0.23.34", "smallvec", "socket2 0.5.10", "solana-keypair", @@ -11323,7 +11291,7 @@ version = "2.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec21c6c242ee93642aa50b829f5727470cdbdf6b461fb7323fe4bc31d1b54c08" dependencies = [ - "rustls 0.23.33", + "rustls 0.23.34", "solana-keypair", "solana-pubkey", "solana-signer", @@ -13147,7 +13115,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.33", + "rustls 0.23.34", "tokio", ] @@ -13163,19 +13131,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "tokio-test" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" -dependencies = [ - "async-stream", - "bytes", - "futures-core", - "tokio", - "tokio-stream", -] - [[package]] name = "tokio-tungstenite" version = "0.20.1" diff --git a/tee-worker/omni-executor/Cargo.toml b/tee-worker/omni-executor/Cargo.toml index 1bd7ea0126..0654fe486c 100644 --- a/tee-worker/omni-executor/Cargo.toml +++ b/tee-worker/omni-executor/Cargo.toml @@ -20,7 +20,6 @@ members = [ "intent/executors/solana", "intent/token-query", "mock-server", - "native-task-handler", "oauth-providers", "omni-cli", "pumpx", @@ -118,7 +117,6 @@ hyperliquid = { path = "hyperliquid" } intent-asset-lock = { path = "intent/asset-lock" } intent-token-query = { path = "intent/token-query" } mock-server = { path = "mock-server" } -native-task-handler = { path = "native-task-handler" } oauth-providers = { path = "oauth-providers" } pumpx = { path = "pumpx" } rpc-server = { path = "rpc-server" } diff --git a/tee-worker/omni-executor/native-task-handler/src/aes256_key_store.rs b/tee-worker/omni-executor/executor-core/src/aes256_key_store.rs similarity index 96% rename from tee-worker/omni-executor/native-task-handler/src/aes256_key_store.rs rename to tee-worker/omni-executor/executor-core/src/aes256_key_store.rs index b7ea1a425e..b408084181 100644 --- a/tee-worker/omni-executor/native-task-handler/src/aes256_key_store.rs +++ b/tee-worker/omni-executor/executor-core/src/aes256_key_store.rs @@ -1,4 +1,4 @@ -use executor_core::key_store::KeyStore; +use crate::key_store::KeyStore; use executor_crypto::aes256::Aes256Key; use rand::Rng; diff --git a/tee-worker/omni-executor/executor-core/src/lib.rs b/tee-worker/omni-executor/executor-core/src/lib.rs index b1b6f742a5..8373a75823 100644 --- a/tee-worker/omni-executor/executor-core/src/lib.rs +++ b/tee-worker/omni-executor/executor-core/src/lib.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . +pub mod aes256_key_store; pub mod ecdsa_key_store; pub mod ed25519_key_store; pub mod event_handler; @@ -26,3 +27,5 @@ pub mod shielding_key_store; pub mod sync_checkpoint_repository; pub mod types; pub mod wallet_metrics; + +pub use aes256_key_store::Aes256KeyStore; diff --git a/tee-worker/omni-executor/executor-worker/Cargo.toml b/tee-worker/omni-executor/executor-worker/Cargo.toml index af95ae30a3..16a4bd2a51 100644 --- a/tee-worker/omni-executor/executor-worker/Cargo.toml +++ b/tee-worker/omni-executor/executor-worker/Cargo.toml @@ -29,7 +29,6 @@ accounting-contract-client = { workspace = true } binance-api = { workspace = true } config-loader = { workspace = true } cross-chain-intent-executor = { workspace = true } -ethereum-intent-executor = { workspace = true } ethereum-rpc = { workspace = true } executor-core = { workspace = true } executor-crypto = { workspace = true } @@ -38,12 +37,10 @@ executor-storage = { workspace = true } heima-identity-verification = { workspace = true } intent-asset-lock = { workspace = true } mock-server = { workspace = true } -native-task-handler = { workspace = true } pumpx = { workspace = true } rpc-server = { workspace = true } signer-client = { workspace = true } solana = { workspace = true } -solana-intent-executor = { workspace = true } wildmeta-api = { workspace = true } [features] diff --git a/tee-worker/omni-executor/executor-worker/src/main.rs b/tee-worker/omni-executor/executor-worker/src/main.rs index 1c7b530576..4036892378 100644 --- a/tee-worker/omni-executor/executor-worker/src/main.rs +++ b/tee-worker/omni-executor/executor-worker/src/main.rs @@ -26,7 +26,6 @@ use clap::Parser; use cli::{Cli, Commands, ExportBundlerKeyArgs}; use config_loader::ConfigLoader; use cross_chain_intent_executor::{Chain, CrossChainIntentExecutor, RpcEndpointRegistry}; -use ethereum_intent_executor::EthereumIntentExecutor; use ethereum_rpc::client::EthereumRpcClient; use executor_core::ecdsa_key_store::EcdsaKeyStore; use executor_core::ed25519_key_store::Ed25519KeyStore; @@ -36,18 +35,17 @@ use executor_core::wallet_metrics::Wallet; use executor_core::wallet_metrics::{ start_wallet_metrics, WalletBalanceFetcher, WalletId, WalletMetrics, WalletNetworkType, }; +use executor_core::Aes256KeyStore; use executor_crypto::{ecdsa, ed25519, PairTrait}; use executor_storage::init_storage; use intent_asset_lock::precise::PreciseAssetsLock; use intent_asset_lock::AccountAssetLocks; use metrics_exporter_prometheus::PrometheusBuilder; -use native_task_handler::Aes256KeyStore; use pumpx::{pubkey_to_evm_address, pubkey_to_solana_address}; use pumpx::{PumpxApi, PumpxApiClient}; use rpc_server::{start_server as start_rpc_server, AuthTokenKeyStore}; use rust_decimal::Decimal; use solana::SolanaRpcClient; -use solana_intent_executor::SolanaIntentExecutor; use std::collections::HashMap; use std::net::SocketAddr; use std::path::Path; @@ -178,12 +176,6 @@ async fn main() -> Result<(), ()> { pumpx_signer_pair, ))); - let ethereum_intent_executor = EthereumIntentExecutor::new( - &config_loader.ethereum_url, - &args.delegation_contract_address, - )?; - let solana_intent_executor = SolanaIntentExecutor::new(&config_loader.solana_url)?; - let mut rpc_endpoint_registry = RpcEndpointRegistry::new(); rpc_endpoint_registry.insert(Chain::Solana, config_loader.solana_url.to_string()); rpc_endpoint_registry.insert(Chain::Ethereum(56), config_loader.bsc_url.to_string()); @@ -531,8 +523,6 @@ async fn main() -> Result<(), ()> { wildmeta_backend_ecdsa_pubkey, evm_accounting_ecdsa_signer_key, bundler_key_export_authorized_pubkey, - Arc::new(ethereum_intent_executor), - Arc::new(solana_intent_executor), Arc::new(cross_chain_intent_executor), aes256_key, entry_point_clients, diff --git a/tee-worker/omni-executor/native-task-handler/Cargo.toml b/tee-worker/omni-executor/native-task-handler/Cargo.toml deleted file mode 100644 index dd135849d7..0000000000 --- a/tee-worker/omni-executor/native-task-handler/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "native-task-handler" -version = "0.1.0" -authors = ['Trust Computing GmbH '] -edition.workspace = true - -[dependencies] -alloy = { workspace = true } -chrono = { workspace = true } -hex = { workspace = true } -parity-scale-codec = { workspace = true } -rand = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -# Local dependencies -aa-contracts-client = { workspace = true } -binance-api = { workspace = true } -ethereum-rpc = { workspace = true } -executor-core = { workspace = true } -executor-crypto = { workspace = true } -executor-primitives = { workspace = true } -executor-storage = { workspace = true } -heima-authentication = { workspace = true } -heima-identity-verification = { workspace = true } -hyperliquid = { workspace = true } -pumpx = { workspace = true } -signer-client = { workspace = true } -sp-core = { workspace = true } - -[dev-dependencies] -async-trait = { workspace = true } -binance-api = { workspace = true, features = ["mocks"] } -tokio-test = "0.4.4" - -[features] -mocks = [] - -[lints] -workspace = true diff --git a/tee-worker/omni-executor/native-task-handler/src/lib.rs b/tee-worker/omni-executor/native-task-handler/src/lib.rs deleted file mode 100644 index a98d51dd92..0000000000 --- a/tee-worker/omni-executor/native-task-handler/src/lib.rs +++ /dev/null @@ -1,3611 +0,0 @@ -mod aes256_key_store; -pub mod types; - -use aa_contracts_client::calculate_user_operation_hash; -use aa_contracts_client::EntryPointClient; -use alloy::primitives::{Address, Bytes, FixedBytes, U256}; -use binance_api::BinancePaymasterApi; -use chrono::{Days, Utc}; -use ethereum_rpc::AlloyRpcProvider; -use executor_core::{ - intent_executor::IntentExecutor, - native_task::{NativeTask, NativeTaskWrapper}, - types::SerializablePackedUserOperation, -}; -use executor_crypto::{ - aes256::{aes_decrypt, Aes256Key}, - jwt, -}; -use executor_primitives::{ - utils::hex::{decode_hex, ToHexPrefixed}, - ChainId, Identity, Intent, PumpxAccountProfile, Web2IdentityType, -}; -use executor_storage::{HeimaJwtStorage, IntentIdStorage, PumpxProfileStorage, Storage, StorageDB}; -use heima_authentication::{ - auth_token::*, - constants::{AUTH_TOKEN_ACCESS_TYPE, AUTH_TOKEN_EXPIRATION_DAYS, AUTH_TOKEN_ID_TYPE}, -}; -use pumpx::{ - methods::create_transfer_tx::CreateTransferTxBody, signer_client::PumpxChainId, PumpxApi, -}; -use signer_client::{ChainType, SignerClient}; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::{mpsc, oneshot}; -use tracing::{debug, error, info}; - -pub use aes256_key_store::Aes256KeyStore; -pub use types::{NativeTaskError, NativeTaskOk, PumpxApiError, PumpxSignerError}; - -pub type ResponseSender = oneshot::Sender>; - -// ============================================================================ -// ERC20 Paymaster Exchange Rate Processing -// ============================================================================ - -// Constants for ERC20 paymaster processing -// paymasterAndData format: paymaster_address (20) + validation_gas_limit (16) + postop_gas_limit (16) + paymaster_data -// paymaster_data: token(20) + exchangeRate(32) + validUntil(32) + validAfter(32) -const MIN_ERC20_PAYMASTER_DATA_LENGTH: usize = 52 + 20 + 32 + 32 + 32; // 168 bytes minimum -const PAYMASTER_DATA_OFFSET: usize = 52; // paymaster address (20) + validation_gas_limit (16) + postop_gas_limit (16) -const EXCHANGE_RATE_FEE_PERCENT: f64 = 0.0; // 0% fee for now -const WEI_PER_ETH: u128 = 1_000_000_000_000_000_000; // 1e18 wei - -// Whitelisted paymaster addresses that we support (deployed by heima), the logic is: -// - Unsigned userOp: If paymaster specified, **must** be whitelisted -// - Signed userOp: Only allowed if paymasterAndData is empty (technically we could still relay it, but we are a private bundler and don't want to relay arbitrary userOps) -// -// These addresses are assumed to be the same across all EVM chains -// -// Note: the SimplePaymaster should be gradually deprecated in favor of the ERC20PaymasterV1. -// Theoretically user could construct an unsiged userOp to use SimplePaymaster "freely" -// -// TODO: add ERC20 paymaster addresses -const WHITELISTED_PAYMASTER_ADDRESSES: &[&str] = &[ - // SimplePaymaster - "0x6255B9F4A4E80BC20eE389fD35DE9d2c029D5912", // staging-v1 - "0xD4dCB31763CBA7295bA4023E9411CB6db607DE07", // prod-v1 - // ERC20PaymasterV1 - "0xA8535e013236E04FAD5dc03eCc4c05A464c01f38", // staging -]; - -// Decode ERC20 paymaster data from paymasterAndData -// Format: paymaster_address(20) + validation_gas_limit(16) + postop_gas_limit(16) + token(20) + exchangeRate(32) + validUntil(32) + validAfter(32) -fn decode_erc20_paymaster_data(paymaster_and_data: &[u8]) -> Option<(Address, u128, u64, u64)> { - if paymaster_and_data.len() < MIN_ERC20_PAYMASTER_DATA_LENGTH { - return None; - } - - // Extract token address from paymaster data (bytes 52-71) - let token_address = - Address::from_slice(&paymaster_and_data[PAYMASTER_DATA_OFFSET..PAYMASTER_DATA_OFFSET + 20]); - - // Extract exchange rate (bytes 72-103, 32 bytes, full u256 but we take as u128) - let rate_bytes = &paymaster_and_data[PAYMASTER_DATA_OFFSET + 20..PAYMASTER_DATA_OFFSET + 52]; - let mut exchange_rate_bytes = [0u8; 16]; - exchange_rate_bytes.copy_from_slice(&rate_bytes[16..32]); // Last 16 bytes for u128 - let exchange_rate = u128::from_be_bytes(exchange_rate_bytes); - - // Extract validUntil (bytes 104-135, 32 bytes, take as u64) - let valid_until_bytes = - &paymaster_and_data[PAYMASTER_DATA_OFFSET + 52..PAYMASTER_DATA_OFFSET + 84]; - let mut until_bytes = [0u8; 8]; - until_bytes.copy_from_slice(&valid_until_bytes[24..32]); // Last 8 bytes for u64 - let valid_until = u64::from_be_bytes(until_bytes); - - // Extract validAfter (bytes 136-167, 32 bytes, take as u64) - let valid_after_bytes = - &paymaster_and_data[PAYMASTER_DATA_OFFSET + 84..PAYMASTER_DATA_OFFSET + 116]; - let mut after_bytes = [0u8; 8]; - after_bytes.copy_from_slice(&valid_after_bytes[24..32]); // Last 8 bytes for u64 - let valid_after = u64::from_be_bytes(after_bytes); - - Some((token_address, exchange_rate, valid_until, valid_after)) -} - -// Encode ERC20 paymaster data with updated exchange rate, preserving validUntil and validAfter -fn encode_erc20_paymaster_data( - original_data: &[u8], - new_exchange_rate: u128, - valid_until: u64, - valid_after: u64, -) -> Vec { - let mut updated_data = original_data.to_vec(); - - // Skip token address (20 bytes), update exchange rate (32 bytes, big-endian u128 in last 16 bytes) - let rate_start = PAYMASTER_DATA_OFFSET + 20; - let rate_bytes = [0u8; 16] - .iter() - .chain(&new_exchange_rate.to_be_bytes()) - .copied() - .collect::>(); - updated_data[rate_start..rate_start + 32].copy_from_slice(&rate_bytes); - - // Update validUntil (32 bytes) - let valid_until_start = rate_start + 32; - let valid_until_bytes = - [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); - updated_data[valid_until_start..valid_until_start + 32].copy_from_slice(&valid_until_bytes); - - // Update validAfter (32 bytes) - let valid_after_start = valid_until_start + 32; - let valid_after_bytes = - [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); - updated_data[valid_after_start..valid_after_start + 32].copy_from_slice(&valid_after_bytes); - - updated_data -} - -/// Extract paymaster address from paymasterAndData field -/// Returns None if the data is too short to contain a valid paymaster address -fn extract_paymaster_address(paymaster_and_data: &Bytes) -> Option
{ - if paymaster_and_data.len() < 20 { - return None; - } - - // First 20 bytes contain the paymaster address - Some(Address::from_slice(&paymaster_and_data[0..20])) -} - -/// Check if a paymaster address is in our whitelist -/// If so, the userOp must be unsigned to allow exchange rate processing -fn is_whitelisted_paymaster(paymaster_address: &Address, whitelisted: &[Address]) -> bool { - whitelisted.contains(paymaster_address) -} - -/// Parse whitelisted paymaster addresses from const strings to Address types -/// Returns empty vec if any address fails to parse (defensive programming) -fn parse_whitelisted_paymasters() -> Vec
{ - WHITELISTED_PAYMASTER_ADDRESSES - .iter() - .filter_map(|addr_str| addr_str.parse().ok()) - .collect() -} - -// Comprehensive token mapping with expanded support -#[derive(Debug, Clone)] -struct TokenInfo { - decimals: u8, - binance_pair: &'static str, -} - -// Get supported tokens - organized by chain for better scalability -fn get_supported_tokens() -> std::collections::HashMap<(u64, &'static str), TokenInfo> { - let mut tokens = std::collections::HashMap::new(); - - // Ethereum mainnet (chain_id 1) - tokens.insert( - (1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC - TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, - ); - tokens.insert( - (1, "0xdac17f958d2ee523a2206206994597c13d831ec7"), // USDT - TokenInfo { decimals: 6, binance_pair: "ETHUSDT" }, - ); - tokens.insert( - (1, "0x6b175474e89094c44da98b954eedeac495271d0f"), // DAI - TokenInfo { decimals: 18, binance_pair: "ETHDAI" }, - ); - - // Arbitrum One (chain_id 42161) - tokens.insert( - (42161, "0xaf88d065e77c8cc2239327c5edb3a432268e5831"), // USDC - TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, - ); - tokens.insert( - (42161, "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"), // USDT - TokenInfo { decimals: 6, binance_pair: "ETHUSDT" }, - ); - - // Arbitrum Sepolia (chain_id 421614) - tokens.insert( - (421614, "0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d"), // USDC - TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, - ); - - // BNB Smart Chain (chain_id 56) - tokens.insert( - (56, "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d"), // USDC - TokenInfo { decimals: 18, binance_pair: "ETHUSDC" }, - ); - tokens.insert( - (56, "0x55d398326f99059ff775485246999027b3197955"), // USDT - TokenInfo { decimals: 18, binance_pair: "ETHUSDT" }, - ); - - // Base (Chain ID 8453) - tokens.insert( - (8453, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"), // USDC - TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, - ); - - tokens -} - -// Map token address to Binance symbol and return (symbol, decimals) from hardcoded mapping -async fn get_token_info_from_mapping( - token_address: &Address, - chain_id: u64, -) -> Option<(String, u8)> { - let tokens = get_supported_tokens(); - let address_str = token_address.to_string().to_lowercase(); - - // Look up token info from our comprehensive mapping - if let Some(token_info) = tokens.get(&(chain_id, address_str.as_str())) { - // Use hardcoded values from our mapping - return Some((token_info.binance_pair.to_string(), token_info.decimals)); - } - - debug!( - "Token address {} on chain {} is not supported. Supported tokens: {:?}", - token_address, - chain_id, - tokens.keys().filter(|(cid, _)| *cid == chain_id).collect::>() - ); - None -} - -// Calculate exchange rate using Binance API -async fn calculate_exchange_rate_with_binance( - binance_api: &dyn BinancePaymasterApi, - token_symbol: &str, - token_decimals: u8, -) -> Result { - // Query price from Binance - // For ETHUSDC, this returns how many USDC for 1 ETH (e.g., 4479.99) - let price_str = binance_api - .get_symbol_price(token_symbol) - .await - .map_err(|e| format!("Failed to get price for {}: {:?}", token_symbol, e))?; - - let tokens_per_eth: f64 = price_str - .parse() - .map_err(|e| format!("Failed to parse price '{}': {}", price_str, e))?; - - if tokens_per_eth <= 0.0 { - return Err(format!("Invalid price from Binance: {}", tokens_per_eth)); - } - - // Apply fee percentage (increase rate to charge more tokens) - let tokens_per_eth_with_fee = tokens_per_eth * (1.0 + EXCHANGE_RATE_FEE_PERCENT / 100.0); - - // Calculate exchange rate according to ERC20PaymasterV1.sol: - // exchangeRate = tokensPerEth * 10^tokenDecimals - // Example: For 6-decimal USDC at $4000/ETH: rate = 4000 * 10^6 = 4000000000 - // This rate is used as: requiredTokenAmount = (maxCost * exchangeRate) / 1e18 - // Where maxCost is in wei, so 1 ETH of gas = 10^18 wei - // Result: (10^18 * 4000000000) / 10^18 = 4000000000 USDC units = 4000 USDC ✓ - - let exchange_rate_f64 = tokens_per_eth_with_fee * (10_f64.powi(token_decimals as i32)); - - if exchange_rate_f64 <= 0.0 || exchange_rate_f64 >= u128::MAX as f64 { - return Err(format!("Exchange rate {} is out of valid range", exchange_rate_f64)); - } - - info!( - "Calculated exchange rate for {}: {} tokens per ETH -> rate {} (with {}% fee)", - token_symbol, tokens_per_eth, exchange_rate_f64 as u128, EXCHANGE_RATE_FEE_PERCENT - ); - - Ok(exchange_rate_f64 as u128) -} - -// Process ERC20 paymaster data -async fn process_erc20_paymaster_data( - binance_api: &dyn BinancePaymasterApi, - paymaster_and_data: &Bytes, - chain_id: u64, -) -> Result, String> { - let data_bytes = paymaster_and_data.as_ref(); - - // Try to decode as ERC20 paymaster data - if let Some((token_address, original_rate, valid_until, valid_after)) = - decode_erc20_paymaster_data(data_bytes) - { - info!( - "Detected ERC20 paymaster with token {} (original rate: {}, valid_until: {}, valid_after: {})", - token_address, original_rate, valid_until, valid_after - ); - - // Get token symbol and decimals from hardcoded mapping - let (token_symbol, token_decimals) = match get_token_info_from_mapping( - &token_address, - chain_id, - ) - .await - { - Some((symbol, decimals)) => (symbol, decimals), - None => { - return Err(format!( - "Token address {} on chain {} is not supported for price queries. Consider adding it to the supported tokens list or ensure it has a trading pair on Binance.", - token_address, chain_id - )); - }, - }; - - // Calculate new exchange rate - let new_exchange_rate = - calculate_exchange_rate_with_binance(binance_api, &token_symbol, token_decimals) - .await?; - - // Encode updated paymaster data with new exchange rate, keeping original timestamps - let updated_paymaster_data = encode_erc20_paymaster_data( - data_bytes, - new_exchange_rate, - valid_until, // Keep original validUntil - valid_after, // Keep original validAfter - ); - - info!( - "Updated ERC20 paymaster exchange rate: {} -> {} for token {}", - original_rate, new_exchange_rate, token_address - ); - - Ok(Some(Bytes::from(updated_paymaster_data))) - } else { - // Not ERC20 paymaster data or insufficient length, return as-is - debug!("PaymasterAndData is not ERC20 paymaster format, skipping processing"); - Ok(None) - } -} - -/// Calculate estimated token cost for ERC20 paymaster operations -async fn calculate_erc20_token_cost( - binance_api: &dyn BinancePaymasterApi, - paymaster_and_data: &Bytes, - gas_estimates: &NativeTaskOk, - user_op: &aa_contracts_client::PackedUserOperation, - chain_id: u64, -) -> Option { - // Decode paymaster data - let (token_address, _, valid_until, valid_after) = - decode_erc20_paymaster_data(paymaster_and_data.as_ref())?; - - // Check if timestamps are valid - let current_time = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .ok()? - .as_secs(); - - if current_time < valid_after || current_time > valid_until { - debug!("Exchange rate timestamps not valid for estimation"); - return None; - } - - // Get token info from mapping - let (token_symbol, token_decimals) = - get_token_info_from_mapping(&token_address, chain_id).await?; - - // Get fresh exchange rate from Binance - let exchange_rate = match calculate_exchange_rate_with_binance( - binance_api, - &token_symbol, - token_decimals, - ) - .await - { - Ok(rate) => rate, - Err(e) => { - error!("Failed to get exchange rate for token cost estimation: {}", e); - return None; - }, - }; - - // Extract gas limits from estimates (pattern match on NativeTaskOk) - let ( - call_gas, - verification_gas, - pre_verification_gas, - paymaster_verification_gas, - paymaster_post_op_gas, - ) = if let NativeTaskOk::EstimateUserOpGas { - call_gas_limit, - verification_gas_limit, - pre_verification_gas, - paymaster_verification_gas_limit, - paymaster_post_op_gas_limit, - .. - } = gas_estimates - { - ( - *call_gas_limit, - *verification_gas_limit, - *pre_verification_gas, - *paymaster_verification_gas_limit, - *paymaster_post_op_gas_limit, - ) - } else { - return None; - }; - - // Extract gas price from the UserOp's gasFees field - // gasFees format: maxFeePerGas (16 bytes) | maxPriorityFeePerGas (16 bytes) - let gas_fees_bytes = user_op.gasFees.0; - let mut max_fee_bytes = [0u8; 16]; - max_fee_bytes.copy_from_slice(&gas_fees_bytes[0..16]); // First 16 bytes for maxFeePerGas - let max_fee_per_gas = u128::from_be_bytes(max_fee_bytes); - - // Calculate total gas cost in wei - // Include all gas components including paymaster overhead - let total_gas = call_gas - + verification_gas - + pre_verification_gas - + paymaster_verification_gas - + paymaster_post_op_gas; - - let total_cost_wei = total_gas.saturating_mul(max_fee_per_gas); - - // Calculate token amount using same formula as the contract with ceil rounding - // tokenAmount = ceil((gasCost * exchangeRate) / 1e18) - let numerator = total_cost_wei.saturating_mul(exchange_rate); - let token_amount = if numerator == 0 { - 0 - } else { - numerator.saturating_add(WEI_PER_ETH - 1).saturating_div(WEI_PER_ETH) - }; - - // Add 10% safety buffer for price fluctuations - let token_amount_with_buffer = token_amount.saturating_mul(110).saturating_div(100); - - Some(crate::types::TokenCostEstimate { - token_address: token_address.to_string(), - amount: token_amount_with_buffer, - decimals: token_decimals, - exchange_rate, - }) -} - -pub type NativeTaskChannelType = (NativeTaskWrapper, ResponseSender); -pub type NativeTaskSender = mpsc::Sender; - -pub type NativeTaskResponse = Result; - -pub const MAX_CONCURRENT_TASKS: usize = 512; // TODO: make it configurable (if we go for semaphore) - -// Gas estimation constants -/// Maximum verification gas limit to prevent DoS attacks -const MAX_VERIFICATION_GAS: u128 = 3_000_000; -/// Default verification gas for testing during binary search -const DEFAULT_VERIFICATION_GAS_FOR_TESTING: u64 = 1_000_000; -/// Minimum transaction gas as per EIP-155 -const MIN_TRANSACTION_GAS: u64 = 21_000; -/// Maximum reasonable gas for normal operations -const MAX_NORMAL_GAS: u64 = 10_000_000; -/// Minimum gas for deployment operations -const MIN_DEPLOYMENT_GAS: u64 = 100_000; -/// Maximum gas for deployment operations -const MAX_DEPLOYMENT_GAS: u64 = 20_000_000; -/// Safety buffer percentage for verification gas -const VERIFICATION_GAS_BUFFER_PERCENT: u64 = 20; -/// Default paymaster verification gas limit -const DEFAULT_PAYMASTER_VERIFICATION_GAS: u128 = 100_000; -/// Default paymaster post-operation gas limit -const DEFAULT_PAYMASTER_POST_OP_GAS: u128 = 50_000; -/// Maximum allowed paymaster gas to prevent abuse -const MAX_PAYMASTER_GAS: u128 = 5_000_000; - -pub struct TaskHandlerContext< - EthereumIntentExecutor: IntentExecutor, - SolanaIntentExecutor: IntentExecutor, - CrossChainIntentExecutor: IntentExecutor, -> { - pub storage_db: Arc, - pub jwt_rsa_private_key: Vec, - pub aes256_key: Aes256Key, - pub ethereum_intent_executor: Arc, - pub solana_intent_executor: Arc, - pub cross_chain_intent_executor: Arc, - pub pumpx_api: Arc>, - pumpx_signer_client: Arc>, - pub binance_api_client: Arc, - pub entry_point_clients: Arc>>>, - pub whitelisted_paymaster: Arc>, -} - -impl< - EthereumIntentExecutor: IntentExecutor, - SolanaIntentExecutor: IntentExecutor, - CrossChainIntentExecutor: IntentExecutor, - > TaskHandlerContext -{ - #[allow(clippy::too_many_arguments)] - pub fn new( - storage_db: Arc, - jwt_rsa_private_key: Vec, - aes256_key: Aes256Key, - ethereum_intent_executor: Arc, - solana_intent_executor: Arc, - cross_chain_intent_executor: Arc, - pumpx_api: Arc>, - pumpx_signer_client: Arc>, - binance_api_client: Arc, - entry_point_clients: Arc>>>, - ) -> Self { - Self { - storage_db, - jwt_rsa_private_key, - aes256_key, - ethereum_intent_executor, - solana_intent_executor, - cross_chain_intent_executor, - pumpx_api, - pumpx_signer_client, - binance_api_client, - entry_point_clients, - whitelisted_paymaster: Arc::new(parse_whitelisted_paymasters()), - } - } - - /// Get EntryPoint client for a specific chain - pub fn get_entry_point_client( - &self, - chain_id: ChainId, - ) -> Option>> { - self.entry_point_clients.get(&chain_id).cloned() - } -} - -pub async fn handle_native_task< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - ctx: Arc< - TaskHandlerContext, - >, - wrapper: NativeTaskWrapper, -) -> NativeTaskResponse { - let client_id = &wrapper.client_id; - - match wrapper.task { - NativeTask::RequestAuthToken(sender) => { - let expires_at = Utc::now() - .checked_add_days(Days::new(AUTH_TOKEN_EXPIRATION_DAYS)) - .expect("Failed to calculate expiration") - .timestamp(); - let auth_options = AuthOptions { expires_at }; - let claims = match sender { - Identity::Email(ref identity_string) => { - let Ok(email) = std::str::from_utf8(identity_string.inner_ref()) else { - error!("Invalid email identity"); - return Err(NativeTaskError::InvalidMemberIdentity); - }; - AuthTokenClaims::new( - email.to_string(), - AUTH_TOKEN_ID_TYPE.to_string(), - client_id.to_string(), - auth_options, - ) - }, - _ => AuthTokenClaims::new( - sender.hash().to_string(), - AUTH_TOKEN_ID_TYPE.to_string(), - client_id.to_string(), - auth_options, - ), - }; - let Ok(token) = jwt::create(&claims, &ctx.jwt_rsa_private_key) else { - error!("Failed to create auth token"); - return Err(NativeTaskError::AuthTokenCreationFailed); - }; - - Ok(NativeTaskOk::AuthToken(token)) - }, - NativeTask::RequestIntent(omni_account, intent_id, intent) => { - let intent = *intent; - debug!("Intent requested, intent_id: {}", intent_id); - - let intent_id_storage = IntentIdStorage::new(ctx.storage_db.clone()); - 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(NativeTaskError::InternalError(None)); - }, - }; - - if intent_id == stored_intent_id + 1 { - if intent_id_storage.insert(&omni_account, intent_id).is_err() { - error!("Failed to save intent id"); - return Err(NativeTaskError::InternalError(None)); - } - } else { - error!( - "Intent id different than expected, expected: {:?}, got: {:?}", - stored_intent_id + 1, - intent_id - ); - return Err(NativeTaskError::IntentNonceMismatch); - } - - let result = match intent { - Intent::SystemRemark(_) - | Intent::TransferNative(_) - | Intent::CallEthereum(_) - | Intent::TransferEthereum(_) - | Intent::TransferSolana(_) => { - info!("Intent temporarily rejected, intent_id: {}", intent_id); - Err(NativeTaskError::InternalError(None)) - }, - Intent::Swap(..) => { - let response = match ctx - .cross_chain_intent_executor - .execute(&omni_account, intent_id, intent.clone()) - .await - { - Ok((response, _)) => response, - Err(e) => { - error!("Error executing intent: {:?}", e); - ctx.cross_chain_intent_executor.on_execution_error().await; - None - }, - }; - if let Some(response) = response { - Ok(NativeTaskOk::IntentSwapResponse(response)) - } else { - Err(NativeTaskError::InternalError(None)) - } - }, - }; - - result - }, - NativeTask::PumpxRequestJwt(_sender, email, invite_code, google_code, language) => { - let expires_at = Utc::now() - .checked_add_days(Days::new(AUTH_TOKEN_EXPIRATION_DAYS)) - .expect("Failed to calculate expiration") - .timestamp(); - let auth_options = AuthOptions { expires_at }; - - debug!("Calling pumpx get_account_user_id, email: {}", email); - let res = match ctx.pumpx_api.get_account_user_id(email.clone()).await { - Ok(res) => res, - Err(e) => { - error!("Failed to get_account_user_id for email {}: {:?}", email, e); - return Err(NativeTaskError::PumpxApiError( - PumpxApiError::GetAccountUserIdFailed, - )); - }, - }; - debug!("Response pumpx get_account_user_id: {:?}", res); - - let Some(user_id) = res.data.user_id else { - error!("Response data.user_id of call get_account_user_id is none"); - return Err(NativeTaskError::PumpxApiError(PumpxApiError::GetAccountUserIdFailed)); - }; - - debug!("get_account_user_id ok, email: {}, user_id: {}", email, user_id); - let omni_account = Identity::from_web2_account(&user_id, Web2IdentityType::Pumpx) - .to_omni_account(client_id); - - let access_token_claims = AuthTokenClaims::new( - omni_account.to_hex(), - AUTH_TOKEN_ACCESS_TYPE.to_string(), - client_id.to_string(), - auth_options.clone(), - ); - let Ok(access_token) = jwt::create(&access_token_claims, &ctx.jwt_rsa_private_key) - else { - error!("Failed to create access token"); - return Err(NativeTaskError::AuthTokenCreationFailed); - }; - - debug!("Calling pumpx user_connect, user_id: {}, email: {}, invite_code: {:?}, google_code: {:?}", user_id, email, invite_code, google_code); - let Ok(backend_response) = ctx - .pumpx_api - .user_connect( - &access_token, - user_id.clone(), - email.clone(), - invite_code, - google_code, - language, - ) - .await - else { - error!("Failed to connect user"); - return Err(NativeTaskError::PumpxApiError(PumpxApiError::UserConnectionFailed)); - }; - debug!("Response pumpx user_connect: {:?}", backend_response); - - // check google auth value - if !backend_response.data.google_auth_check.unwrap_or(false) { - error!("Google code verification failed from user_connect"); - return Err(NativeTaskError::PumpxApiError( - PumpxApiError::GoogleCodeVerificationFailed, - )); - } - - let id_token_claims = AuthTokenClaims::new( - omni_account.to_hex(), - AUTH_TOKEN_ID_TYPE.to_string(), - client_id.to_string(), - auth_options, - ); - let Ok(id_token) = jwt::create(&id_token_claims, &ctx.jwt_rsa_private_key) else { - error!("Failed to create id token"); - return Err(NativeTaskError::AuthTokenCreationFailed); - }; - - let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); - if storage - .insert(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE), access_token.clone()) - .is_err() - { - error!("Failed to insert pumpx_{}_jwt_token into storage", AUTH_TOKEN_ACCESS_TYPE); - }; - - if storage.insert(&(omni_account, AUTH_TOKEN_ID_TYPE), id_token.clone()).is_err() { - error!("Failed to insert pumpx_{}_jwt_token into storage", AUTH_TOKEN_ID_TYPE); - }; - - Ok(NativeTaskOk::PumpxRequestJwt { access_token, id_token, backend_response }) - }, - NativeTask::PumpxExportWallet( - omni_account, - google_code, - pumpx_chain_id, - pumpx_wallet_index, - expected_wallet_address, - ) => { - let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); - 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(NativeTaskError::InternalError(None)); - }; - - let verify_success = verify_google_code( - ctx.pumpx_api.as_ref().as_ref(), - &access_token, - google_code, - None, - ) - .await; - if !verify_success { - error!("Failed to verify google code within NativeTask::PumpxExportWallet"); - return Err(NativeTaskError::PumpxApiError( - PumpxApiError::GoogleCodeVerificationFailed, - )); - } - - let Some(chain) = ChainType::from_pumpx_chain_id(pumpx_chain_id) else { - error!("Failed to map pumpx chain_id {}", pumpx_chain_id); - return Err(NativeTaskError::ChainNotSupported(pumpx_chain_id as u64)); - }; - - let Ok(mut wallet) = ctx - .pumpx_signer_client - .export_wallet( - chain, - pumpx_wallet_index, - omni_account.clone().into(), - // TODO: theoretically we could pass the aes_key from initial RPC to signer, so that - // we don't have to do double encryption/decryption - ctx.aes256_key.to_vec(), - expected_wallet_address, - ) - .await - else { - error!("Failed to export wallet from pumpx-signer"); - return Err(NativeTaskError::SignatureServiceUnavailable); - }; - let Some(decrypted_wallet) = aes_decrypt(&ctx.aes256_key, &mut wallet) else { - error!("Failed to decrypt wallet"); - return Err(NativeTaskError::InternalError(None)); - }; - - let omni_account_profile_storage = PumpxProfileStorage::new(ctx.storage_db.clone()); - if let Ok(maybe_profile) = omni_account_profile_storage.get(&omni_account) { - let profile = maybe_profile - .map(|mut p| { - p.wallet_exported = true; - p - }) - .unwrap_or_else(|| PumpxAccountProfile { wallet_exported: true }); - if let Err(e) = omni_account_profile_storage.insert(&omni_account, profile) { - error!("Failed to update pumpx account profile: {:?}", e); - return Err(NativeTaskError::InternalError(None)); - }; - } else { - error!("Failed to get pumpx account profile"); - return Err(NativeTaskError::InternalError(None)); - } - Ok(NativeTaskOk::PumpxExportWallet(decrypted_wallet)) - }, - NativeTask::PumpxAddWallet(omni_account) => { - let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); - let Ok(Some(access_token)) = storage.get(&(omni_account, AUTH_TOKEN_ACCESS_TYPE)) - else { - error!("Failed to get pumpx_{}_jwt_token", AUTH_TOKEN_ACCESS_TYPE); - return Err(NativeTaskError::InternalError(None)); - }; - - // Call Pumpx API to add wallet - debug!("Calling pumpx add_wallet"); - let Ok(backend_response) = ctx.pumpx_api.add_wallet(&access_token, None).await else { - error!("Failed to add wallet through Pumpx API"); - return Err(NativeTaskError::PumpxApiError(PumpxApiError::AddWalletFailed)); - }; - - Ok(NativeTaskOk::PumpxAddWallet(backend_response)) - }, - NativeTask::PumpxSignLimitOrder(omni_account, chain_id, wallet_index, unsigned_tx) => { - let Some(chain) = ChainType::from_pumpx_chain_id(chain_id) else { - error!("Failed to map pumpx chain_id {}", chain_id); - return Err(NativeTaskError::ChainNotSupported(chain_id as u64)); - }; - let Ok(signed_txs) = ctx - .pumpx_signer_client - .request_signatures(chain, wallet_index, omni_account.into(), unsigned_tx) - .await - else { - error!("Failed to request signatures from pumpx-signer"); - return Err(NativeTaskError::SignatureServiceUnavailable); - }; - Ok(NativeTaskOk::PumpxSignLimitOrder(signed_txs)) - }, - NativeTask::PumpxTransferWidthdraw( - omni_account, - request_id, - chain_id, - wallet_index, - recipient_address, - token_ca, - amount, - google_code, - language, - ) => { - // 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.clone(), AUTH_TOKEN_ACCESS_TYPE)) - else { - error!("Failed to get access_token within NativeTask::PumpxTransferWidthdraw"); - return Err(NativeTaskError::InternalError(None)); - }; - - // 2. Verify google code in every case - let verify_success = verify_google_code( - ctx.pumpx_api.as_ref().as_ref(), - &access_token, - google_code, - language.clone(), - ) - .await; - if !verify_success { - error!("Failed to verify google code within NativeTask::PumpxTransferWidthdraw"); - return Err(NativeTaskError::PumpxApiError( - PumpxApiError::GoogleCodeVerificationFailed, - )); - } - - // 3. Create a transfer tx and send to backend - let body = CreateTransferTxBody { - request_id, - chain_id, - wallet_index, - recipient_address, - token_ca, - amount, - }; - - debug!("Calling pumpx create_transfer_tx, body {:?}", body); - match ctx.pumpx_api.create_transfer_tx(&access_token, body, language.clone()).await { - Ok(res) => Ok(NativeTaskOk::PumpxTransferWithdraw(res)), - Err(e) => { - error!("Failed to create transfer tx: {}", e); - Err(NativeTaskError::PumpxApiError(PumpxApiError::CreateTransferTxFailed)) - }, - } - }, - NativeTask::PumpxNotifyLimitOrderResult(_omni_account, intent_id, result, message) => { - if result != "ok" && result != "nok" { - error!("Invalid result value: {}. Must be 'ok' or 'nok'", result); - return Err(NativeTaskError::PumpxApiError(PumpxApiError::InvalidInput)); - } - - if let Some(msg) = message { - info!("Limit order result message for intent_id {}: {}", intent_id, msg); - } - - Ok(NativeTaskOk::PumpxNotifyLimitOrderResult) - }, - NativeTask::EstimateUserOpGas( - omni_account, - serializable_user_op, - chain_id, - wallet_index, - ) => { - info!( - "Processing EstimateUserOpGas for account {:?}, wallet_index: {}, chain_id: {}", - omni_account, wallet_index, chain_id - ); - - // Get EntryPoint client for this chain - let entry_point_client = match ctx.get_entry_point_client(chain_id) { - Some(client) => client, - None => { - error!("No EntryPoint client configured for chain_id: {}", chain_id); - return Err(NativeTaskError::ChainNotSupported(chain_id)); - }, - }; - - // Convert SerializablePackedUserOperation to PackedUserOperation - let packed_user_op = match convert_to_packed_user_op(serializable_user_op.clone()) { - Ok(user_op) => user_op, - Err(e) => { - error!("Failed to convert UserOperation: {}", e); - return Err(NativeTaskError::InvalidUserOperation( - "Invalid user operation format".to_string(), - )); - }, - }; - - // Perform gas estimation (wallet_index can be used for wallet-specific optimizations) - match estimate_user_op_gas( - entry_point_client, - packed_user_op, - chain_id, - ctx.binance_api_client.as_ref(), - ) - .await - { - Ok(gas_estimates) => { - info!("Gas estimation successful: {:?}", gas_estimates); - Ok(gas_estimates) - }, - Err(e) => { - error!("Gas estimation failed: {}", e); - Err(NativeTaskError::GasEstimationFailed) - }, - } - }, - NativeTask::SubmitUserOp(omni_account, serializable_user_ops, chain_id, wallet_index) => { - info!( - "Processing SubmitUserOp for {} UserOperations on chain_id: {}", - serializable_user_ops.len(), - chain_id - ); - - // Get EntryPoint client for this chain (needed for both signing and submission) - let entry_point_client = match ctx.get_entry_point_client(chain_id) { - Some(client) => client, - None => { - error!("No EntryPoint client configured for chain_id: {}", chain_id); - return Err(NativeTaskError::ChainNotSupported(chain_id)); - }, - }; - - // Process each UserOperation in the batch - let mut aa_user_ops = Vec::new(); - - for (index, serializable_user_op) in serializable_user_ops.iter().enumerate() { - // Convert SerializablePackedUserOperation to PackedUserOperation - let mut packed_user_op = - match convert_to_packed_user_op(serializable_user_op.clone()) { - Ok(user_op) => user_op, - Err(e) => { - error!("Failed to convert UserOperation {}: {}", index, e); - return Err(NativeTaskError::InvalidUserOperation(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, - &ctx.whitelisted_paymaster, - ) { - error!( - "UserOperation {} uses non-whitelisted paymaster {}. Only whitelisted paymasters are allowed for unsigned userOps.", - index, paymaster_address - ); - return Err(NativeTaskError::InvalidUserOperation(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, - 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(NativeTaskError::InvalidUserOperation(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, - 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 - .pumpx_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).unwrap().to_vec(), - Err(_) => { - error!("Failed to sign user operation {}", index); - return Err(NativeTaskError::SignatureServiceUnavailable); - }, - }; - - // 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(NativeTaskError::InvalidUserOperation(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 = match entry_point_client.get_wallet_address().await { - Ok(address) => address, - Err(_) => { - let err_msg = "Failed to get wallet address from EntryPoint client".to_string(); - error!("{}", err_msg.clone()); - return Err(NativeTaskError::InternalError(Some(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(NativeTaskError::InvalidUserOperation(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(NativeTaskError::InternalError(Some(err_msg))); - }, - }; - - Ok(NativeTaskOk::SubmitUserOp(transaction_hash)) - }, - NativeTask::RequestLoanTest( - omni_account, - user_operation, - chain_id, - wallet_index, - collateral_ticker, - collateral_size, - lending_ratio, - ) => { - info!( - "Processing RequestLoanTest for {:?}, chain_id: {}, wallet_index: {}, sender: {}, collateral: {}, collateral_size: {}, lending_ratio: {}", - omni_account, chain_id, wallet_index, user_operation.sender, collateral_ticker, collateral_size, lending_ratio - ); - - handle_request_loan( - ctx, - omni_account, - user_operation, - chain_id, - wallet_index, - &collateral_ticker, - &collateral_size, - lending_ratio, - client_id, - ) - .await - }, - } -} - -async fn verify_google_code( - pumpx_api: &dyn PumpxApi, - access_token: &str, - google_code: String, - language: Option, -) -> bool { - debug!("Calling pumpx verify_google_code, code: {}", google_code); - let verify_result = pumpx_api.verify_google_code(access_token, google_code, language).await; - verify_result.map_or_else( - |e| { - error!("Google code verification request failed: {:?}", e); - false - }, - |res| { - res.data.result.map_or_else( - || { - error!("Google code verification response result is none"); - false - }, - |success| success, - ) - }, - ) -} - -// Helper functions for gas limit packing/unpacking -fn pack_account_gas_limits(verification_gas: u128, call_gas: u128) -> FixedBytes<32> { - let packed: U256 = (U256::from(verification_gas) << 128) | U256::from(call_gas); - FixedBytes::from(packed.to_be_bytes()) -} - -#[cfg(test)] -fn unpack_verification_gas_limit(packed: FixedBytes<32>) -> u128 { - let value = U256::from_be_bytes(packed.0); - let result: U256 = (value >> 128) & U256::from(u128::MAX); - result.to::() -} - -#[cfg(test)] -fn unpack_call_gas_limit(packed: FixedBytes<32>) -> u128 { - let value = U256::from_be_bytes(packed.0); - let result: U256 = value & U256::from(u128::MAX); - result.to::() -} - -/// Convert Substrate signature to Ethereum ECDSA format -/// Returns signature in format: [r (32 bytes), s (32 bytes), v (1 byte)] -pub fn substrate_to_ethereum_signature(substrate_sig: &[u8]) -> Result<[u8; 65], &'static str> { - if substrate_sig.len() != 65 { - return Err("Invalid signature length"); - } - - // Parse as (r, s, v) format - most common - let mut r = [0u8; 32]; - let mut s = [0u8; 32]; - r.copy_from_slice(&substrate_sig[0..32]); - s.copy_from_slice(&substrate_sig[32..64]); - let substrate_v = substrate_sig[64]; - - // Convert recovery parameter: 0/1 -> 27/28 - let ethereum_v = match substrate_v { - 0 => 27, - 1 => 28, - 27 => 27, // Already Ethereum format - 28 => 28, // Already Ethereum format - _ => return Err("Invalid recovery parameter"), - }; - - // Build Ethereum signature: [r, s, v] - let mut ethereum_sig = [0u8; 65]; - ethereum_sig[0..32].copy_from_slice(&r); - ethereum_sig[32..64].copy_from_slice(&s); - ethereum_sig[64] = ethereum_v; - - Ok(ethereum_sig) -} - -/// Convert SerializablePackedUserOperation to aa_contracts_client::PackedUserOperation -pub fn convert_to_packed_user_op( - user_op: SerializablePackedUserOperation, -) -> Result { - use std::str::FromStr; - - // Helper function to parse hex string to fixed bytes - let parse_hex_fixed = - |hex_str: &str, expected_len: usize, name: &str| -> Result, String> { - let bytes = decode_hex(hex_str) - .map_err(|e| format!("Invalid hex string '{}' '{}': {}", hex_str, name, e))?; - if bytes.len() != expected_len { - return Err(format!( - "Expected {} bytes, got {} for '{}'", - expected_len, - bytes.len(), - hex_str - )); - } - Ok(bytes) - }; - - Ok(aa_contracts_client::PackedUserOperation { - sender: Address::from_str(&user_op.sender) - .map_err(|e| format!("Invalid sender address '{}': {}", user_op.sender, e))?, - nonce: U256::from(user_op.nonce), - initCode: Bytes::from( - decode_hex(&user_op.init_code).map_err(|e| format!("Invalid init_code hex: {}", e))?, - ), - callData: Bytes::from( - decode_hex(&user_op.call_data).map_err(|e| format!("Invalid call_data hex: {}", e))?, - ), - accountGasLimits: { - let bytes = parse_hex_fixed(&user_op.account_gas_limits, 32, "account_gas_limits")?; - FixedBytes::from_slice(&bytes) - }, - preVerificationGas: U256::from(user_op.pre_verification_gas), - gasFees: { - let bytes = parse_hex_fixed(&user_op.gas_fees, 32, "gas_fees")?; - FixedBytes::from_slice(&bytes) - }, - paymasterAndData: Bytes::from( - decode_hex(&user_op.paymaster_and_data) - .map_err(|e| format!("Invalid paymaster_and_data hex: {}", e))?, - ), - signature: match user_op.signature { - Some(sig) => { - Bytes::from(decode_hex(&sig).map_err(|e| format!("Invalid signature hex: {}", e))?) - }, - None => Bytes::new(), // Empty signature for unsigned operations - }, - }) -} - -/// Estimate gas for a UserOperation using simulation -async fn estimate_user_op_gas( - entry_point_client: Arc>, - user_op: aa_contracts_client::PackedUserOperation, - chain_id: ChainId, - binance_api: &dyn BinancePaymasterApi, -) -> Result { - // Step 1: Simulate validation to get base gas requirements - let validation_result = entry_point_client - .simulate_validation(user_op.clone()) - .await - .map_err(|e| format!("Validation simulation failed: {:?}", e))?; - - // Extract preOpGas from validation result - let pre_op_gas = validation_result.returnInfo.preOpGas; - debug!("Validation simulation preOpGas: {}", pre_op_gas); - - // Step 2: Calculate verification gas limit based on validation result - // Add buffer for safety - let buffer_multiplier = U256::from(100 + VERIFICATION_GAS_BUFFER_PERCENT); - let verification_gas_base = pre_op_gas.saturating_mul(buffer_multiplier) / U256::from(100); - let verification_gas_limit = verification_gas_base.min(U256::from(MAX_VERIFICATION_GAS)); - - // Step 3: Binary search for optimal call gas limit - let call_gas_limit = - estimate_call_gas_limit(entry_point_client.clone(), user_op.clone(), chain_id).await?; - - // Step 4: Calculate preVerificationGas (static + dynamic components) - let (static_pvg, dynamic_pvg) = calculate_pre_verification_gas(&user_op, chain_id); - let pre_verification_gas = static_pvg + dynamic_pvg; - - // Step 5: Extract paymaster gas limits if paymaster is present - let (paymaster_verification_gas_limit, paymaster_post_op_gas_limit) = - extract_paymaster_gas_limits(&user_op.paymasterAndData); - - // Step 6: Calculate gas fees with buffer - let (max_fee_per_gas, max_priority_fee_per_gas) = entry_point_client - .calculate_gas_fees_with_buffer(10) // Use 10% additional buffer for estimation - .await - .map_err(|e| format!("Failed to calculate gas fees: {:?}", e))?; - - // Convert to u128 for response, ensuring values are within bounds - let call_gas_limit = call_gas_limit.try_into().map_err(|_| { - format!("Call gas limit {} exceeds maximum supported value", call_gas_limit) - })?; - - let verification_gas_limit = verification_gas_limit.try_into().map_err(|_| { - format!("Verification gas limit {} exceeds maximum supported value", verification_gas_limit) - })?; - - let pre_verification_gas = pre_verification_gas.try_into().map_err(|_| { - format!("Pre-verification gas {} exceeds maximum supported value", pre_verification_gas) - })?; - - let max_fee_per_gas = max_fee_per_gas.try_into().map_err(|_| { - format!("Max fee per gas {} exceeds maximum supported value", max_fee_per_gas) - })?; - - let max_priority_fee_per_gas = max_priority_fee_per_gas.try_into().map_err(|_| { - format!( - "Max priority fee per gas {} exceeds maximum supported value", - max_priority_fee_per_gas - ) - })?; - - // Build initial response with gas estimates - let gas_response = NativeTaskOk::EstimateUserOpGas { - call_gas_limit, - verification_gas_limit, - pre_verification_gas, - paymaster_verification_gas_limit, - paymaster_post_op_gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - estimated_token_cost: None, // Will be updated if ERC20 paymaster is detected - }; - - let estimated_token_cost = if !user_op.paymasterAndData.is_empty() { - // Step 7: Calculate token cost if ERC20 paymaster is present - calculate_erc20_token_cost( - binance_api, - &user_op.paymasterAndData, - &gas_response, - &user_op, - chain_id, - ) - .await - } else { - None - }; - - // Update response with token cost estimate - let final_response = NativeTaskOk::EstimateUserOpGas { - call_gas_limit, - verification_gas_limit, - pre_verification_gas, - paymaster_verification_gas_limit, - paymaster_post_op_gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - estimated_token_cost, - }; - - info!("Gas estimation complete: {:?}", final_response); - Ok(final_response) -} - -/// Binary search for optimal call gas limit following Rundler's approach -async fn estimate_call_gas_limit( - entry_point_client: Arc>, - user_op: aa_contracts_client::PackedUserOperation, - chain_id: ChainId, -) -> Result { - // Determine gas limits based on operation type - let (min_gas, max_gas) = if !user_op.initCode.is_empty() { - // Deployment operation requires higher gas limits - (U256::from(MIN_DEPLOYMENT_GAS), U256::from(MAX_DEPLOYMENT_GAS)) - } else { - // Normal operation - (U256::from(MIN_TRANSACTION_GAS), U256::from(MAX_NORMAL_GAS)) - }; - - // Step 1: Initial simulation at maximum to get baseline gas usage - let mut test_user_op = user_op.clone(); - let verification_gas = U256::from(DEFAULT_VERIFICATION_GAS_FOR_TESTING); - test_user_op.accountGasLimits = - pack_account_gas_limits(verification_gas.to::(), max_gas.to::()); - - let initial_result = entry_point_client - .simulate_handle_ops(&vec![test_user_op], Address::ZERO) - .await - .map_err(|e| format!("Initial simulation failed: {:?}", e))?; - - if initial_result.is_empty() || !initial_result[0].targetSuccess { - return Err("UserOperation validation failed at maximum gas limit".to_string()); - } - - // Extract actual gas used from the successful simulation - let initial_gas_used = initial_result[0].paid; - - // Step 2: Set initial guess as 2x the gas used (accounts for 63/64ths rule) - let initial_guess = initial_gas_used.saturating_mul(U256::from(2)).min(max_gas); - - info!("Initial gas simulation: used={}, initial_guess={}", initial_gas_used, initial_guess); - - // Step 3: Binary search with 10% tolerance (following Rundler's approach) - let mut lower_bound = min_gas; - let mut upper_bound = initial_guess; - let mut iterations = 0; - const MAX_ITERATIONS: u32 = 20; - const TOLERANCE_PERCENT: u64 = 10; // Stop when bounds are within 10% - - while iterations < MAX_ITERATIONS { - // Check if we've converged within tolerance - if lower_bound > U256::ZERO { - let range = upper_bound - lower_bound; - let tolerance_threshold = lower_bound / U256::from(TOLERANCE_PERCENT); - - if range <= tolerance_threshold { - debug!( - "Binary search converged after {} iterations: range={}, threshold={}", - iterations, range, tolerance_threshold - ); - break; - } - } - - let mid_gas = (lower_bound + upper_bound) / U256::from(2); - - // Test if this gas limit works - let mut test_user_op = user_op.clone(); - test_user_op.accountGasLimits = - pack_account_gas_limits(verification_gas.to::(), mid_gas.to::()); - - let test_result = - entry_point_client.simulate_handle_ops(&vec![test_user_op], Address::ZERO).await; - - match test_result { - Ok(results) if !results.is_empty() && results[0].targetSuccess => { - // Simulation succeeded, try lower gas - upper_bound = mid_gas; - debug!("Binary search iteration {}: gas {} succeeded", iterations, mid_gas); - }, - _ => { - // Simulation failed, need more gas - lower_bound = mid_gas + U256::from(1); - debug!("Binary search iteration {}: gas {} failed", iterations, mid_gas); - }, - } - - iterations += 1; - } - - // Use the upper bound as our estimate (ensures success) - let optimal_gas = upper_bound; - - // Add safety buffer based on chain - let buffer_percent = match chain_id { - 1 => 50, // Mainnet: 50% buffer - 42161 => 30, // Arbitrum: 30% buffer - 8453 => 30, // Base: 30% buffer - 56 => 40, // BSC: 40% buffer - 80084 => 20, // HyperEVM: 20% buffer - _ => 40, // Default: 40% buffer - }; - - let final_gas = optimal_gas.saturating_mul(U256::from(100 + buffer_percent)) / U256::from(100); - - info!("Call gas limit estimation: optimal={}, with_buffer={}", optimal_gas, final_gas); - Ok(final_gas) -} - -/// Calculate preVerificationGas split into static and dynamic components -fn calculate_pre_verification_gas( - user_op: &aa_contracts_client::PackedUserOperation, - chain_id: ChainId, -) -> (U256, U256) { - // EIP-2028 gas costs - const GAS_PER_ZERO_BYTE: u64 = 4; - const GAS_PER_NON_ZERO_BYTE: u64 = 16; - const BASE_TRANSACTION_GAS: u64 = 21_000; - const CREATE2_OVERHEAD_GAS: u64 = 32_000; - const BUNDLE_OVERHEAD_GAS: u64 = 5_000; // Per-UserOp share of bundle transaction overhead - - // Helper function to calculate gas for bytes - let calculate_bytes_gas = |data: &[u8]| -> U256 { - let zero_bytes = data.iter().filter(|&&b| b == 0).count() as u64; - let non_zero_bytes = (data.len() as u64) - zero_bytes; - U256::from(zero_bytes * GAS_PER_ZERO_BYTE + non_zero_bytes * GAS_PER_NON_ZERO_BYTE) - }; - - // === STATIC PVG === - // These costs don't change based on network conditions - let mut static_gas = U256::from(BASE_TRANSACTION_GAS + BUNDLE_OVERHEAD_GAS); - - // Calculate gas for UserOp calldata that will be included in the bundle - static_gas += calculate_bytes_gas(&user_op.callData); - static_gas += calculate_bytes_gas(&user_op.initCode); - static_gas += calculate_bytes_gas(&user_op.paymasterAndData); - static_gas += calculate_bytes_gas(&user_op.signature); - - // Add gas for fixed-size fields - // sender (address as bytes20 padded to bytes32) - static_gas += U256::from(20 * GAS_PER_NON_ZERO_BYTE + 12 * GAS_PER_ZERO_BYTE); - // nonce (usually has many zero bytes) - static_gas += U256::from(32 * GAS_PER_ZERO_BYTE); - // accountGasLimits (bytes32) - static_gas += U256::from(16 * GAS_PER_NON_ZERO_BYTE + 16 * GAS_PER_ZERO_BYTE); - // preVerificationGas (uint256) - static_gas += U256::from(8 * GAS_PER_NON_ZERO_BYTE + 24 * GAS_PER_ZERO_BYTE); - // gasFees (bytes32) - static_gas += U256::from(16 * GAS_PER_NON_ZERO_BYTE + 16 * GAS_PER_ZERO_BYTE); - - // Add deployment overhead if initCode is present - if !user_op.initCode.is_empty() { - static_gas += U256::from(CREATE2_OVERHEAD_GAS); - } - - // === DYNAMIC PVG === - // L2-specific costs that can change based on L1 gas prices - let dynamic_gas = calculate_l2_data_cost(user_op, chain_id); - - // Apply buffers - let static_with_buffer = static_gas.saturating_mul(U256::from(110)) / U256::from(100); // 10% buffer - let dynamic_with_buffer = if dynamic_gas > U256::ZERO { - // L2s need higher buffer due to L1 gas price volatility - dynamic_gas.saturating_mul(U256::from(125)) / U256::from(100) // 25% buffer for L2 - } else { - U256::ZERO - }; - - debug!( - "PreVerificationGas: static={}, dynamic={} (chain_id={}, total_bytes={})", - static_with_buffer, - dynamic_with_buffer, - chain_id, - user_op.callData.len() + user_op.initCode.len() + user_op.paymasterAndData.len() - ); - - (static_with_buffer, dynamic_with_buffer) -} - -/// Calculate L2-specific data availability costs -fn calculate_l2_data_cost( - user_op: &aa_contracts_client::PackedUserOperation, - chain_id: ChainId, -) -> U256 { - // Check if this is an L2 network - let is_l2 = matches!( - chain_id, - 42161 | 421614 | // Arbitrum One, Arbitrum Sepolia - 10 | 11155420 | // Optimism, Optimism Sepolia - 8453 | 84532 | // Base, Base Sepolia - 137 | 80001 // Polygon, Mumbai - ); - - if !is_l2 { - return U256::ZERO; - } - - // Calculate total calldata size that needs to be posted to L1 - let total_bytes = user_op.callData.len() - + user_op.initCode.len() - + user_op.paymasterAndData.len() - + user_op.signature.len() - + 32 * 5; // Fixed fields - - // L2-specific multipliers (these would ideally come from an oracle) - // These are rough estimates - production should use actual L1 gas price oracles - let l1_data_cost_per_byte = match chain_id { - 42161 | 421614 => U256::from(140), // Arbitrum (uses Nitro compression) - 10 | 11155420 => U256::from(160), // Optimism (uses bedrock compression) - 8453 | 84532 => U256::from(160), // Base (same as Optimism) - 137 | 80001 => U256::from(50), // Polygon (cheaper as sidechain) - _ => U256::from(100), // Default for unknown L2s - }; - - let dynamic_cost = U256::from(total_bytes) * l1_data_cost_per_byte; - - debug!( - "L2 data cost calculation: chain_id={}, bytes={}, cost_per_byte={}, total={}", - chain_id, total_bytes, l1_data_cost_per_byte, dynamic_cost - ); - - dynamic_cost -} - -/// Extract and validate paymaster gas limits from paymasterAndData field -fn extract_paymaster_gas_limits(paymaster_and_data: &Bytes) -> (u128, u128) { - // If no paymaster, return zeros - if paymaster_and_data.len() < 20 { - return (0, 0); - } - - // paymasterAndData format: - // [0:20] - paymaster address - // [20:36] - paymaster verification gas limit (uint128) - // [36:52] - paymaster post-op gas limit (uint128) - // [52:] - paymaster data - // check UserOperationLib.sol - - if paymaster_and_data.len() >= 52 { - // Extract verification gas limit (bytes 20-36) - let mut verification_bytes = [0u8; 16]; - verification_bytes.copy_from_slice(&paymaster_and_data[20..36]); - let verification_gas_limit = u128::from_be_bytes(verification_bytes); - - // Extract post-op gas limit (bytes 36-52) - let mut post_op_bytes = [0u8; 16]; - post_op_bytes.copy_from_slice(&paymaster_and_data[36..52]); - let post_op_gas_limit = u128::from_be_bytes(post_op_bytes); - - // Validate gas limits to prevent abuse - let validated_verification = if verification_gas_limit > MAX_PAYMASTER_GAS { - debug!( - "Paymaster verification gas {} exceeds maximum, using default", - verification_gas_limit - ); - DEFAULT_PAYMASTER_VERIFICATION_GAS - } else if verification_gas_limit == 0 { - debug!("Paymaster verification gas is zero, using default"); - DEFAULT_PAYMASTER_VERIFICATION_GAS - } else { - verification_gas_limit - }; - - let validated_post_op = if post_op_gas_limit > MAX_PAYMASTER_GAS { - debug!("Paymaster post-op gas {} exceeds maximum, using default", post_op_gas_limit); - DEFAULT_PAYMASTER_POST_OP_GAS - } else if post_op_gas_limit == 0 { - debug!("Paymaster post-op gas is zero, using default"); - DEFAULT_PAYMASTER_POST_OP_GAS - } else { - post_op_gas_limit - }; - - debug!( - "Validated paymaster gas limits: verification={}, post_op={}", - validated_verification, validated_post_op - ); - - (validated_verification, validated_post_op) - } else { - // Paymaster present but no gas limits specified, use defaults - debug!("Paymaster present but gas limits not specified, using defaults"); - (DEFAULT_PAYMASTER_VERIFICATION_GAS, DEFAULT_PAYMASTER_POST_OP_GAS) - } -} - -async fn print_account_state( - hypercore_client: &hyperliquid::HyperCoreClient, - user_address: &str, - label: &str, -) { - use tracing::info; - - info!("========== Account State: {} ==========", label); - - // Print spot balances - match hypercore_client.get_spot_clearinghouse_state(user_address).await { - Ok(spot_state) => { - info!("Spot Balances:"); - for balance in &spot_state.balances { - let total: f64 = balance.total.parse().unwrap_or(0.0); - let hold: f64 = balance.hold.parse().unwrap_or(0.0); - if total > 0.0 || hold > 0.0 { - info!(" {} - Total: {}, Hold: {}", balance.coin, balance.total, balance.hold); - } - } - }, - Err(e) => { - info!("Failed to fetch spot balances: {}", e); - }, - } - - // Print perp clearinghouse state - match hypercore_client.get_perp_clearinghouse_state(user_address).await { - Ok(perp_state) => { - info!("Perp Margin Summary:"); - info!( - " Account Value: {}, Total Margin Used: {}, Withdrawable: {}", - perp_state.margin_summary.account_value, - perp_state.margin_summary.total_margin_used, - perp_state.withdrawable - ); - - if !perp_state.asset_positions.is_empty() { - info!("Open Positions:"); - for asset_pos in &perp_state.asset_positions { - let pos = &asset_pos.position; - info!( - " {} - Size: {}, Entry Px: {}, Position Value: {}, Unrealized PnL: {}, Leverage: {}x", - pos.coin, - pos.szi, - pos.entry_px.as_ref().unwrap_or(&"N/A".to_string()), - pos.position_value, - pos.unrealized_pnl, - pos.leverage.value - ); - } - } else { - info!("Open Positions: None"); - } - }, - Err(e) => { - info!("Failed to fetch perp clearinghouse state: {}", e); - }, - } - - info!("=========================================="); -} - -#[allow(clippy::too_many_arguments)] -/// Validates all loan request parameters before executing trades -/// Returns (user_balance, lending_ratio_f64) on success -async fn validate_loan_request_parameters( - hypercore_client: &hyperliquid::HyperCoreClient, - smart_wallet_address: &str, - collateral_ticker: &str, - collateral_size: f64, - collateral_token: &hyperliquid::SpotToken, - perp_asset: &hyperliquid::PerpAsset, - lending_ratio: u32, - spot_market_price: f64, - perp_market_price: f64, -) -> Result<(f64, f64), NativeTaskError> { - use hyperliquid::validate_trade_size; - // 1. Validate collateral size for spot trading - validate_trade_size(collateral_size, collateral_token.sz_decimals, None).map_err(|e| { - error!("Invalid collateral size for spot trading: {}", e); - NativeTaskError::InternalError(Some(format!("Invalid collateral size: {}", e))) - })?; - - info!( - "✓ Collateral size {} validated for spot trading (sz_decimals={})", - collateral_size, collateral_token.sz_decimals - ); - - // 2. Calculate estimated values for perp position - let lending_ratio_f64 = (lending_ratio as f64) / 100.0; - let estimated_usdc_from_spot = collateral_size * spot_market_price; - let estimated_usdc_for_perp = estimated_usdc_from_spot * (1.0 - lending_ratio_f64); - let estimated_leverage = (1.0 / (1.0 - lending_ratio_f64)).min(perp_asset.max_leverage as f64); - let estimated_perp_notional = estimated_usdc_for_perp * estimated_leverage; - - // 3. Validate minimum perp order notional value ($10 minimum) - - const MIN_PERP_NOTIONAL: f64 = 10.0; // Hyperliquid: "Perp Order must have minimum value of $10." - - if estimated_perp_notional < MIN_PERP_NOTIONAL { - error!( - "Perp order notional value too small: estimated ${:.2} (collateral_size={}, spot_price={:.2}, lending_ratio={}%, leverage={:.2}x) - minimum required: ${}", - estimated_perp_notional, collateral_size, spot_market_price, lending_ratio, estimated_leverage, MIN_PERP_NOTIONAL - ); - return Err(NativeTaskError::InternalError(Some(format!( - "Perp order notional value too small: ${:.2} < ${} minimum. Increase collateral_size or decrease lending_ratio.", - estimated_perp_notional, MIN_PERP_NOTIONAL - )))); - } - - info!( - "✓ Perp notional value: ${:.2} (margin={:.2}, leverage={:.2}x) >= ${} minimum", - estimated_perp_notional, estimated_usdc_for_perp, estimated_leverage, MIN_PERP_NOTIONAL - ); - - // 4. Validate estimated hedge size can be properly rounded to perp sz_decimals - // hedge_size = (margin * leverage) / price - let estimated_hedge_size = estimated_perp_notional / perp_market_price; - - validate_trade_size(estimated_hedge_size, perp_asset.sz_decimals, None).map_err(|e| { - error!("Invalid estimated hedge size for perp trading: {}", e); - NativeTaskError::InternalError(Some(format!( - "Invalid estimated hedge size (margin={:.2}, leverage={:.2}x, perp_price={:.2}, size={}): {}", - estimated_usdc_for_perp, estimated_leverage, perp_market_price, estimated_hedge_size, e - ))) - })?; - - info!( - "✓ Estimated hedge size {} validated for perp trading (sz_decimals={})", - estimated_hedge_size, perp_asset.sz_decimals - ); - - // 5. Validate user balance - let user_balance = hypercore_client - .get_spot_balance(smart_wallet_address, collateral_ticker) - .await - .map_err(|e| { - error!("Failed to get user balance: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to query balance: {}", e))) - })?; - - if user_balance < collateral_size { - error!( - "Insufficient balance: user has {} but needs {} {}", - user_balance, collateral_size, collateral_ticker - ); - return Err(NativeTaskError::InternalError(Some(format!( - "Insufficient balance: user has {} but needs {} {}", - user_balance, collateral_size, collateral_ticker - )))); - } - - info!( - "✓ Balance check passed: user has {} {} (required: {})", - user_balance, collateral_ticker, collateral_size - ); - - Ok((user_balance, lending_ratio_f64)) -} - -#[allow(clippy::too_many_arguments)] -async fn handle_request_loan< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - ctx: Arc< - TaskHandlerContext, - >, - omni_account: executor_primitives::AccountId, - skeleton_user_op: executor_core::types::SerializablePackedUserOperation, - chain_id: u64, - wallet_index: u32, - collateral_ticker: &str, - collateral_size_str: &str, - lending_ratio: u32, - client_id: &str, -) -> NativeTaskResponse { - use hyperliquid::*; - - // Extract smart wallet address from the skeleton UserOp - let smart_wallet_address_str = &skeleton_user_op.sender; - - let hypercore_client = HyperCoreClient::new(chain_id).map_err(|e| { - error!("Failed to create HyperCore client: {}", e); - NativeTaskError::ChainNotSupported(chain_id) - })?; - - let collateral_size = collateral_size_str.parse::().map_err(|e| { - error!("Failed to parse collateral_size: {}", e); - NativeTaskError::InternalError(Some(format!("Invalid collateral_size: {}", e))) - })?; - - // Fetch metadata from HyperCore - let spot_meta = hypercore_client.get_spot_meta().await.map_err(|e| { - error!("Failed to get spot meta: {}", e); - NativeTaskError::InternalError(Some(e)) - })?; - - let meta = hypercore_client.get_meta().await.map_err(|e| { - error!("Failed to get perp meta: {}", e); - NativeTaskError::InternalError(Some(e)) - })?; - - // Get asset IDs - let spot_asset_id = get_spot_asset_id(collateral_ticker, &spot_meta).map_err(|e| { - error!("Failed to get spot asset ID: {}", e); - NativeTaskError::InternalError(Some(e)) - })?; - - let perp_asset_id = get_perp_asset_id(collateral_ticker, &meta).map_err(|e| { - error!("Failed to get perp asset ID: {}", e); - NativeTaskError::InternalError(Some(e)) - })?; - - info!( - "Resolved asset IDs - spot: {}, perp: {} for ticker: {}", - spot_asset_id, perp_asset_id, collateral_ticker - ); - - // Get token metadata for size/price calculations - let collateral_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); - NativeTaskError::InternalError(Some(format!( - "Token {} not found in spot meta", - collateral_ticker - ))) - })?; - - let usdc_token = spot_meta - .tokens - .iter() - .find(|t| t.name.eq_ignore_ascii_case("USDC")) - .ok_or_else(|| { - error!("USDC token not found in spot meta"); - NativeTaskError::InternalError(Some("USDC token not found in spot meta".to_string())) - })?; - - let perp_asset = meta.universe.get(perp_asset_id as usize).ok_or_else(|| { - error!("Perp asset {} not found in meta", perp_asset_id); - NativeTaskError::InternalError(Some(format!("Perp asset {} not found", perp_asset_id))) - })?; - - info!( - "Token metadata - collateral: weiDecimals={}, szDecimals={}, USDC: weiDecimals={}, perp: szDecimals={}, maxLeverage={}", - collateral_token.wei_decimals, - collateral_token.sz_decimals, - usdc_token.wei_decimals, - perp_asset.sz_decimals, - perp_asset.max_leverage - ); - - // Fetch market prices - use spot price for spot sell, perp price for perp hedge - let spot_market_price = hypercore_client - .get_spot_mid_price(collateral_ticker, &spot_meta) - .await - .map_err(|e| { - error!("Failed to get spot market price for {}: {}", collateral_ticker, e); - NativeTaskError::InternalError(Some(format!( - "Failed to get spot market price for {}: {}", - collateral_ticker, e - ))) - })?; - - let perp_market_price = - hypercore_client.get_perp_mid_price(collateral_ticker).await.map_err(|e| { - error!("Failed to get perp market price for {}: {}", collateral_ticker, e); - NativeTaskError::InternalError(Some(format!( - "Failed to get perp market price for {}: {}", - collateral_ticker, e - ))) - })?; - - info!( - "Market prices for {} - spot: {} USDC, perp: {} USDC", - collateral_ticker, spot_market_price, perp_market_price - ); - - // Run all early validations - let (_user_balance, lending_ratio_f64) = validate_loan_request_parameters( - &hypercore_client, - smart_wallet_address_str, - collateral_ticker, - collateral_size, - collateral_token, - perp_asset, - lending_ratio, - spot_market_price, - perp_market_price, - ) - .await?; - - // Print initial account state - print_account_state(&hypercore_client, smart_wallet_address_str, "Before Actions").await; - - // Action 1: Sell collateral_size as spot to get X USDC - // Clamp size and price to comply with HyperLiquid tick/lot size rules - let clamped_size = clamp_size(collateral_size, collateral_token.sz_decimals); - - // Calculate aggressive sell price using configured ratio - let target_price = spot_market_price * SPOT_SELL_PRICE_RATIO; - let clamped_price = clamp_price(target_price, collateral_token.sz_decimals, true); // true = spot market - - info!( - "Clamped values for spot sell - size: {} -> {}, price: {} -> {}", - collateral_size, clamped_size, target_price, clamped_price - ); - - // Use CoreWriter encoding: 10^8 * human_readable_value - let clamped_size_f64 = clamped_size.parse::().map_err(|e| { - error!("Failed to parse clamped size: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to parse clamped size: {}", e))) - })?; - let clamped_price_f64 = clamped_price.parse::().map_err(|e| { - error!("Failed to parse clamped price: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to parse clamped price: {}", e))) - })?; - - let spot_sell_size_units = (clamped_size_f64 * 100_000_000.0) as u64; - let spot_sell_price_units = (clamped_price_f64 * 100_000_000.0) as u64; - - // Generate cloids for orders (USD transfers don't use cloids) - let spot_sell_cloid = generate_cloid(); - let hedge_open_cloid = generate_cloid() + 1; - - info!( - "Generated cloids - spot_sell: {} (hex: 0x{:032x}), hedge_open: {} (hex: 0x{:032x})", - spot_sell_cloid, spot_sell_cloid, hedge_open_cloid, hedge_open_cloid - ); - - // Action 1: Build and submit spot sell action - info!( - "Building spot sell: asset_id={}, size_units={}, price_units={}, cloid={}, size_human={}, price_usdc={}", - spot_asset_id, spot_sell_size_units, spot_sell_price_units, spot_sell_cloid, clamped_size, clamped_price - ); - - let spot_sell_action = build_spot_sell_order( - spot_asset_id, - spot_sell_size_units, - spot_sell_price_units, - spot_sell_cloid, - ); - let spot_sell_corewriter_calldata = encode_send_raw_action(spot_sell_action); - - let spot_sell_calldata = - encode_omni_account_execute(get_core_writer_address(), spot_sell_corewriter_calldata); - - let spot_sell_tx_hash = submit_corewriter_userop( - ctx.clone(), - &omni_account, - &skeleton_user_op, - chain_id, - wallet_index, - spot_sell_calldata, - client_id, - ) - .await?; - - info!("Action 1: Spot sell submitted with tx_hash: {:?}", spot_sell_tx_hash); - - // Wait for order to be placed and filled by polling HyperCore API - info!("Polling HyperCore API for spot sell order completion (cloid: {})...", spot_sell_cloid); - let order_filled = hypercore_client - .wait_for_order( - smart_wallet_address_str, - &spot_sell_cloid.to_string(), - 20, - hyperliquid::OrderWaitCondition::Filled, - ) - .await - .map_err(|e| { - error!("Spot sell order did not complete: {}", e); - NativeTaskError::InternalError(Some(format!("Spot sell order failed: {}", e))) - })?; - - if !order_filled { - error!("Spot sell order was rejected or canceled"); - return Err(NativeTaskError::InternalError(Some( - "Spot sell order was rejected or canceled".to_string(), - ))); - } - - info!("Action 1: Spot sell order filled successfully"); - - // Get the actual fill to see how much USDC we received - let spot_sell_fill = hypercore_client - .get_fill_by_cloid(smart_wallet_address_str, spot_sell_cloid) - .await - .map_err(|e| { - error!("Failed to get fill for spot sell order: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to get fill: {}", e))) - })?; - - let usdc_received = calculate_usdc_received_from_spot_sell(&spot_sell_fill).map_err(|e| { - error!("Failed to calculate USDC received: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to calculate USDC: {}", e))) - })?; - - info!( - "Action 1: Spot sell completed - sold {} {} at price {} (filled: {}), received {:.2} USDC (fee: {} {})", - collateral_size, - collateral_ticker, - spot_sell_fill.px, - spot_sell_fill.sz, - usdc_received, - spot_sell_fill.fee, - spot_sell_fill.fee_token - ); - - // Print account state after Action 1 - print_account_state(&hypercore_client, smart_wallet_address_str, "After Action 1 - Spot Sell") - .await; - - // Calculate actual USDC to transfer based on USDC received from the spot sell - let usdc_for_perp = usdc_received * (1.0 - lending_ratio_f64); - let usdc_to_lend = usdc_received * lending_ratio_f64; - - info!( - "USDC allocation: total_received={:.2}, for_perp={:.2} ({:.0}%), to_lend={:.2} ({:.0}%)", - usdc_received, - usdc_for_perp, - (1.0 - lending_ratio_f64) * 100.0, - usdc_to_lend, - lending_ratio_f64 * 100.0 - ); - - // Action 2: Move usdc_for_perp into perps within HyperCore - let mut current_nonce = skeleton_user_op.nonce + 1; - let usdc_for_perp_units = (usdc_for_perp * 1_000_000.0) as u64; - let usd_transfer_action = build_usd_class_transfer_to_perp(usdc_for_perp_units); - let usd_transfer_corewriter_calldata = encode_send_raw_action(usd_transfer_action); - - let usd_transfer_calldata = - encode_omni_account_execute(get_core_writer_address(), usd_transfer_corewriter_calldata); - - // Create updated skeleton with incremented nonce for Action 2 - let mut skeleton_action2 = skeleton_user_op.clone(); - skeleton_action2.nonce = current_nonce; - skeleton_action2.init_code = "0x".to_string(); - - let usd_transfer_tx_hash = submit_corewriter_userop( - ctx.clone(), - &omni_account, - &skeleton_action2, - chain_id, - wallet_index, - usd_transfer_calldata, - client_id, - ) - .await?; - - info!("Action 2: USD class transfer submitted: {:?}", usd_transfer_tx_hash); - - // Get initial perp balance before transfer - let initial_perp_balance = hypercore_client - .get_perp_clearinghouse_state(smart_wallet_address_str) - .await - .map_err(|e| { - error!("Failed to get initial perp balance: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to query perp balance: {}", e))) - })? - .cross_margin_summary - .account_value - .parse::() - .map_err(|e| { - error!("Failed to parse initial perp balance: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to parse perp balance: {}", e))) - })?; - - info!( - "Action 2: Initial perp balance: {:.2} USDC, expecting increase of {:.2} USDC", - initial_perp_balance, usdc_for_perp - ); - - // Wait for USD transfer to complete by polling for perp account balance increase - info!( - "Polling HyperCore API for perp balance to increase by {:.2} USDC (from {:.2} to {:.2})...", - usdc_for_perp, - initial_perp_balance, - initial_perp_balance + usdc_for_perp - ); - let actual_perp_balance = hypercore_client - .wait_for_perp_balance_increase( - smart_wallet_address_str, - initial_perp_balance, - usdc_for_perp, - 20, - ) - .await - .map_err(|e| { - error!("USD transfer to perp did not complete: {}", e); - NativeTaskError::InternalError(Some(format!("USD transfer failed: {}", e))) - })?; - - info!( - "Action 2: USD class transfer completed successfully - perp account value: {:.2} USDC (increased by {:.2} from {:.2})", - actual_perp_balance, actual_perp_balance - initial_perp_balance, initial_perp_balance - ); - - // Print account state after Action 2 - print_account_state( - &hypercore_client, - smart_wallet_address_str, - "After Action 2 - USD Transfer to Perp", - ) - .await; - - // Action 3: Open hedge position - current_nonce += 1; - - // Calculate effective leverage used - let desired_leverage: f64 = 1.0 / (1.0 - lending_ratio_f64); - let effective_leverage = desired_leverage.min(perp_asset.max_leverage as f64); - - // Calculate perp size: (margin * leverage) / price - let hedge_size = (usdc_for_perp * effective_leverage) / perp_market_price; - - info!( - "Calculated hedge size: {} (margin={:.2}, leverage={:.2}x, perp_price={:.2})", - hedge_size, usdc_for_perp, effective_leverage, perp_market_price - ); - - // Clamp size and price to comply with HyperLiquid tick/lot size rules - let clamped_hedge_size = clamp_size(hedge_size, perp_asset.sz_decimals); - - // Use configured ratio for perp entry price - let target_hedge_price = perp_market_price * PERP_ENTRY_PRICE_RATIO; - let clamped_hedge_price = clamp_price(target_hedge_price, perp_asset.sz_decimals, false); // false = perp market - - info!( - "Clamped values for hedge - size: {} -> {}, price: {} -> {}", - hedge_size, clamped_hedge_size, target_hedge_price, clamped_hedge_price - ); - - // Use CoreWriter encoding: 10^8 * human_readable_value - let clamped_hedge_size_f64 = clamped_hedge_size.parse::().map_err(|e| { - error!("Failed to parse clamped hedge size: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to parse clamped hedge size: {}", e))) - })?; - let clamped_hedge_price_f64 = clamped_hedge_price.parse::().map_err(|e| { - error!("Failed to parse clamped hedge price: {}", e); - NativeTaskError::InternalError(Some(format!("Failed to parse clamped hedge price: {}", e))) - })?; - - let hedge_size_units = (clamped_hedge_size_f64 * 100_000_000.0) as u64; - let hedge_price_units = (clamped_hedge_price_f64 * 100_000_000.0) as u64; - - info!( - "Opening hedge position: margin={:.2} USDC, leverage={:.2}x (max={}), size_units={}, price_units={}, size_human={}, price_usdc={}", - usdc_for_perp, effective_leverage, perp_asset.max_leverage, hedge_size_units, hedge_price_units, clamped_hedge_size, clamped_hedge_price - ); - - let hedge_action = - build_perp_long_order(perp_asset_id, hedge_size_units, hedge_price_units, hedge_open_cloid); - let hedge_corewriter_calldata = encode_send_raw_action(hedge_action); - - let hedge_calldata = - encode_omni_account_execute(get_core_writer_address(), hedge_corewriter_calldata); - - // Create updated skeleton with incremented nonce for Action 3 - // Clear init_code since wallet is already deployed after Action 1 - let mut skeleton_action3 = skeleton_user_op.clone(); - skeleton_action3.nonce = current_nonce; - skeleton_action3.init_code = "0x".to_string(); - - let hedge_tx_hash = submit_corewriter_userop( - ctx.clone(), - &omni_account, - &skeleton_action3, - chain_id, - wallet_index, - hedge_calldata, - client_id, - ) - .await?; - - info!("Action 3: Hedge position submitted with tx_hash: {:?}", hedge_tx_hash); - - // Wait for order to be successfully opened (retrievable via HyperCore API) - info!("Polling HyperCore API to verify hedge order is opened (cloid: {})...", hedge_open_cloid); - let order_opened = hypercore_client - .wait_for_order( - smart_wallet_address_str, - &hedge_open_cloid.to_string(), - 20, - hyperliquid::OrderWaitCondition::Opened, - ) - .await - .map_err(|e| { - error!("Hedge order was not successfully opened: {}", e); - NativeTaskError::InternalError(Some(format!("Hedge order failed to open: {}", e))) - })?; - - if !order_opened { - error!("Hedge order was rejected or canceled"); - return Err(NativeTaskError::InternalError(Some( - "Hedge order was rejected or canceled".to_string(), - ))); - } - - info!("Action 3: Hedge order successfully opened"); - - // Print final account state after Action 3 - print_account_state(&hypercore_client, smart_wallet_address_str, "After Action 3 - Hedge Open") - .await; - - let usdc_received = format!("{:.2}", usdc_to_lend); - - Ok(NativeTaskOk::RequestLoan { - spot_sell_cloid: spot_sell_cloid.to_string(), - hedge_open_cloid: hedge_open_cloid.to_string(), - usdc_received, - spot_sell_tx_hash, - hedge_open_tx_hash: None, - }) -} - -async fn submit_corewriter_userop< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - ctx: Arc< - TaskHandlerContext, - >, - omni_account: &executor_primitives::AccountId, - skeleton_user_op: &executor_core::types::SerializablePackedUserOperation, - chain_id: u64, - wallet_index: u32, - call_data: String, - client_id: &str, -) -> Result, NativeTaskError> { - use executor_core::types::SerializablePackedUserOperation; - use hyperliquid::*; - - let smart_wallet_address = &skeleton_user_op.sender; - - let entry_point_client = ctx.get_entry_point_client(chain_id).ok_or_else(|| { - error!("No EntryPoint client for chain_id: {}", chain_id); - NativeTaskError::ChainNotSupported(chain_id) - })?; - - // Nonce handling: Always use nonce from skeleton_user_op and increment locally between actions - // The skeleton_user_op.nonce comes from RPC initially, then caller increments it for each subsequent action - // TODO: This assumes no concurrent transactions are sent from this smart wallet at the same time - // If concurrent transactions are possible, we need to implement proper nonce synchronization - let nonce = skeleton_user_op.nonce; - info!("Using nonce {} for smart wallet {}", nonce, smart_wallet_address); - - // Use gas settings from skeleton UserOp if provided, otherwise calculate - let (gas_fees, account_gas_limits, pre_verification_gas) = - if !skeleton_user_op.gas_fees.is_empty() - && skeleton_user_op.gas_fees != "0x" - && !skeleton_user_op.account_gas_limits.is_empty() - && skeleton_user_op.account_gas_limits != "0x" - { - info!("Using gas settings from skeleton UserOp"); - ( - skeleton_user_op.gas_fees.clone(), - skeleton_user_op.account_gas_limits.clone(), - skeleton_user_op.pre_verification_gas, - ) - } 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); - NativeTaskError::InternalError(Some("Failed to calculate gas fees".to_string())) - })?; - ( - pack_gas_fees(max_fee_per_gas.to::(), max_priority_fee_per_gas.to::()), - pack_account_gas_limits(1_000_000, 2_000_000), - 100_000, - ) - }; - - // Init_code handling: Use whatever is in skeleton_user_op (empty or not) - // For first action, skeleton contains init_code if wallet needs creation - // For subsequent actions, skeleton should have empty init_code since wallet is already deployed - let init_code = skeleton_user_op.init_code.clone(); - - // Build UserOp - let user_op = SerializablePackedUserOperation { - sender: smart_wallet_address.to_string(), - nonce, - init_code, - call_data, - account_gas_limits, - pre_verification_gas, - gas_fees, - paymaster_and_data: if !skeleton_user_op.paymaster_and_data.is_empty() - && skeleton_user_op.paymaster_and_data != "0x" - { - skeleton_user_op.paymaster_and_data.clone() - } else { - encode_simple_paymaster() - }, - signature: None, // Will be signed by SubmitUserOp handler - }; - - // Submit via existing SubmitUserOp handler - let wrapper = executor_core::native_task::NativeTaskWrapper::new( - executor_core::native_task::NativeTask::SubmitUserOp( - omni_account.clone(), - vec![user_op], - chain_id, - wallet_index, - ), - None, - None, - client_id.to_string(), - ); - - match Box::pin(handle_native_task(ctx, wrapper)).await { - Ok(NativeTaskOk::SubmitUserOp(tx_hash)) => Ok(tx_hash), - Ok(_) => Err(NativeTaskError::InternalError(Some( - "Unexpected response from SubmitUserOp".to_string(), - ))), - Err(e) => Err(e), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use aa_contracts_client::PackedUserOperation; - use alloy::{ - hex, - primitives::{Bytes, FixedBytes, U256}, - }; - use executor_core::types::SerializablePackedUserOperation; - use executor_primitives::ChainId; - - #[test] - fn test_convert_to_packed_user_op() { - let serializable_user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0xdeadbeef".to_string(), - call_data: "0xcafebabe".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: Some("0x1234567890abcdef".to_string()), - }; - - let packed_user_op = convert_to_packed_user_op(serializable_user_op) - .expect("Failed to convert SerializablePackedUserOperation"); - - // Verify the conversion - assert_eq!(packed_user_op.sender.to_string(), "0x1234567890123456789012345678901234567890"); - assert_eq!(packed_user_op.nonce, U256::from(42)); - assert_eq!(packed_user_op.initCode, Bytes::from(hex::decode("deadbeef").unwrap())); - assert_eq!(packed_user_op.callData, Bytes::from(hex::decode("cafebabe").unwrap())); - assert_eq!(packed_user_op.preVerificationGas, U256::from(21000)); - assert_eq!(packed_user_op.paymasterAndData, Bytes::from(Vec::::new())); - assert_eq!(packed_user_op.signature, Bytes::from(hex::decode("1234567890abcdef").unwrap())); - } - - #[test] - fn test_convert_to_packed_user_op_unsigned() { - let serializable_user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0xdeadbeef".to_string(), - call_data: "0xcafebabe".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, // Unsigned operation - }; - - let packed_user_op = convert_to_packed_user_op(serializable_user_op) - .expect("Failed to convert unsigned SerializablePackedUserOperation"); - - // Verify the signature is empty for unsigned operation - assert!(packed_user_op.signature.is_empty()); - assert_eq!(packed_user_op.sender.to_string(), "0x1234567890123456789012345678901234567890"); - assert_eq!(packed_user_op.nonce, U256::from(42)); - } - - #[test] - fn test_convert_to_packed_user_op_empty_init_code() { - let serializable_user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "".to_string(), // Empty init_code - call_data: "0xcafebabe".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: Some("0x1234567890abcdef".to_string()), - }; - - let result = convert_to_packed_user_op(serializable_user_op); - assert!(result.is_ok(), "Empty init_code should not cause an error: {:?}", result.err()); - - let packed_user_op = result.unwrap(); - // Empty init_code should result in empty Bytes - assert!(packed_user_op.initCode.is_empty()); - assert_eq!(packed_user_op.sender.to_string(), "0x1234567890123456789012345678901234567890"); - assert_eq!(packed_user_op.nonce, U256::from(42)); - } - - #[test] - fn test_convert_to_packed_user_op_0x_init_code() { - let serializable_user_op = SerializablePackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".to_string(), - nonce: 42, - init_code: "0x".to_string(), // "0x" prefix only - call_data: "0xcafebabe".to_string(), - account_gas_limits: - "0x0000000000000000000000000030d4000000000000000000000000000000c350".to_string(), - pre_verification_gas: 21000, - gas_fees: "0x000000000000000000000003b9aca0000000000000000000000000000b2d05e0" - .to_string(), - paymaster_and_data: "0x".to_string(), - signature: Some("0x1234567890abcdef".to_string()), - }; - - let result = convert_to_packed_user_op(serializable_user_op); - assert!(result.is_ok(), "0x init_code should not cause an error: {:?}", result.err()); - - let packed_user_op = result.unwrap(); - // "0x" init_code should result in empty Bytes - assert!(packed_user_op.initCode.is_empty()); - assert_eq!(packed_user_op.sender.to_string(), "0x1234567890123456789012345678901234567890"); - assert_eq!(packed_user_op.nonce, U256::from(42)); - } - - #[test] - fn test_pack_account_gas_limits() { - let verification_gas = 500_000u128; - let call_gas = 300_000u128; - - let packed = pack_account_gas_limits(verification_gas, call_gas); - - // Verify the packed format - let expected: U256 = (U256::from(verification_gas) << 128) | U256::from(call_gas); - assert_eq!(packed.0, expected.to_be_bytes()); - } - - #[test] - fn test_unpack_verification_gas_limit() { - // Create a packed value with verification gas = 500000, call gas = 300000 - let verification_gas = 500_000u128; - let call_gas = 300_000u128; - let packed_value: U256 = (U256::from(verification_gas) << 128) | U256::from(call_gas); - let packed_bytes = FixedBytes::from(packed_value.to_be_bytes()); - - let unpacked = unpack_verification_gas_limit(packed_bytes); - assert_eq!(unpacked, verification_gas); - } - - #[test] - fn test_unpack_call_gas_limit() { - // Create a packed value with verification gas = 500000, call gas = 300000 - let verification_gas = 500_000u128; - let call_gas = 300_000u128; - let packed_value: U256 = (U256::from(verification_gas) << 128) | U256::from(call_gas); - let packed_bytes = FixedBytes::from(packed_value.to_be_bytes()); - - let unpacked = unpack_call_gas_limit(packed_bytes); - assert_eq!(unpacked, call_gas); - } - - #[test] - fn test_pack_unpack_roundtrip() { - let test_cases = vec![ - (0u128, 0u128), - (1u128, 1u128), - (u128::MAX, u128::MAX), - (1_000_000u128, 500_000u128), - (3_000_000u128, 10_000_000u128), - ]; - - for (verification, call) in test_cases { - let packed = pack_account_gas_limits(verification, call); - let unpacked_verification = unpack_verification_gas_limit(packed); - let unpacked_call = unpack_call_gas_limit(packed); - - assert_eq!( - unpacked_verification, - verification, - "Verification gas mismatch for {:?}", - (verification, call) - ); - assert_eq!(unpacked_call, call, "Call gas mismatch for {:?}", (verification, call)); - } - } - - #[test] - fn test_calculate_pre_verification_gas_mainnet() { - // Test with a simple UserOp for mainnet (no L2 costs) - let user_op = PackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".parse().unwrap(), - nonce: U256::from(1), - initCode: Bytes::from(vec![]), - callData: Bytes::from(vec![0x00, 0x01, 0x02, 0x03]), // 4 bytes - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(0), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: Bytes::from(vec![]), - signature: Bytes::from(vec![0xff; 65]), // 65 bytes signature - }; - - let chain_id: ChainId = 1; // Ethereum mainnet - let (static_pvg, dynamic_pvg) = calculate_pre_verification_gas(&user_op, chain_id); - - // Static PVG should include base costs + calldata - // Base: 21000 + 5000 = 26000 - // Calldata: sender(20) + nonce(~3) + initCode(0) + callData(4) + signature(65) + other fields - // This is approximate since we need to calculate exact calldata costs - assert!(static_pvg > U256::from(26_000), "Static PVG should be at least base costs"); - - // Dynamic PVG should be 0 for mainnet - assert_eq!(dynamic_pvg, U256::ZERO, "Dynamic PVG should be 0 for mainnet"); - } - - #[test] - fn test_calculate_pre_verification_gas_arbitrum() { - // Test with UserOp for Arbitrum (includes L2 data costs) - let user_op = PackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".parse().unwrap(), - nonce: U256::from(1), - initCode: Bytes::from(vec![]), - callData: Bytes::from(vec![0x00; 100]), // 100 zero bytes - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(0), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: Bytes::from(vec![]), - signature: Bytes::from(vec![0xff; 65]), - }; - - let chain_id: ChainId = 42161; // Arbitrum One - let (static_pvg, dynamic_pvg) = calculate_pre_verification_gas(&user_op, chain_id); - - // Static PVG should include base costs - assert!(static_pvg > U256::from(26_000), "Static PVG should include base costs"); - - // Dynamic PVG should be non-zero for Arbitrum (140 gas per byte) - assert!(dynamic_pvg > U256::ZERO, "Dynamic PVG should be non-zero for Arbitrum"); - - // Verify L2 multiplier is applied (140 gas per byte for Arbitrum) - let total_bytes = user_op.sender.len() + 100 + 65; // Approximate total bytes - let expected_min_dynamic = U256::from(total_bytes * 140); - assert!( - dynamic_pvg >= expected_min_dynamic, - "Dynamic PVG should apply Arbitrum multiplier" - ); - } - - #[test] - fn test_calculate_pre_verification_gas_optimism() { - // Test with UserOp for Optimism - let user_op = PackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".parse().unwrap(), - nonce: U256::from(1), - initCode: Bytes::from(vec![]), - callData: Bytes::from(vec![0x01; 50]), // 50 non-zero bytes - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(0), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: Bytes::from(vec![]), - signature: Bytes::from(vec![0xff; 65]), - }; - - let chain_id: ChainId = 10; // Optimism - let (_, dynamic_pvg) = calculate_pre_verification_gas(&user_op, chain_id); - - // Dynamic PVG should use Optimism multiplier (160 gas per byte) - assert!(dynamic_pvg > U256::ZERO, "Dynamic PVG should be non-zero for Optimism"); - - // Should be higher than Arbitrum for same data - let arbitrum_chain: ChainId = 42161; // Arbitrum One - let (_, arbitrum_dynamic) = calculate_pre_verification_gas(&user_op, arbitrum_chain); - assert!( - dynamic_pvg > arbitrum_dynamic, - "Optimism should have higher dynamic PVG than Arbitrum" - ); - } - - #[test] - fn test_calculate_pre_verification_gas_with_deployment() { - // Test with initCode present (deployment scenario) - let user_op = PackedUserOperation { - sender: "0x1234567890123456789012345678901234567890".parse().unwrap(), - nonce: U256::from(0), // First transaction - initCode: Bytes::from(vec![0x60; 200]), // 200 bytes of deployment code - callData: Bytes::from(vec![]), - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(0), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: Bytes::from(vec![]), - signature: Bytes::from(vec![0xff; 65]), - }; - - let chain_id: ChainId = 1; // Ethereum mainnet - let (static_pvg, _) = calculate_pre_verification_gas(&user_op, chain_id); - - // Should include CREATE2 overhead (32000 gas) - // Base (26000) + CREATE2 (32000) + calldata costs - assert!(static_pvg > U256::from(58_000), "Static PVG should include CREATE2 overhead"); - } - - #[test] - fn test_calculate_pre_verification_gas_zero_bytes() { - // Test calldata gas calculation with all zero bytes - let user_op = PackedUserOperation { - sender: "0x0000000000000000000000000000000000000000".parse().unwrap(), - nonce: U256::from(0), - initCode: Bytes::from(vec![]), - callData: Bytes::from(vec![0x00; 1000]), // 1000 zero bytes - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(0), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: Bytes::from(vec![]), - signature: Bytes::from(vec![0x00; 65]), // All zero signature - }; - - let chain_id: ChainId = 1; // Ethereum mainnet - let (static_pvg, _) = calculate_pre_verification_gas(&user_op, chain_id); - - // Zero bytes cost 4 gas each (EIP-2028) - // Should be significantly lower than non-zero bytes - let non_zero_op = PackedUserOperation { - callData: Bytes::from(vec![0xff; 1000]), // 1000 non-zero bytes - ..user_op.clone() - }; - let (non_zero_static, _) = calculate_pre_verification_gas(&non_zero_op, chain_id); - - assert!( - static_pvg < non_zero_static, - "Zero bytes should cost less gas than non-zero bytes" - ); - } - - #[test] - fn test_extract_paymaster_gas_limits_valid() { - // Valid paymasterAndData with gas limits at bytes 20-52 - let mut paymaster_data = vec![0x11; 20]; // 20 bytes of paymaster address - - // Add verification gas limit (16 bytes, u128) - let verification_gas = 150_000u128; - paymaster_data.extend_from_slice(&verification_gas.to_be_bytes()); - - // Add post-op gas limit (16 bytes, u128) - let post_op_gas = 50_000u128; - paymaster_data.extend_from_slice(&post_op_gas.to_be_bytes()); - - // Add some extra data - paymaster_data.extend_from_slice(&[0xff; 20]); - - let paymaster_and_data = Bytes::from(paymaster_data); - let (extracted_verification, extracted_post_op) = - extract_paymaster_gas_limits(&paymaster_and_data); - - assert_eq!(extracted_verification, verification_gas, "Verification gas should match"); - assert_eq!(extracted_post_op, post_op_gas, "Post-op gas should match"); - } - - #[test] - fn test_extract_paymaster_gas_limits_short_data() { - // Data shorter than 52 bytes - let paymaster_data = vec![0x11; 30]; // Only 30 bytes - let paymaster_and_data = Bytes::from(paymaster_data); - - let (verification, post_op) = extract_paymaster_gas_limits(&paymaster_and_data); - - // Should return defaults - assert_eq!(verification, DEFAULT_PAYMASTER_VERIFICATION_GAS); - assert_eq!(post_op, DEFAULT_PAYMASTER_POST_OP_GAS); - } - - #[test] - fn test_extract_paymaster_gas_limits_empty() { - // Empty paymasterAndData - let paymaster_and_data = Bytes::from(vec![]); - - let (verification, post_op) = extract_paymaster_gas_limits(&paymaster_and_data); - - // Should return zeros for no paymaster - assert_eq!(verification, 0); - assert_eq!(post_op, 0); - } -} - -#[cfg(test)] -mod erc20_paymaster_tests { - use super::*; - use alloy::primitives::Bytes; - use binance_api::Error as BinanceApiError; - - #[test] - fn test_decode_erc20_paymaster_data_valid() { - // Create test paymaster data with correct format: - // paymaster (20) + validation_gas_limit (16) + postop_gas_limit (16) + token (20) + exchangeRate (32) + validUntil (32) + validAfter (32) - let mut data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - - // Set paymaster address (bytes 0-19) - data[0..20].copy_from_slice(&[0x12; 20]); - - // Set validation gas limit (bytes 20-35) - 100000 - let validation_gas = 100000u128; - data[20..36].copy_from_slice(&validation_gas.to_be_bytes()); - - // Set postop gas limit (bytes 36-51) - 50000 - let postop_gas = 50000u128; - data[36..52].copy_from_slice(&postop_gas.to_be_bytes()); - - // Set token address (bytes 52-71) - USDC-like token - data[52..72].copy_from_slice(&[0xA0; 20]); - - // Set exchange rate (bytes 72-103) - representing 2000 * 10^6 for USDC - let exchange_rate = 2000000000u128; - let rate_bytes = [0u8; 16] - .iter() - .chain(&exchange_rate.to_be_bytes()) - .copied() - .collect::>(); - data[72..104].copy_from_slice(&rate_bytes); - - // Set validUntil (bytes 104-135) - timestamp 1700000000 - let valid_until = 1700000000u64; - let valid_until_bytes = - [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); - data[104..136].copy_from_slice(&valid_until_bytes); - - // Set validAfter (bytes 136-167) - timestamp 1600000000 - let valid_after = 1600000000u64; - let valid_after_bytes = - [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); - data[136..168].copy_from_slice(&valid_after_bytes); - - // Test decoding - let result = decode_erc20_paymaster_data(&data); - assert!(result.is_some()); - - let (token_address, exchange_rate_result, valid_until_result, valid_after_result) = - result.unwrap(); - assert_eq!(token_address, Address::from([0xA0; 20])); - assert_eq!(exchange_rate_result, exchange_rate); - assert_eq!(valid_until_result, valid_until); - assert_eq!(valid_after_result, valid_after); - } - - #[test] - fn test_decode_erc20_paymaster_data_too_short() { - let data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH - 1]; - let result = decode_erc20_paymaster_data(&data); - assert!(result.is_none()); - } - - #[test] - fn test_encode_erc20_paymaster_data() { - // Create initial paymaster data with correct format - let mut original_data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - - // Fill with some initial values - original_data[0..20].copy_from_slice(&[0x12; 20]); // paymaster address - original_data[20..36].copy_from_slice(&100000u128.to_be_bytes()); // validation gas - original_data[36..52].copy_from_slice(&50000u128.to_be_bytes()); // postop gas - original_data[52..72].copy_from_slice(&[0xA0; 20]); // token address - - // Set initial exchange rate (bytes 72-103) - let initial_rate = 1500000000u128; - let rate_bytes = [0u8; 16] - .iter() - .chain(&initial_rate.to_be_bytes()) - .copied() - .collect::>(); - original_data[72..104].copy_from_slice(&rate_bytes); - - // Test encoding new values - let new_exchange_rate = 3000000000u128; - let new_valid_until = 1800000000u64; - let new_valid_after = 1700000000u64; - - let updated_data = encode_erc20_paymaster_data( - &original_data, - new_exchange_rate, - new_valid_until, - new_valid_after, - ); - - // Verify the updated data contains the new values - let result = decode_erc20_paymaster_data(&updated_data); - assert!(result.is_some()); - - let (token_address, exchange_rate_result, valid_until, valid_after) = result.unwrap(); - assert_eq!(token_address, Address::from([0xA0; 20])); // Token address should remain unchanged - assert_eq!(exchange_rate_result, new_exchange_rate); - assert_eq!(valid_until, new_valid_until); - assert_eq!(valid_after, new_valid_after); - } - - #[test] - fn test_supported_token_mapping_consistency() { - let tokens = get_supported_tokens(); - - // Verify that we have tokens for major chains - let eth_tokens: Vec<_> = tokens.keys().filter(|(chain_id, _)| *chain_id == 1).collect(); - assert!(eth_tokens.len() >= 3, "Should have at least 3 tokens on Ethereum mainnet"); - - let arbitrum_tokens: Vec<_> = - tokens.keys().filter(|(chain_id, _)| *chain_id == 42161).collect(); - assert!(arbitrum_tokens.len() >= 2, "Should have at least 2 tokens on Arbitrum"); - } - - #[tokio::test] - async fn test_calculate_erc20_token_cost_valid() { - // Mock Binance API - struct MockBinanceApi; - #[async_trait::async_trait] - impl BinancePaymasterApi for MockBinanceApi { - async fn get_symbol_price(&self, symbol: &str) -> Result { - // Return mock price for ETHUSDC: 3000 USDC per ETH - if symbol == "ETHUSDC" { - Ok("3000.00".to_string()) - } else { - Err(BinanceApiError::SymbolNotFound) - } - } - - async fn get_symbol_precision(&self, _symbol: &str) -> Result { - Ok(6) - } - - async fn get_all_trading_symbols(&self) -> Result, BinanceApiError> { - Ok(vec!["ETHUSDC".to_string()]) - } - } - - // Create mock paymaster data with USDC token - let mut paymaster_data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - // Set paymaster address (bytes 0-19) - paymaster_data[0..20].copy_from_slice(&[0x12; 20]); - // Set validation gas limit (bytes 20-35) - 100000 - paymaster_data[20..36].copy_from_slice(&100000u128.to_be_bytes()); - // Set postop gas limit (bytes 36-51) - 50000 - paymaster_data[36..52].copy_from_slice(&50000u128.to_be_bytes()); - // Set USDC token address on Ethereum mainnet (bytes 52-71) - let usdc_address_bytes = hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - paymaster_data[52..72].copy_from_slice(&usdc_address_bytes); - - // Set exchange rate (bytes 72-103) - will be recalculated - let exchange_rate = 0u128; // Placeholder - let rate_bytes = [0u8; 16] - .iter() - .chain(&exchange_rate.to_be_bytes()) - .copied() - .collect::>(); - paymaster_data[72..104].copy_from_slice(&rate_bytes); - - // Set validUntil to future timestamp - let valid_until = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - + 3600; // 1 hour from now - let valid_until_bytes = - [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); - paymaster_data[104..136].copy_from_slice(&valid_until_bytes); - - // Set validAfter to past timestamp - let valid_after = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - - 3600; // 1 hour ago - let valid_after_bytes = - [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); - paymaster_data[136..168].copy_from_slice(&valid_after_bytes); - - let paymaster_and_data = Bytes::from(paymaster_data); - - // Create mock gas estimates - let gas_estimates = NativeTaskOk::EstimateUserOpGas { - call_gas_limit: 100000, - verification_gas_limit: 200000, - pre_verification_gas: 50000, - paymaster_verification_gas_limit: 100000, - paymaster_post_op_gas_limit: 50000, - max_fee_per_gas: 1000000000, - max_priority_fee_per_gas: 100000000, - estimated_token_cost: None, - }; - - // Create mock UserOp with gas fees - let mut gas_fees_bytes = [0u8; 32]; - // maxFeePerGas: 30 gwei = 30_000_000_000 - let max_fee = 30_000_000_000u128; - gas_fees_bytes[0..16].copy_from_slice(&max_fee.to_be_bytes()); - // maxPriorityFeePerGas: 2 gwei = 2_000_000_000 - let priority_fee = 2_000_000_000u128; - gas_fees_bytes[16..32].copy_from_slice(&priority_fee.to_be_bytes()); - - let user_op = aa_contracts_client::PackedUserOperation { - sender: Address::from([0x01; 20]), - nonce: U256::from(0), - initCode: Bytes::new(), - callData: Bytes::new(), - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(50000), - gasFees: FixedBytes::from(gas_fees_bytes), - paymasterAndData: paymaster_and_data.clone(), - signature: Bytes::new(), - }; - - let mock_api = MockBinanceApi; - let result = calculate_erc20_token_cost( - &mock_api, - &paymaster_and_data, - &gas_estimates, - &user_op, - 1, // Ethereum mainnet - ) - .await; - - assert!(result.is_some()); - let token_cost = result.unwrap(); - - // Verify the token cost estimate - assert_eq!(token_cost.decimals, 6); // USDC has 6 decimals - assert!(token_cost.amount > 0); - // The amount should be reasonable (not too high) - // Total gas = 500k, gas price = 30 gwei, ETH price = 3000 USDC - // Expected cost ~= 500000 * 30e9 * 3000 / 1e18 = 45 USDC - // With 10% buffer = 49.5 USDC = 49,500,000 in smallest unit - let expected_min = 40_000_000u128; // 40 USDC - let expected_max = 60_000_000u128; // 60 USDC - assert!( - token_cost.amount >= expected_min && token_cost.amount <= expected_max, - "Token cost {} should be between {} and {}", - token_cost.amount, - expected_min, - expected_max - ); - } - - #[tokio::test] - async fn test_calculate_erc20_token_cost_rounds_up() { - struct MockBinanceApi; - #[async_trait::async_trait] - impl BinancePaymasterApi for MockBinanceApi { - async fn get_symbol_price(&self, symbol: &str) -> Result { - if symbol == "ETHUSDC" { - Ok("3000.00".to_string()) - } else { - Err(BinanceApiError::SymbolNotFound) - } - } - - async fn get_symbol_precision(&self, _symbol: &str) -> Result { - Ok(6) - } - - async fn get_all_trading_symbols(&self) -> Result, BinanceApiError> { - Ok(vec!["ETHUSDC".to_string()]) - } - } - - let mut paymaster_data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - paymaster_data[0..20].copy_from_slice(&[0x12; 20]); - paymaster_data[20..36].copy_from_slice(&100000u128.to_be_bytes()); - paymaster_data[36..52].copy_from_slice(&50000u128.to_be_bytes()); - let usdc_address_bytes = hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - paymaster_data[52..72].copy_from_slice(&usdc_address_bytes); - let rate_bytes = [0u8; 16].iter().chain(&0u128.to_be_bytes()).copied().collect::>(); - paymaster_data[72..104].copy_from_slice(&rate_bytes); - let valid_until = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - + 3600; - let valid_until_bytes = - [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); - paymaster_data[104..136].copy_from_slice(&valid_until_bytes); - let valid_after = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - - 3600; - let valid_after_bytes = - [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); - paymaster_data[136..168].copy_from_slice(&valid_after_bytes); - - let paymaster_and_data = Bytes::from(paymaster_data); - - let gas_estimates = NativeTaskOk::EstimateUserOpGas { - call_gas_limit: 1, - verification_gas_limit: 0, - pre_verification_gas: 0, - paymaster_verification_gas_limit: 0, - paymaster_post_op_gas_limit: 0, - max_fee_per_gas: 1000000000, - max_priority_fee_per_gas: 100000000, - estimated_token_cost: None, - }; - - let mut gas_fees_bytes = [0u8; 32]; - let max_fee = 1u128; - gas_fees_bytes[0..16].copy_from_slice(&max_fee.to_be_bytes()); - let priority_fee = 0u128; - gas_fees_bytes[16..32].copy_from_slice(&priority_fee.to_be_bytes()); - - let user_op = aa_contracts_client::PackedUserOperation { - sender: Address::from([0x01; 20]), - nonce: U256::from(0), - initCode: Bytes::new(), - callData: Bytes::new(), - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(0), - gasFees: FixedBytes::from(gas_fees_bytes), - paymasterAndData: paymaster_and_data.clone(), - signature: Bytes::new(), - }; - - let mock_api = MockBinanceApi; - let result = - calculate_erc20_token_cost(&mock_api, &paymaster_and_data, &gas_estimates, &user_op, 1) - .await; - - let token_cost = result.expect("Expected token cost to be calculated"); - assert_eq!(token_cost.amount, 1); - } - - #[tokio::test] - async fn test_calculate_erc20_token_cost_invalid_timestamps() { - struct MockBinanceApi; - #[async_trait::async_trait] - impl BinancePaymasterApi for MockBinanceApi { - async fn get_symbol_price(&self, _symbol: &str) -> Result { - Ok("3000.00".to_string()) - } - - async fn get_symbol_precision(&self, _symbol: &str) -> Result { - Ok(6) - } - - async fn get_all_trading_symbols(&self) -> Result, BinanceApiError> { - Ok(vec!["ETHUSDC".to_string()]) - } - } - - // Create paymaster data with expired timestamps - let mut paymaster_data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - paymaster_data[0..20].copy_from_slice(&[0x12; 20]); - paymaster_data[20..36].copy_from_slice(&100000u128.to_be_bytes()); - paymaster_data[36..52].copy_from_slice(&50000u128.to_be_bytes()); - - // Set USDC token address - let usdc_address_bytes = hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - paymaster_data[52..72].copy_from_slice(&usdc_address_bytes); - - // Set validUntil to past timestamp (expired) - let valid_until = 1000u64; // Very old timestamp - let valid_until_bytes = - [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); - paymaster_data[104..136].copy_from_slice(&valid_until_bytes); - - // Set validAfter to recent timestamp - let valid_after = 900u64; - let valid_after_bytes = - [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); - paymaster_data[136..168].copy_from_slice(&valid_after_bytes); - - let paymaster_and_data = Bytes::from(paymaster_data); - - // Create mock gas estimates - let gas_estimates = NativeTaskOk::EstimateUserOpGas { - call_gas_limit: 100000, - verification_gas_limit: 200000, - pre_verification_gas: 50000, - paymaster_verification_gas_limit: 100000, - paymaster_post_op_gas_limit: 50000, - max_fee_per_gas: 1000000000, - max_priority_fee_per_gas: 100000000, - estimated_token_cost: None, - }; - - // Create mock UserOp - let user_op = aa_contracts_client::PackedUserOperation { - sender: Address::from([0x01; 20]), - nonce: U256::from(0), - initCode: Bytes::new(), - callData: Bytes::new(), - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(50000), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: paymaster_and_data.clone(), - signature: Bytes::new(), - }; - - let mock_api = MockBinanceApi; - let result = - calculate_erc20_token_cost(&mock_api, &paymaster_and_data, &gas_estimates, &user_op, 1) - .await; - - // Should return None due to expired timestamps - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_calculate_erc20_token_cost_unsupported_token() { - struct MockBinanceApi; - #[async_trait::async_trait] - impl BinancePaymasterApi for MockBinanceApi { - async fn get_symbol_price(&self, _symbol: &str) -> Result { - Ok("3000.00".to_string()) - } - - async fn get_symbol_precision(&self, _symbol: &str) -> Result { - Ok(6) - } - - async fn get_all_trading_symbols(&self) -> Result, BinanceApiError> { - Ok(vec!["ETHUSDC".to_string()]) - } - } - - // Create paymaster data with unsupported token - let mut paymaster_data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - paymaster_data[0..20].copy_from_slice(&[0x12; 20]); - paymaster_data[20..36].copy_from_slice(&100000u128.to_be_bytes()); - paymaster_data[36..52].copy_from_slice(&50000u128.to_be_bytes()); - - // Set unsupported token address - paymaster_data[52..72].copy_from_slice(&[0xFF; 20]); - - // Set valid timestamps - let valid_until = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - + 3600; - let valid_until_bytes = - [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); - paymaster_data[104..136].copy_from_slice(&valid_until_bytes); - - let valid_after = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - - 3600; - let valid_after_bytes = - [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); - paymaster_data[136..168].copy_from_slice(&valid_after_bytes); - - let paymaster_and_data = Bytes::from(paymaster_data); - - let gas_estimates = NativeTaskOk::EstimateUserOpGas { - call_gas_limit: 100000, - verification_gas_limit: 200000, - pre_verification_gas: 50000, - paymaster_verification_gas_limit: 100000, - paymaster_post_op_gas_limit: 50000, - max_fee_per_gas: 1000000000, - max_priority_fee_per_gas: 100000000, - estimated_token_cost: None, - }; - - let user_op = aa_contracts_client::PackedUserOperation { - sender: Address::from([0x01; 20]), - nonce: U256::from(0), - initCode: Bytes::new(), - callData: Bytes::new(), - accountGasLimits: FixedBytes::from([0u8; 32]), - preVerificationGas: U256::from(50000), - gasFees: FixedBytes::from([0u8; 32]), - paymasterAndData: paymaster_and_data.clone(), - signature: Bytes::new(), - }; - - let mock_api = MockBinanceApi; - let result = - calculate_erc20_token_cost(&mock_api, &paymaster_and_data, &gas_estimates, &user_op, 1) - .await; - - // Should return None due to unsupported token - assert!(result.is_none()); - } - - #[test] - fn test_get_token_info_from_mapping_unknown_token() { - let token_address = Address::from([0xFF; 20]); - let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(get_token_info_from_mapping(&token_address, 1)); - assert!(result.is_none()); - } - - #[test] - fn test_supported_tokens_ethereum_mainnet() { - let tokens = get_supported_tokens(); - - // Test USDC on Ethereum mainnet - let usdc_info = tokens.get(&(1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")); - assert!(usdc_info.is_some()); - let usdc = usdc_info.unwrap(); - assert_eq!(usdc.decimals, 6); - assert_eq!(usdc.binance_pair, "ETHUSDC"); - // Extract symbol from binance_pair: ETHUSDC -> USDC - let symbol = usdc.binance_pair.strip_prefix("ETH").unwrap_or(""); - assert_eq!(symbol, "USDC"); - - // Test USDT on Ethereum mainnet - let usdt_info = tokens.get(&(1, "0xdac17f958d2ee523a2206206994597c13d831ec7")); - assert!(usdt_info.is_some()); - let usdt = usdt_info.unwrap(); - assert_eq!(usdt.decimals, 6); - assert_eq!(usdt.binance_pair, "ETHUSDT"); - // Extract symbol from binance_pair: ETHUSDT -> USDT - let symbol = usdt.binance_pair.strip_prefix("ETH").unwrap_or(""); - assert_eq!(symbol, "USDT"); - } - - #[test] - fn test_supported_tokens_arbitrum() { - let tokens = get_supported_tokens(); - - // Test USDC on Arbitrum - let usdc_info = tokens.get(&(42161, "0xaf88d065e77c8cc2239327c5edb3a432268e5831")); - assert!(usdc_info.is_some()); - let usdc = usdc_info.unwrap(); - assert_eq!(usdc.decimals, 6); - // Extract symbol from binance_pair: ETHUSDC -> USDC - let symbol = usdc.binance_pair.strip_prefix("ETH").unwrap_or(""); - assert_eq!(symbol, "USDC"); - } - - #[test] - fn test_get_token_info_unsupported_token() { - let rt = tokio::runtime::Runtime::new().unwrap(); - let token_address = Address::from([0xFF; 20]); - let result = rt.block_on(get_token_info_from_mapping(&token_address, 999)); - assert!(result.is_none()); - } - - #[test] - fn test_process_erc20_paymaster_data_invalid_length() { - let rt = tokio::runtime::Runtime::new().unwrap(); - - // Create a mock BinanceApiClient (won't be used in this test) - let binance_client = binance_api::BinanceApiClient::new( - "test_key".to_string(), - "test_secret".to_string(), - "https://api.binance.com".to_string(), - ); - - // Create paymaster data that's too short (not ERC20 paymaster format) - let data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH - 10]; - let paymaster_and_data = Bytes::from(data); - - let result = rt.block_on(process_erc20_paymaster_data( - &binance_client as &dyn BinancePaymasterApi, - &paymaster_and_data, - 1, - )); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); // Should return None for non-ERC20 paymaster - } - - #[test] - fn test_exchange_rate_calculation_usdc() { - // Test USDC (6 decimals) at $4000/ETH - let token_decimals = 6; - let tokens_per_eth = 4000.0; - - let expected_rate = (tokens_per_eth * 10_f64.powi(token_decimals)) as u128; - // 4000 * 10^6 = 4000000000 - assert_eq!(expected_rate, 4000000000u128); - - // Test the actual calculation would work: - // If maxCost is 1 ETH (10^18 wei), then: - // requiredTokenAmount = (10^18 * 4000000000) / 10^18 = 4000000000 USDC units = 4000 USDC ✓ - } - - #[test] - fn test_exchange_rate_calculation_dai() { - // Test DAI (18 decimals) at $4000/ETH - let token_decimals = 18; - let tokens_per_eth = 4000.0; - - let expected_rate = (tokens_per_eth * 10_f64.powi(token_decimals)) as u128; - // 4000 * 10^18 = 4000000000000000000000 - assert_eq!(expected_rate, 4000000000000000000000u128); - } - - #[test] - fn test_paymaster_data_format_validation() { - // Test that we correctly validate the minimum length - assert_eq!(MIN_ERC20_PAYMASTER_DATA_LENGTH, 168); - // 52 (paymaster + gas limits) + 20 (token) + 32 (rate) + 32 (until) + 32 (after) = 168 - - // Test structure offsets - assert_eq!(PAYMASTER_DATA_OFFSET, 52); - } - - #[test] - fn test_paymaster_data_alignment_with_contract() { - // This test ensures our format exactly matches ERC20PaymasterV1.sol - let mut data = vec![0u8; MIN_ERC20_PAYMASTER_DATA_LENGTH]; - - // Contract expects: paymaster(20) + validation_gas(16) + postop_gas(16) + token(20) + exchangeRate(32) + validUntil(32) + validAfter(32) - - // Paymaster address - data[0..20].copy_from_slice(&[0x11; 20]); - - // Validation gas limit - data[20..36].copy_from_slice(&150000u128.to_be_bytes()); - - // PostOp gas limit - data[36..52].copy_from_slice(&50000u128.to_be_bytes()); - - // Token address (from PAYMASTER_DATA_OFFSET) - let token_addr = [0xA0; 20]; - data[52..72].copy_from_slice(&token_addr); - - // Exchange rate - let rate = 2500000000u128; // 2500 USDC per ETH - let rate_bytes = [0u8; 16].iter().chain(&rate.to_be_bytes()).copied().collect::>(); - data[72..104].copy_from_slice(&rate_bytes); - - // ValidUntil - let until = 1800000000u64; - let until_bytes = - [0u8; 24].iter().chain(&until.to_be_bytes()).copied().collect::>(); - data[104..136].copy_from_slice(&until_bytes); - - // ValidAfter - let after = 1700000000u64; - let after_bytes = - [0u8; 24].iter().chain(&after.to_be_bytes()).copied().collect::>(); - data[136..168].copy_from_slice(&after_bytes); - - // Test decoding matches what we encoded - let result = decode_erc20_paymaster_data(&data).unwrap(); - assert_eq!(result.0, Address::from(token_addr)); - assert_eq!(result.1, rate); - assert_eq!(result.2, until); - assert_eq!(result.3, after); - - // Test re-encoding preserves the structure - let updated = encode_erc20_paymaster_data(&data, rate + 100, until + 100, after + 100); - let redecoded = decode_erc20_paymaster_data(&updated).unwrap(); - assert_eq!(redecoded.0, Address::from(token_addr)); // Token unchanged - assert_eq!(redecoded.1, rate + 100); // Rate updated - assert_eq!(redecoded.2, until + 100); // Until updated - assert_eq!(redecoded.3, after + 100); // After updated - } -} - -#[cfg(test)] -mod paymaster_validation_tests { - use super::*; - use alloy::primitives::{address, Bytes}; - - #[test] - fn test_extract_paymaster_address_valid() { - // Create paymasterAndData with a valid paymaster address - let paymaster_address = address!("0x1234567890123456789012345678901234567890"); - let mut paymaster_data = vec![]; - // Add paymaster address (20 bytes) - paymaster_data.extend_from_slice(paymaster_address.as_slice()); - // Add some additional data - paymaster_data.extend_from_slice(&[0xff; 32]); // gas limits etc. - - let paymaster_and_data = Bytes::from(paymaster_data); - let result = extract_paymaster_address(&paymaster_and_data); - - assert!(result.is_some()); - assert_eq!(result.unwrap(), paymaster_address); - } - - #[test] - fn test_extract_paymaster_address_too_short() { - // Create paymasterAndData that's too short (less than 20 bytes) - let short_data = vec![0x11; 19]; // Only 19 bytes - let paymaster_and_data = Bytes::from(short_data); - let result = extract_paymaster_address(&paymaster_and_data); - - assert!(result.is_none()); - } - - #[test] - fn test_extract_paymaster_address_empty() { - // Empty paymasterAndData - let paymaster_and_data = Bytes::from(vec![]); - let result = extract_paymaster_address(&paymaster_and_data); - - assert!(result.is_none()); - } - - #[test] - fn test_is_whitelisted_paymaster_found() { - let paymaster1 = address!("0x1234567890123456789012345678901234567890"); - let paymaster2 = address!("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"); - let paymaster3 = address!("0x9999999999999999999999999999999999999999"); - - let whitelist = vec![paymaster1, paymaster2]; - - // Test that whitelisted addresses are found - assert!(is_whitelisted_paymaster(&paymaster1, &whitelist)); - assert!(is_whitelisted_paymaster(&paymaster2, &whitelist)); - - // Test that non-whitelisted address is not found - assert!(!is_whitelisted_paymaster(&paymaster3, &whitelist)); - } - - #[test] - fn test_is_whitelisted_paymaster_empty_list() { - let paymaster = address!("0x1234567890123456789012345678901234567890"); - let empty_whitelist: Vec
= vec![]; - - // Test that no address is found in empty whitelist - assert!(!is_whitelisted_paymaster(&paymaster, &empty_whitelist)); - } - - #[test] - fn test_paymaster_whitelist_validation() { - // Test paymaster validation logic: - // - Unsigned userOp: paymaster must be whitelisted if specified - // - Signed userOp: no paymaster allowed - let whitelisted_address = address!("0x1234567890123456789012345678901234567890"); - let non_whitelisted_address = address!("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"); - - let mut whitelisted_data = vec![]; - whitelisted_data.extend_from_slice(whitelisted_address.as_slice()); - whitelisted_data.extend_from_slice(&[0xff; 32]); - let whitelisted_paymaster_and_data = Bytes::from(whitelisted_data); - - let mut non_whitelisted_data = vec![]; - non_whitelisted_data.extend_from_slice(non_whitelisted_address.as_slice()); - non_whitelisted_data.extend_from_slice(&[0xff; 32]); - let non_whitelisted_paymaster_and_data = Bytes::from(non_whitelisted_data); - - let whitelist = vec![whitelisted_address]; - - // Whitelisted paymaster should be accepted for unsigned userOps - let extracted = extract_paymaster_address(&whitelisted_paymaster_and_data).unwrap(); - assert!(is_whitelisted_paymaster(&extracted, &whitelist)); - - // Non-whitelisted paymaster should be rejected for unsigned userOps - let extracted = extract_paymaster_address(&non_whitelisted_paymaster_and_data).unwrap(); - assert!(!is_whitelisted_paymaster(&extracted, &whitelist)); - } - - #[test] - fn test_signed_userop_validation_logic() { - // Test the new validation logic: - // - Signed userOp with paymaster -> should be rejected - // - Signed userOp without paymaster -> should be accepted - let paymaster_address = address!("0x1234567890123456789012345678901234567890"); - let mut paymaster_data = vec![]; - paymaster_data.extend_from_slice(paymaster_address.as_slice()); - paymaster_data.extend_from_slice(&[0xff; 32]); - let paymaster_and_data = Bytes::from(paymaster_data); - - // Test that paymaster address is extracted correctly - let extracted = extract_paymaster_address(&paymaster_and_data).unwrap(); - assert_eq!(extracted, paymaster_address); - - // Empty paymasterAndData should be allowed for signed userOps - let empty_paymaster_data = Bytes::new(); - assert!(extract_paymaster_address(&empty_paymaster_data).is_none()); - } -} diff --git a/tee-worker/omni-executor/native-task-handler/src/types.rs b/tee-worker/omni-executor/native-task-handler/src/types.rs deleted file mode 100644 index f762018fb9..0000000000 --- a/tee-worker/omni-executor/native-task-handler/src/types.rs +++ /dev/null @@ -1,110 +0,0 @@ -use executor_primitives::Hash; -use parity_scale_codec::{Decode, Encode}; - -// Simple replacement for TransactionStatus -#[derive(Encode, Decode, Debug, PartialEq, Eq)] -pub enum TransactionStatus { - InBlock(Hash), - Finalized(Hash), - Invalid, - Dropped, -} -use pumpx::methods::add_wallet::AddWalletResponse; -use pumpx::methods::create_transfer_tx::CreateTransferTxResponse; -use pumpx::methods::user_connect::UserConnectResponse; -use serde::{Deserialize, Serialize}; - -/// Information about estimated token cost for ERC20 paymaster operations -#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq)] -pub struct TokenCostEstimate { - /// The ERC20 token address used for gas payment - pub token_address: String, - /// Token amount in smallest unit (e.g., wei for 18 decimal tokens) - pub amount: u128, - /// Token decimals for frontend formatting - pub decimals: u8, - /// Exchange rate used for calculation (tokens per ETH * 10^18) - pub exchange_rate: u128, -} - -#[derive(Encode, Decode, Debug, PartialEq, Eq)] -pub enum NativeTaskOk { - ExtrinsicReport { - extrinsic_hash: Hash, - block_hash: Option, - status: TransactionStatus, - }, - AuthToken(String), - PumpxRequestJwt { - /// Used for less sensitive operations - access_token: String, - /// Used for user's identity verification before making sensitive operations - id_token: String, - backend_response: UserConnectResponse, - }, - RequestIntentResult { - intent_id: u32, - success: bool, - }, - IntentSwapResponse(Vec), - PumpxExportWallet(Vec), - PumpxAddWallet(AddWalletResponse), - PumpxSignLimitOrder(Vec>), - PumpxTransferWithdraw(CreateTransferTxResponse), - PumpxNotifyLimitOrderResult, - SubmitUserOp(Option), // transaction_hash - EstimateUserOpGas { - call_gas_limit: u128, - verification_gas_limit: u128, - pre_verification_gas: u128, - paymaster_verification_gas_limit: u128, - paymaster_post_op_gas_limit: u128, - max_fee_per_gas: u128, - max_priority_fee_per_gas: u128, - estimated_token_cost: Option, - }, - RequestLoan { - spot_sell_cloid: String, - hedge_open_cloid: String, - usdc_received: String, // human-readable USDC amount received - spot_sell_tx_hash: Option, - hedge_open_tx_hash: Option, - }, -} - -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] -pub enum NativeTaskError { - UnauthorizedSender, - AuthTokenCreationFailed, - InternalError(Option), - InvalidMemberIdentity, - ValidationDataVerificationFailed, - UnsupportedIdentityType, - PumpxApiError(PumpxApiError), - PumpxSignerError(PumpxSignerError), - IntentNonceMismatch, - UnsupportedChain, - ChainNotSupported(u64), - InvalidUserOperation(String), - GasEstimationFailed, - SignatureServiceUnavailable, -} - -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] -pub enum PumpxApiError { - GoogleCodeVerificationFailed, - UserConnectionFailed, - UnknownError, - InvalidInput, - AddWalletFailed, - CreateTransferUnsignedTxFailed, - SendTransferTxFailed, - CreateTransferTxFailed, - GetAccountUserIdFailed, -} - -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] -pub enum PumpxSignerError { - RequestSignatureFailed, - RequestWalletFailed, -} diff --git a/tee-worker/omni-executor/rpc-server/Cargo.toml b/tee-worker/omni-executor/rpc-server/Cargo.toml index 4d8ffb4852..1185b92d3f 100644 --- a/tee-worker/omni-executor/rpc-server/Cargo.toml +++ b/tee-worker/omni-executor/rpc-server/Cargo.toml @@ -40,7 +40,7 @@ heima-authentication = { workspace = true } heima-identity-verification = { workspace = true } heima-primitives = { workspace = true } heima-utils = { workspace = true } -native-task-handler = { workspace = true } +hyperliquid = { workspace = true } oauth-providers = { workspace = true } pumpx = { workspace = true } signer-client = { workspace = true } diff --git a/tee-worker/omni-executor/rpc-server/src/auth_utils.rs b/tee-worker/omni-executor/rpc-server/src/auth_utils.rs index a5b07e32d7..c7e1393ac8 100644 --- a/tee-worker/omni-executor/rpc-server/src/auth_utils.rs +++ b/tee-worker/omni-executor/rpc-server/src/auth_utils.rs @@ -1,3 +1,4 @@ +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; @@ -11,7 +12,6 @@ use executor_primitives::{ use executor_storage::{Storage, WildmetaTimestampStorage}; use heima_primitives::{Address20, Identity}; use jsonrpsee::types::ErrorObject; -use native_task_handler::convert_to_packed_user_op; use std::sync::Arc; use tracing::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 4b5a984b3f..5f09d24df7 100644 --- a/tee-worker/omni-executor/rpc-server/src/error_code.rs +++ b/tee-worker/omni-executor/rpc-server/src/error_code.rs @@ -1,7 +1,5 @@ // we should use -32000 to -32099 for implementation defined error codes, // see https://www.jsonrpc.org/specification#error_object -use native_task_handler::{NativeTaskError, PumpxApiError, PumpxSignerError}; -use tracing::error; // Standard JSON-RPC error codes pub const PARSE_ERROR_CODE: i32 = -32700; @@ -9,36 +7,42 @@ 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 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; -const AUTH_TOKEN_CREATION_FAILED_CODE: i32 = -32007; -const INVALID_MEMBER_IDENTITY_CODE: i32 = -32008; -const VALIDATION_DATA_VERIFICATION_FAILED_CODE: i32 = -32009; -const UNSUPPORTED_IDENTITY_TYPE_CODE: i32 = -32010; +#[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_USER_CONNECTION_FAILED_CODE: i32 = -32032; -const PUMPX_API_ERROR_CODE: i32 = -32033; -const PUMPX_API_ADD_WALLET_FAILED_CODE: i32 = -32034; -const PUMPX_API_CREATE_TRANSFER_UNSIGNED_TX_FAILED_CODE: i32 = -32035; -const PUMPX_API_SEND_TRANSFER_TX_FAILED_CODE: i32 = -32036; -const PUMPX_API_INVALID_INPUT_FAILED_CODE: i32 = -32037; -const PUMPX_API_CREATE_TRANSFER_TX_FAILED_CODE: i32 = -32038; +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; // Input Validation Error Codes (-32100 to -32119) @@ -68,46 +72,3 @@ pub const UNEXPECTED_RESPONSE_TYPE_CODE: i32 = -32180; 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 fn get_native_task_error_code(error: &NativeTaskError) -> i32 { - match error { - NativeTaskError::UnauthorizedSender => UNAUTHORIZED_SENDER_CODE, - NativeTaskError::AuthTokenCreationFailed => AUTH_TOKEN_CREATION_FAILED_CODE, - NativeTaskError::InvalidMemberIdentity => INVALID_MEMBER_IDENTITY_CODE, - NativeTaskError::ValidationDataVerificationFailed => { - VALIDATION_DATA_VERIFICATION_FAILED_CODE - }, - NativeTaskError::UnsupportedIdentityType => UNSUPPORTED_IDENTITY_TYPE_CODE, - NativeTaskError::PumpxApiError(api_error) => match api_error { - PumpxApiError::GoogleCodeVerificationFailed => { - PUMPX_API_GOOGLE_CODE_VERIFICATION_FAILED_CODE - }, - PumpxApiError::UserConnectionFailed => PUMPX_API_USER_CONNECTION_FAILED_CODE, - PumpxApiError::UnknownError => PUMPX_API_ERROR_CODE, - PumpxApiError::AddWalletFailed => PUMPX_API_ADD_WALLET_FAILED_CODE, - PumpxApiError::CreateTransferUnsignedTxFailed => { - PUMPX_API_CREATE_TRANSFER_UNSIGNED_TX_FAILED_CODE - }, - PumpxApiError::SendTransferTxFailed => PUMPX_API_SEND_TRANSFER_TX_FAILED_CODE, - PumpxApiError::InvalidInput => PUMPX_API_INVALID_INPUT_FAILED_CODE, - PumpxApiError::CreateTransferTxFailed => PUMPX_API_CREATE_TRANSFER_TX_FAILED_CODE, - PumpxApiError::GetAccountUserIdFailed => PUMPX_API_GET_ACCOUNT_USER_ID_FAILED_CODE, - }, - NativeTaskError::PumpxSignerError(signer_error) => match signer_error { - PumpxSignerError::RequestSignatureFailed => PUMPX_SIGNER_REQUEST_SIGNATURE_FAILED_CODE, - PumpxSignerError::RequestWalletFailed => PUMPX_SIGNER_REQUEST_WALLET_FAILED_CODE, - }, - NativeTaskError::InternalError(_) => { - error!("Internal error: {:?}", error); - // This should not happen, we return the generic interal error code already from the api - INTERNAL_ERROR_CODE - }, - NativeTaskError::IntentNonceMismatch => INTENT_NONCE_MISMATCH_ERROR_CODE, - NativeTaskError::UnsupportedChain => INVALID_CHAIN_ID_CODE, - - NativeTaskError::ChainNotSupported(_) => INVALID_CHAIN_ID_CODE, - NativeTaskError::InvalidUserOperation(_) => INVALID_USER_OPERATION_CODE, - NativeTaskError::GasEstimationFailed => GAS_ESTIMATION_FAILED_CODE, - NativeTaskError::SignatureServiceUnavailable => SIGNATURE_SERVICE_UNAVAILABLE_CODE, - } -} diff --git a/tee-worker/omni-executor/rpc-server/src/lib.rs b/tee-worker/omni-executor/rpc-server/src/lib.rs index bd6c52006e..0123f14cb9 100644 --- a/tee-worker/omni-executor/rpc-server/src/lib.rs +++ b/tee-worker/omni-executor/rpc-server/src/lib.rs @@ -8,7 +8,7 @@ mod methods; mod middlewares; mod oauth2_factory; mod server; -mod task; +pub mod utils; mod validation_helpers; mod verify_auth; @@ -16,7 +16,7 @@ pub use auth_token_key_store::AuthTokenKeyStore; pub use executor_crypto::shielding_key::ShieldingKey; pub use server::start_server; -use executor_primitives::utils::hex::{hex_encode, FromHexPrefixed}; +// Removed unused hex imports - they may be used in other modules use jsonrpsee::types::ErrorCode; -use parity_scale_codec::{Decode, Encode}; +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 6062156525..8134f72ae6 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/mod.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/mod.rs @@ -16,14 +16,8 @@ pub const PROTECTED_METHODS: [&str; 8] = [ "omni_exportWallet", ]; -pub fn register_methods< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_methods( + module: &mut RpcModule>, ) { register_omni(module); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/README_HYPERLIQUID_UNIFIED_RPC.md b/tee-worker/omni-executor/rpc-server/src/methods/omni/README_HYPERLIQUID_UNIFIED_RPC.md deleted file mode 100644 index 4ce691f798..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/README_HYPERLIQUID_UNIFIED_RPC.md +++ /dev/null @@ -1,245 +0,0 @@ -# Unified Hyperliquid RPC Method - -This document demonstrates the unified approach for Hyperliquid signature generation using a single RPC endpoint. - -## RPC Method: `omni_getHyperliquidSignatureData` - -A single endpoint that handles all Hyperliquid operations through discriminated union parameters. - -### Usage Examples - -#### 1a. Approve Agent Wallet (Email Authentication) - -```bash -curl -X POST http://localhost:2100 \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "method": "omni_getHyperliquidSignatureData", - "params": { - "user_id": {"type": "email", "value": "user@example.com"}, - "user_auth": {"type": "email", "value": "123456"}, - "client_id": "heima", - "action_type": { - "type": "approve_agent", - "agent_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f02A10", - "agent_name": "My Trading Bot" - }, - "chain_id": 42161 - }, - "id": 1 - }' -``` - -#### 1b. Approve Agent Wallet (WildMeta Client Authentication) - -```bash -curl -X POST http://localhost:2100 \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "method": "omni_getHyperliquidSignatureData", - "params": { - "user_id": {"type": "evm", "value": "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"}, - "client_id": "wildmeta", - "client_auth": { - "type": "wildmeta_hl", - "value": { - "agent_address": "0xf8b16F021438B710fDE9d59dD17dDE1Eb2691BFd", - "business_json": "{\"action\":\"approve_agent\",\"agent_address\":\"0x742d35Cc6634C0532925a3b844Bc9e7595f02A10\",\"timestamp\":1752573555}", - "main_address": "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e", - "signature": "0x46c737250d61b60cbf0f46a6755e59815844a2f7cdb9dc16bf867b57bfed3526424343a237c15eef9089d571d1f60fd0bd7f91d5888c649216a7df147b386a681c", - "login_type": 0 - } - }, - "action_type": { - "type": "approve_agent", - "agent_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f02A10", - "agent_name": "My Trading Bot" - }, - "chain_id": 42161 - }, - "id": 1 - }' -``` - -#### 1c. Approve Agent Wallet (EVM Web3 Authentication) - -```bash -curl -X POST http://localhost:2100 \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "method": "omni_getHyperliquidSignatureData", - "params": { - "user_id": {"type": "evm", "value": "0xA9d439F4DED81152DB00CB7CD94A8d908FEF903e"}, - "user_auth": {"type": "evm", "value": "0x1234567890abcdef..."}, - "client_id": "heima", - "action_type": { - "type": "approve_agent", - "agent_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f02A10", - "agent_name": "My Trading Bot" - }, - "chain_id": 42161 - }, - "id": 1 - }' -``` - -#### 2. Initiate Withdrawal - -```bash -curl -X POST http://localhost:2100 \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "method": "omni_getHyperliquidSignatureData", - "params": { - "user_id": {"type": "email", "value": "user@example.com"}, - "user_auth": {"type": "email", "value": "123456"}, - "client_id": "heima", - "action_type": { - "type": "withdraw3", - "amount": "100.0", - "destination": "0x742d35Cc6634C0532925a3b844Bc9e7595f02A10" - }, - "chain_id": 42161 - }, - "id": 1 - }' -``` - -#### 3. Approve Builder Fee - -```bash -curl -X POST http://localhost:2100 \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "method": "omni_getHyperliquidSignatureData", - "params": { - "user_id": {"type": "email", "value": "user@example.com"}, - "user_auth": {"type": "email", "value": "123456"}, - "client_id": "heima", - "action_type": { - "type": "approve_builder_fee", - "max_fee_rate": "0.01", - "builder": "0x742d35Cc6634C0532925a3b844Bc9e7595f02A10" - }, - "chain_id": 42161 - }, - "id": 1 - }' -``` - -### Response Format - -```json -{ - "jsonrpc": "2.0", - "result": { - "main_address": "0x1234567890123456789012345678901234567890", - "hyperliquid_signature_data": { - "action": { - "type": "approve_agent", - "signatureChainId": "0xa4b1", - "hyperliquidChain": "Mainnet", - "agentAddress": "0x742d35cc6634c0532925a3b844bc9e7595f02a10", - "agentName": "My Trading Bot", - "nonce": 1700000000000 - }, - "nonce": 1700000000000, - "signature": "0x1234567890abcdef..." - } - }, - "id": 1 -} -``` - -## Parameters - -### Common Parameters -- `user_id`: User identification (email, EVM address, etc.) -- `user_auth`: **Optional** - User authentication (email verification, Web3 signature, auth token, OAuth2, passkey) -- `client_id`: Client identifier (`heima`, `pumpx`, `wildmeta`) -- `client_auth`: **Optional** - Client authentication (WildMeta signature-based auth) -- `chain_id`: Target blockchain network ID -- `action_type`: Discriminated union for different operations - -### Authentication Flow -The RPC method now supports **dual authentication modes**: - -1. **User Authentication** (`user_auth` provided): - - Email verification codes - - Web3 signatures (EVM, Substrate, Solana, Bitcoin) - - Auth tokens (JWT) - - OAuth2 (Google) - - Passkey authentication - -2. **Client Authentication** (`client_auth` provided): - - WildMeta signature-based authentication - - Agent-to-main address linking verification - - Timestamp-based replay protection - -**Note**: At least one of `user_auth` OR `client_auth` must be provided. `user_auth` takes a privileged role. - -### Action Types - -#### `approve_agent` -- `agent_address`: Agent's Ethereum address (required) -- `agent_name`: Human-readable agent name (optional) - -#### `withdraw3` -- `amount`: Withdrawal amount as string (required) -- `destination`: Destination wallet address (required) - -#### `approve_builder_fee` -- `max_fee_rate`: Maximum fee rate as string (required) -- `builder`: Builder's wallet address (required) - -## Features - -### Security -- **TEE-Based Signing**: Private keys never leave trusted execution environment -- **Email Authentication**: Verification codes required before signing -- **EIP-712 Compliance**: Proper domain separation and type hashing -- **Replay Protection**: Timestamp-based nonces - -### Technical -- **Chain Detection**: Automatic testnet vs mainnet detection -- **Address Validation**: Proper Ethereum address parsing -- **Error Handling**: Comprehensive error types and logging -- **Extensibility**: Easy to add new Hyperliquid action types - -### Supported Chains -- **Mainnet**: Ethereum (1), BSC (56), Arbitrum (42161), HyperEVM (999) -- **Testnet**: Sepolia (11155111), BSC Testnet (97), Arbitrum Sepolia (421614), HyperEVM Testnet (998) - -## EIP-712 Signatures - -### Type Hashes -- **ApproveAgent**: `HyperliquidTransaction:ApproveAgent(string hyperliquidChain,address agentAddress,string agentName,uint64 nonce)` -- **Withdraw3**: `HyperliquidTransaction:Withdraw3(string hyperliquidChain,string amount,uint64 time,address destination)` -- **ApproveBuilderFee**: `HyperliquidTransaction:ApproveBuilderFee(string hyperliquidChain,string maxFeeRate,address builder,uint64 nonce)` - -### Domain -- **Name**: `"HyperliquidSignTransaction"` -- **Version**: `"1"` -- **Chain ID**: Matches target network -- **Verifying Contract**: `Address::ZERO` - -## Integration with Hyperliquid - -The generated signatures are ready for direct use with Hyperliquid's API endpoints: -- **Approve Agent**: POST to `/exchange` with type `approveBuilderFee` -- **Withdraw3**: POST to `/exchange` with type `withdraw3` -- **Builder Fee**: POST to `/exchange` with type `approveBuilderFee` - -## Advantages - -✅ **Single Endpoint**: One RPC method for all Hyperliquid operations -✅ **Code Reuse**: Shared logic reduces duplication -✅ **Extensibility**: Trait-based design for easy additions -✅ **Type Safety**: Discriminated unions ensure correct parameters -✅ **Maintainability**: Centralized error handling and validation -✅ **Security**: Same TEE-based signing model throughout \ No newline at end of file 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 a94244d989..e304c1adc8 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,4 +1,4 @@ -use super::common::{check_omni_api_response, handle_omni_native_task}; +use super::common::check_omni_api_response; use crate::{ detailed_error::DetailedError, error_code::*, @@ -6,11 +6,11 @@ use crate::{ server::RpcContext, }; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::*; use executor_primitives::{utils::hex::FromHexPrefixed, AccountId}; +use executor_storage::{HeimaJwtStorage, Storage}; +use heima_authentication::constants::AUTH_TOKEN_ACCESS_TYPE; use heima_primitives::Address32; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use pumpx::methods::add_wallet::AddWalletResponse; use serde::Serialize; use tracing::{debug, error}; @@ -20,18 +20,12 @@ pub struct RPCAddWalletResponse { pub backend_response: AddWalletResponse, } -pub fn register_add_wallet< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_add_wallet( + module: &mut RpcModule>, ) { module .register_async_method("omni_addWallet", |_params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + let omni_account = check_auth(&ext).map_err(|e| { error!("Authentication check failed: {:?}", e); PumpxRpcError::from( DetailedError::new( @@ -44,35 +38,42 @@ pub fn register_add_wallet< debug!("Received omni_addWallet"); - let Ok(address) = Address32::from_hex(&user.omni_account) else { + let Ok(address) = Address32::from_hex(&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 omni_account_id = AccountId::from(address); - let wrapper = NativeTaskWrapper::new( - NativeTask::PumpxAddWallet(AccountId::from(address)), - None, - None, - user.client_id, - ); + // Inlined handler logic from handle_pumpx_add_wallet + let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); + let Ok(Some(access_token)) = storage.get(&(omni_account_id, 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"), + )); + }; + + // Call Pumpx API to add wallet + debug!("Calling pumpx add_wallet"); + let backend_response = + ctx.pumpx_api.add_wallet(&access_token, None).await.map_err(|e| { + error!("Failed to add wallet through Pumpx API: {:?}", e); + PumpxRpcError::from( + DetailedError::new( + PUMPX_API_ADD_WALLET_FAILED_CODE, + "Failed to add wallet through Pumpx API", + ) + .with_reason(format!("{:?}", e)), + ) + })?; - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::PumpxAddWallet(response) => { - check_omni_api_response(response.clone(), "Add wallet".into())?; - Ok(RPCAddWalletResponse { backend_response: response }) - }, - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Unexpected response type from native task handler"), - )) - }, - }) - .await + check_omni_api_response(backend_response.clone(), "Add wallet".into())?; + Ok(RPCAddWalletResponse { backend_response }) }) .expect("Failed to register omni_addWallet method"); } 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 index 05c2d10b03..ed4aa024b9 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/common.rs @@ -5,17 +5,8 @@ use pumpx::methods::common::ApiResponse; use serde::Serialize; use crate::{ - detailed_error::DetailedError, - error_code::{ - INTENT_NONCE_MISMATCH_ERROR_CODE, INTERNAL_ERROR_CODE, INVALID_CHAIN_ID_CODE, - UNAUTHORIZED_SENDER_CODE, *, - }, - middlewares::RpcExtensions, - server::RpcContext, + detailed_error::DetailedError, error_code::INTERNAL_ERROR_CODE, middlewares::RpcExtensions, }; -use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::*; -use native_task_handler::{handle_native_task, NativeTaskError, NativeTaskOk}; use tracing::error; #[derive(Serialize, Debug)] @@ -86,80 +77,7 @@ impl From> for PumpxRpcError { } } -/// Process native task and handle response -pub async fn handle_omni_native_task< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, - F, - R, ->( - ctx: &RpcContext, - wrapper: NativeTaskWrapper, - task_ok_handler: F, -) -> Result -where - F: FnOnce(NativeTaskOk) -> Result, -{ - // We handle the task right here - let native_task_response = handle_native_task(ctx.to_task_handler_context(), wrapper).await; - - // Process response - match native_task_response { - Ok(task_ok) => task_ok_handler(task_ok), - Err(NativeTaskError::InternalError(message)) => { - error!("Internal error in native task"); - match message { - Some(msg) => Err(PumpxRpcError::from_code_and_message( - get_native_task_error_code(&NativeTaskError::InternalError(Some(msg.clone()))), - msg, - )), - None => Err(PumpxRpcError::from_error_code(ErrorCode::InternalError)), - } - }, - Err(native_task_error) => { - error!("Native task error: {:?}", native_task_error); - - let detailed_error = match native_task_error { - NativeTaskError::ChainNotSupported(chain_id) => { - DetailedError::chain_not_supported(chain_id) - }, - NativeTaskError::InvalidUserOperation(desc) => { - DetailedError::invalid_user_operation_error(&desc) - }, - NativeTaskError::GasEstimationFailed => DetailedError::gas_estimation_failed(), - NativeTaskError::SignatureServiceUnavailable => { - DetailedError::signature_service_unavailable() - }, - NativeTaskError::InternalError(_) => { - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("An internal error occurred") - .with_suggestion("Please try again later") - }, - NativeTaskError::UnauthorizedSender => { - DetailedError::new(UNAUTHORIZED_SENDER_CODE, "Unauthorized sender") - .with_suggestion("Please check your authentication credentials") - }, - NativeTaskError::UnsupportedChain => { - DetailedError::new(INVALID_CHAIN_ID_CODE, "Chain not supported") - .with_suggestion("Please use a supported blockchain network") - }, - NativeTaskError::IntentNonceMismatch => { - DetailedError::new(INTENT_NONCE_MISMATCH_ERROR_CODE, "Intent nonce mismatch") - .with_suggestion("Please retry the operation") - }, - _ => { - // For other errors, use the existing error code mapping - let error_code = get_native_task_error_code(&native_task_error); - DetailedError::new(error_code, "Operation failed") - .with_suggestion("Please try again") - }, - }; - - Err(PumpxRpcError::from(detailed_error)) - }, - } -} +// Removed: handle_omni_native_task - all RPC methods now call handlers directly pub fn check_omni_api_response( response: ApiResponse, @@ -175,21 +93,13 @@ where Ok(()) } -pub struct User { - pub omni_account: String, - pub client_id: String, -} - /// 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 { +pub fn check_auth(ext: &Extensions) -> Result { if let Some(rpc_extensions) = ext.get::() { - return Ok(User { - omni_account: rpc_extensions.sender.clone(), - client_id: rpc_extensions.client_id.clone(), - }); + 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 68e2f7b22e..5fa1d464e8 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,22 +14,22 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use super::common::{handle_omni_native_task, PumpxRpcError}; +use super::common::PumpxRpcError; use crate::detailed_error::DetailedError; use crate::server::RpcContext; +use crate::utils::gas_estimation::estimate_user_op_gas; +use crate::utils::user_op::convert_to_packed_user_op; use crate::validation_helpers::{ validate_ethereum_address, validate_omni_account_hex, validate_omni_account_length, }; use alloy::primitives::utils::format_units; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::{NativeTask, NativeTaskWrapper}; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::{AccountId, ChainId}; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use parity_scale_codec::Decode; use serde::{Deserialize, Serialize}; -use tracing::{debug, error}; +use tracing::{debug, error, info}; /// Format a token amount with decimals to a human-readable string fn format_token_amount(amount: u128, decimals: u8) -> String { @@ -59,6 +59,7 @@ pub struct EstimateUserOpGasParams { pub chain_id: ChainId, pub wallet_index: u32, pub omni_account: String, + #[allow(dead_code)] pub client_id: String, } @@ -88,13 +89,9 @@ pub struct EstimateUserOpGasResponse { } pub fn register_estimate_user_op_gas< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_estimateUserOpGas", |params, ctx, _ext| async move { @@ -125,31 +122,42 @@ pub fn register_estimate_user_op_gas< validate_ethereum_address(¶ms.user_operation.sender, "user_operation.sender") .map_err(PumpxRpcError::from)?; - let wrapper = NativeTaskWrapper::new( - NativeTask::EstimateUserOpGas( - account_id, - params.user_operation.clone(), - params.chain_id, - params.wallet_index, - ), - None, - None, - params.client_id, + // Inlined handler logic from handle_estimate_user_op_gas + info!( + "Processing EstimateUserOpGas for account {:?}, wallet_index: {}, chain_id: {}", + account_id, params.wallet_index, params.chain_id ); - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::EstimateUserOpGas { - call_gas_limit, - verification_gas_limit, - pre_verification_gas, - paymaster_verification_gas_limit, - paymaster_post_op_gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - estimated_token_cost, - } => { + // Get EntryPoint client for this chain + 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)) + })?; + + // 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", + )) + })?; + + // Perform gas estimation + let result = estimate_user_op_gas( + entry_point_client.clone(), + packed_user_op, + params.chain_id, + ctx.binance_api_client.as_ref(), + ) + .await; + + // Process response + match result { + Ok(gas_estimate) => { // Convert token cost estimate to RPC format if present - let token_cost_info = estimated_token_cost.map(|cost| { + let token_cost_info = gas_estimate.estimated_token_cost.map(|cost| { // Format the amount as a human-readable value let formatted_amount = format_token_amount(cost.amount, cost.decimals); @@ -164,26 +172,25 @@ pub fn register_estimate_user_op_gas< }); Ok(EstimateUserOpGasResponse { - call_gas_limit: call_gas_limit.to_string(), - verification_gas_limit: verification_gas_limit.to_string(), - pre_verification_gas: pre_verification_gas.to_string(), - paymaster_verification_gas_limit: paymaster_verification_gas_limit + call_gas_limit: gas_estimate.call_gas_limit.to_string(), + verification_gas_limit: gas_estimate.verification_gas_limit.to_string(), + pre_verification_gas: gas_estimate.pre_verification_gas.to_string(), + paymaster_verification_gas_limit: gas_estimate + .paymaster_verification_gas_limit .to_string(), - paymaster_post_op_gas_limit: paymaster_post_op_gas_limit.to_string(), - max_fee_per_gas: max_fee_per_gas.to_string(), - max_priority_fee_per_gas: max_priority_fee_per_gas.to_string(), + paymaster_post_op_gas_limit: gas_estimate + .paymaster_post_op_gas_limit + .to_string(), + max_fee_per_gas: gas_estimate.max_fee_per_gas.to_string(), + max_priority_fee_per_gas: gas_estimate.max_priority_fee_per_gas.to_string(), estimated_token_cost: token_cost_info, }) }, - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from(DetailedError::unexpected_response_type( - "EstimateUserOpGas response", - "Unknown response type", - ))) + Err(e) => { + error!("Gas estimation failed: {}", e); + Err(PumpxRpcError::from(DetailedError::gas_estimation_failed())) }, - }) - .await + } }) .expect("Failed to register omni_estimateUserOpGas method"); } 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 bc70886769..080df6520d 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 @@ -164,13 +164,9 @@ fn verify_signature( } pub fn register_export_bundler_private_key< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_method("omni_exportBundlerPrivateKey", |params, ctx, _ext| { @@ -242,11 +238,12 @@ pub fn register_export_bundler_private_key< #[cfg(test)] mod tests { use super::*; - use crate::{hex_encode, start_server, ShieldingKey}; + use crate::{start_server, ShieldingKey}; use binance_api::mocks::MockBinanceApiClient; use config_loader::ConfigLoader; use executor_core::intent_executor::MockedIntentExecutor; use executor_crypto::{ecdsa, PairTrait}; + use executor_primitives::utils::hex::hex_encode; use executor_storage::{StorageDB, WildmetaTimestampStorage}; use jsonrpsee::{core::client::ClientT, rpc_params, ws_client::WsClientBuilder}; use pumpx::PumpxApiClient; @@ -317,8 +314,6 @@ mod tests { let wildmeta_timestamp_storage = Arc::new(WildmetaTimestampStorage::new(storage_db.clone())); - let (solana_intent_executor, _) = MockedIntentExecutor::new(); - let (ethereum_intent_executor, _) = MockedIntentExecutor::new(); let (cross_chain_intent_executor, _) = MockedIntentExecutor::new(); let aes_key = TEST_AES_KEY; @@ -338,8 +333,6 @@ mod tests { [0u8; 33], bundler_key, authorized_pubkey, - Arc::new(ethereum_intent_executor), - Arc::new(solana_intent_executor), Arc::new(cross_chain_intent_executor), aes_key, Arc::new(entry_point_clients), 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 667764eb62..e87244ac92 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,4 +1,3 @@ -use super::common::handle_omni_native_task; use crate::{ detailed_error::DetailedError, error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE, *}, @@ -6,16 +5,19 @@ use crate::{ server::RpcContext, Deserialize, }; +use ::pumpx::signer_client::PumpxChainId as _; use ethers::types::Bytes; use executor_core::intent_executor::IntentExecutor; use executor_core::native_task::*; -use executor_crypto::aes256::{aes_encrypt_default, Aes256Key, SerdeAesOutput}; -use executor_primitives::{utils::hex::FromHexPrefixed, AccountId}; +use executor_crypto::aes256::{aes_decrypt, aes_encrypt_default, Aes256Key, SerdeAesOutput}; +use executor_primitives::{utils::hex::FromHexPrefixed, AccountId, PumpxAccountProfile}; +use executor_storage::{HeimaJwtStorage, PumpxProfileStorage, Storage}; +use heima_authentication::constants::AUTH_TOKEN_ACCESS_TYPE; use heima_primitives::Address32; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use rsa::Oaep; use sha2::Sha256; +use signer_client::ChainType; use tracing::{debug, error}; #[derive(Debug, Deserialize)] @@ -27,39 +29,12 @@ pub struct ExportWalletParams { pub wallet_address: String, } -impl ExportWalletParams { - pub fn into_native_task_wrapper( - self, - client_id: String, - omni_account: AccountId, - ) -> NativeTaskWrapper { - NativeTaskWrapper::new( - NativeTask::PumpxExportWallet( - omni_account, - self.google_code, - self.chain_id, - self.wallet_index, - self.wallet_address, - ), - None, - None, - client_id, - ) - } -} - -pub fn register_export_wallet< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_export_wallet( + module: &mut RpcModule>, ) { module .register_async_method("omni_exportWallet", |params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + let omni_account = check_auth(&ext).map_err(|e| { error!("Authentication check failed: {:?}", e); PumpxRpcError::from(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, @@ -77,14 +52,14 @@ pub fn register_export_wallet< debug!("Received omni_exportWallet, chain_id: {}, wallet_index: {}, expected_wallet_address: {}", params.chain_id, params.wallet_index, params.wallet_address); - let Ok(address) = Address32::from_hex(&user.omni_account) else { + let Ok(address) = Address32::from_hex(&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 omni_account = AccountId::from(address); + let omni_account_id = AccountId::from(address); let aes_key = ctx .shielding_key @@ -105,23 +80,108 @@ pub fn register_export_wallet< ).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)")) })?; - let wrapper = params.into_native_task_wrapper(user.client_id, omni_account); + // 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_id.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"), + )); + }; - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::PumpxExportWallet(wallet) => { - let encrypted_wallet: SerdeAesOutput = - aes_encrypt_default(&aes_key, &wallet).into(); - Ok(encrypted_wallet) + // 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_success = verify_result.map_or_else( + |e| { + error!("Google code verification request failed: {:?}", e); + false }, - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from(DetailedError::new( - INTERNAL_ERROR_CODE, - "Internal error" - ).with_reason("Unexpected response type from native task handler"))) + |res| { + res.data.result.map_or_else( + || { + error!("Google code verification response result is none"); + false + }, + |success| success, + ) }, - }) - .await + ); + if !verify_success { + error!("Failed to verify google code within NativeTask::PumpxExportWallet"); + 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 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(INVALID_CHAIN_ID_CODE, "Chain not supported") + .with_reason(format!("Chain ID {} is not supported", params.chain_id)), + )); + }; + + let Ok(mut wallet) = ctx + .signer_client + .export_wallet( + chain, + params.wallet_index, + omni_account_id.clone().into(), + ctx.aes256_key.to_vec(), + params.wallet_address, + ) + .await + else { + error!("Failed to export wallet from pumpx-signer"); + return Err(PumpxRpcError::from( + DetailedError::new( + PUMPX_SIGNER_REQUEST_WALLET_FAILED_CODE, + "Failed to export wallet from pumpx-signer", + ) + .with_suggestion("Please try again"), + )); + }; + let Some(decrypted_wallet) = aes_decrypt(&ctx.aes256_key, &mut wallet) else { + error!("Failed to decrypt wallet"); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("Failed to decrypt wallet"), + )); + }; + + let omni_account_profile_storage = PumpxProfileStorage::new(ctx.storage_db.clone()); + if let Ok(maybe_profile) = omni_account_profile_storage.get(&omni_account_id) { + let profile = maybe_profile + .map(|mut p| { + p.wallet_exported = true; + p + }) + .unwrap_or_else(|| PumpxAccountProfile { wallet_exported: true }); + if let Err(e) = omni_account_profile_storage.insert(&omni_account_id, profile) { + error!("Failed to update pumpx account profile: {:?}", e); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("Failed to update pumpx account profile"), + )); + }; + } else { + error!("Failed to get pumpx account profile"); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("Failed to get pumpx account profile"), + )); + } + + let encrypted_wallet: SerdeAesOutput = + aes_encrypt_default(&aes_key, &decrypted_wallet).into(); + Ok(encrypted_wallet) }) .expect("Failed to register omni_exportWallet method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_health.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_health.rs index 97577b550c..d03afc3349 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_health.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_health.rs @@ -18,14 +18,8 @@ use crate::server::RpcContext; use executor_core::intent_executor::IntentExecutor; use jsonrpsee::{types::ErrorObject, RpcModule}; -pub fn register_get_health< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_get_health( + module: &mut RpcModule>, ) { module .register_method("omni_getHealth", |_, _, _| Ok::("OK".to_string())) @@ -70,8 +64,6 @@ mod test { let wildmeta_api: Arc> = Arc::new(Box::new(MockWildmetaApi)); let wildmeta_timestamp_storage = Arc::new(WildmetaTimestampStorage::new(db.clone())); - let (solana_intent_executor, _solana_mock_recv) = MockedIntentExecutor::new(); - let (ethereum_intent_executor, _ethereum_mock_recv) = MockedIntentExecutor::new(); let (cross_chain_intent_executor, _cross_chain_mock_recv) = MockedIntentExecutor::new(); let aes_key = [0u8; 32]; let entry_point_clients = HashMap::new(); @@ -90,8 +82,6 @@ mod test { [0u8; 33], // Test ECDSA public key [0u8; 32], // Test bundler private key [0u8; 33], // Test bundler export authorized pubkey - Arc::new(ethereum_intent_executor), - Arc::new(solana_intent_executor), Arc::new(cross_chain_intent_executor), aes_key, Arc::new(entry_point_clients), diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs index 677f4b66e6..76bde601ab 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_hyperliquid_signature_data.rs @@ -100,13 +100,9 @@ fn is_testnet_chain(chain_id: ChainId) -> bool { } pub fn register_get_hyperliquid_signature_data< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getHyperliquidSignatureData", |params, ctx, _| async move { @@ -358,12 +354,10 @@ pub fn register_get_hyperliquid_signature_data< } async fn generate_eip712_signature< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, T: Eip712 + Send + Sync, >( - ctx: &RpcContext, + ctx: &RpcContext, action: &T, omni_account: &[u8; 32], ) -> Result> { 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 a2b8fc04f9..d34385e9f3 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 @@ -31,13 +31,9 @@ pub struct GetNextIntentIdParams { } pub fn register_get_next_intent_id< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getNextIntentId", |params, ctx, _| async move { 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 f53edb70bf..a6fd889bc7 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 @@ -37,13 +37,9 @@ struct OAuth2AuthorizationData { } pub fn register_get_oauth2_authorization_data< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getOAuth2AuthorizationData", |params, ctx, _| async move { 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 85768a6318..349d455a05 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 @@ -32,13 +32,9 @@ pub struct GetOmniAccountParams { // Directly converts Identity to OmniAccount using 1:1 mapping pub fn register_get_omni_account< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getOmniAccount", |params, _, _| async move { diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_shielding_key.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_shielding_key.rs index 69b07d2cc7..ad3590dfcb 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/get_shielding_key.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/get_shielding_key.rs @@ -6,13 +6,9 @@ use jsonrpsee::{types::ErrorObject, RpcModule}; // example output: // {"jsonrpc":"2.0","id":1,"result":{"e":"0x010001","n":"0x0dee6b464b9a1033d0442be11ea013565e7cda30ff50bfafa9245e4d221e27a056be373251771cd5a949c6a29c1264eceaddf078d820d331f8ab6d9f4da167c55364ed8094cdc1ab467c2e68f2bcebe83453af3044f33cb4269428842cb354571fbc7c10aca61a06843a9d82c52231c18799dd8249d6e696f2c92695b40594b0b00d0b0fb77e7f7c9c89d2be0b9fd105d51d643094ebb2eb03185f056e75caf57df818ce6fca6ed104919296ba171caf950d2241c34257db7323e5c90fbbec813243ecbc41c0fbf6df2112c781d4f8540d4af356fbe999368c3404ebb7f806fdd94694d231704097be1da0ce895ce5ce69130a0d43432024cf3018e4621cfb370507d2ec1070ba65b6223de2d6a39dc997e3a3407302d7f0b456412d579f13ce57d8e4230d3c935956a526c77de48f73c73606f713aeb3ab9e8817aa53a48df1a961df9d262d88ee116fecfe4c4551b1fd704a23c25568f6448e3f22c53451061203cff70ec87780416dc02e4078a608359b20f6d129438bd5bce5bee16f24d3"}} pub fn register_get_shielding_key< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getShieldingKey", |_params, ctx, _| async move { @@ -62,8 +58,6 @@ mod test { let wildmeta_api: Arc> = Arc::new(Box::new(MockWildmetaApi)); let wildmeta_timestamp_storage = Arc::new(WildmetaTimestampStorage::new(db.clone())); - let (solana_intent_executor, _solana_mock_recv) = MockedIntentExecutor::new(); - let (ethereum_intent_executor, _ethereum_mock_recv) = MockedIntentExecutor::new(); let (cross_chain_intent_executor, _cross_chain_mock_recv) = MockedIntentExecutor::new(); let aes_key = [0u8; 32]; let entry_point_clients = HashMap::new(); @@ -82,8 +76,6 @@ mod test { [0u8; 33], // Test ECDSA public key [0u8; 32], // Test bundler private key [0u8; 33], // Test bundler export authorized pubkey - Arc::new(ethereum_intent_executor), - Arc::new(solana_intent_executor), Arc::new(cross_chain_intent_executor), aes_key, Arc::new(entry_point_clients), 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 b2258d13d0..fe0396779f 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 @@ -40,13 +40,9 @@ pub struct GetSmartWalletRootSignerParams { } pub fn register_get_smart_wallet_root_signer< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getSmartWalletRootSigner", |params, ctx, _| async move { 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 9270d7cc4e..525745a520 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 @@ -18,13 +18,9 @@ pub struct GetWeb3SignInMessageParams { } pub fn register_get_web3_sign_in_message< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_getWeb3SignInMessage", |params, ctx, _| async move { 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 1a3ed10b80..42c4fb9a55 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 @@ -69,13 +69,9 @@ fn create_jwt_for_user( } pub fn register_login_with_oauth2< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_loginWithOAuth2", |params, ctx, _| async move { 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 4723be2d1c..9856d4db74 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 @@ -19,9 +19,6 @@ use get_shielding_key::*; mod request_email_verification_code; use request_email_verification_code::*; -mod submit_native_task; -use submit_native_task::*; - mod get_web3_sign_in_message; use get_web3_sign_in_message::*; @@ -92,19 +89,12 @@ mod request_loan_test; #[cfg(feature = "test-endpoints")] use request_loan_test::*; -pub fn register_omni< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_omni( + module: &mut RpcModule>, ) { register_get_health(module); register_get_next_intent_id(module); register_get_shielding_key(module); - register_submit_native_task(module); register_request_email_verification_code(module); register_get_oauth2_authorization_data(module); register_get_web3_sign_in_message(module); 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 f80f366a57..1ab3b57988 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,18 +1,13 @@ -use super::common::handle_omni_native_task; use crate::methods::omni::{common::check_auth, PumpxRpcError}; use crate::{ detailed_error::DetailedError, - error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE, *}, + error_code::{PARSE_ERROR_CODE, *}, server::RpcContext, Deserialize, }; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::*; -use executor_primitives::{utils::hex::FromHexPrefixed, AccountId}; -use heima_primitives::Address32; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; -use tracing::{debug, error}; +use tracing::{debug, error, info}; #[derive(Debug, Deserialize)] pub struct NotifyLimitOrderResultParams { @@ -22,17 +17,13 @@ pub struct NotifyLimitOrderResultParams { } pub fn register_notify_limit_order_result< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module - .register_async_method("omni_notifyLimitOrderResult", |params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + .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( @@ -56,37 +47,20 @@ pub fn register_notify_limit_order_result< params.intent_id, params.result, params.message ); - let Ok(address) = Address32::from_hex(&user.omni_account) else { - error!("Failed to parse from omni account token"); + // 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(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Failed to parse omni account from authentication token"), + DetailedError::new(INVALID_PARAMS_CODE, "Invalid input") + .with_reason("Result must be 'ok' or 'nok'"), )); - }; + } - let wrapper = NativeTaskWrapper::new( - NativeTask::PumpxNotifyLimitOrderResult( - AccountId::from(address), - params.intent_id, - params.result, - params.message, - ), - None, - None, - user.client_id, - ); + if let Some(msg) = ¶ms.message { + info!("Limit order result message for intent_id {}: {}", params.intent_id, msg); + } - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::PumpxNotifyLimitOrderResult => Ok(()), - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Unexpected response type from native task handler"), - )) - }, - }) - .await + Ok(()) }) .expect("Failed to register omni_notifyLimitOrderResult 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 88de8b720c..445fff4eb2 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 @@ -18,13 +18,9 @@ pub struct RequestEmailVerificationCodeParams { } pub fn register_request_email_verification_code< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_requestEmailVerificationCode", |params, ctx, _| async move { 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 258d60c273..782b56694d 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,4 +1,4 @@ -use super::common::{check_omni_api_response, handle_omni_native_task}; +use super::common::check_omni_api_response; use crate::{ detailed_error::DetailedError, error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE, *}, @@ -7,12 +7,17 @@ use crate::{ verify_auth::verify_auth, Deserialize, }; +use chrono::{Days, Utc}; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::*; -use executor_primitives::OmniAuth; +use executor_crypto::jwt; +use executor_primitives::{utils::hex::ToHexPrefixed, OmniAuth}; +use executor_storage::{HeimaJwtStorage, Storage}; +use heima_authentication::{ + auth_token::*, + constants::{AUTH_TOKEN_ACCESS_TYPE, AUTH_TOKEN_EXPIRATION_DAYS, AUTH_TOKEN_ID_TYPE}, +}; use heima_primitives::{Identity, Web2IdentityType}; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use pumpx::methods::user_connect::UserConnectResponse; use serde::Serialize; use tracing::{debug, error}; @@ -35,30 +40,13 @@ pub struct RequestJwtResponse { } impl RequestJwtParams { - pub fn into_native_task_wrapper(self) -> NativeTaskWrapper { - NativeTaskWrapper::new( - NativeTask::PumpxRequestJwt( - Identity::from_web2_account(self.user_email.as_str(), Web2IdentityType::Email), // actually unused - self.user_email.clone(), - self.invite_code, - self.google_code, - self.language, - ), - None, - Some(OmniAuth::Email(self.client_id.clone(), self.user_email, self.email_code)), - self.client_id, - ) + pub fn get_omni_auth(&self) -> OmniAuth { + OmniAuth::Email(self.client_id.clone(), self.user_email.clone(), self.email_code.clone()) } } -pub fn register_request_jwt< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_request_jwt( + module: &mut RpcModule>, ) { module .register_async_method("omni_requestJwt", |params, ctx, _ext| async move { @@ -75,42 +63,133 @@ pub fn register_request_jwt< params.user_email, params.client_id ); - let wrapper = params.into_native_task_wrapper(); - - if wrapper.task.require_auth() { - let Some(ref auth) = wrapper.auth else { - error!("Missing auth token"); - return Err(PumpxRpcError::from( - DetailedError::new(REQUIRE_AUTHENTICATION_CODE, "Authentication required") - .with_suggestion("Please provide authentication credentials"), - )); - }; - verify_auth(ctx.clone(), auth).await.map_err(|e| { - error!("Failed to verify auth: {:?}, reason: {:?}", wrapper.auth, e); + // Verify email authentication + let auth = params.get_omni_auth(); + verify_auth(ctx.clone(), &auth).await.map_err(|e| { + error!("Failed to verify auth: {:?}, reason: {:?}", auth, e); + PumpxRpcError::from( + DetailedError::new( + AUTH_VERIFICATION_FAILED_CODE, + "Authentication verification failed", + ) + .with_suggestion("Please check your authentication credentials"), + ) + })?; + + // Inlined handler logic from handle_pumpx_request_jwt + let expires_at = Utc::now() + .checked_add_days(Days::new(AUTH_TOKEN_EXPIRATION_DAYS)) + .expect("Failed to calculate expiration") + .timestamp(); + let auth_options = AuthOptions { expires_at }; + + debug!("Calling pumpx get_account_user_id, email: {}", params.user_email); + let res = ctx.pumpx_api.get_account_user_id(params.user_email.clone()).await.map_err( + |e| { + error!( + "Failed to get_account_user_id for email {}: {:?}", + params.user_email, e + ); PumpxRpcError::from( - DetailedError::new( - AUTH_VERIFICATION_FAILED_CODE, - "Authentication verification failed", - ) - .with_suggestion("Please check your authentication credentials"), + DetailedError::new(INTERNAL_ERROR_CODE, "Failed to get account user ID") + .with_suggestion("Please try again"), ) + }, + )?; + debug!("Response pumpx get_account_user_id: {:?}", res); + + let Some(user_id) = res.data.user_id else { + error!("Response data.user_id of call get_account_user_id is none"); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Failed to get account user ID") + .with_reason("User ID not found in response"), + )); + }; + + debug!("get_account_user_id ok, email: {}, user_id: {}", params.user_email, user_id); + let omni_account = Identity::from_web2_account(&user_id, Web2IdentityType::Pumpx) + .to_omni_account(¶ms.client_id); + + let access_token_claims: AuthTokenClaims = AuthTokenClaims::new( + omni_account.to_hex(), + AUTH_TOKEN_ACCESS_TYPE.to_string(), + params.client_id.to_string(), + auth_options.clone(), + ); + let access_token = jwt::create(&access_token_claims, &ctx.jwt_rsa_private_key) + .map_err(|e| { + error!("Failed to create access token: {:?}", e); + PumpxRpcError::from(DetailedError::new( + INTERNAL_ERROR_CODE, + "Failed to create authentication token", + )) })?; + + debug!( + "Calling pumpx user_connect, user_id: {}, email: {}, invite_code: {:?}, google_code: {:?}", + user_id, params.user_email, params.invite_code, params.google_code + ); + let backend_response = ctx + .pumpx_api + .user_connect( + &access_token, + user_id.clone(), + params.user_email.clone(), + params.invite_code, + params.google_code, + params.language, + ) + .await + .map_err(|e| { + error!("Failed to connect user: {:?}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Failed to connect user") + .with_suggestion("Please try again"), + ) + })?; + debug!("Response pumpx user_connect: {:?}", backend_response); + + // check google auth value + if !backend_response.data.google_auth_check.unwrap_or(false) { + error!("Google code verification failed from user_connect"); + 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"), + )); } - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::PumpxRequestJwt { access_token, id_token, backend_response } => { - check_omni_api_response(backend_response.clone(), "Request pumpx jwt".into())?; - Ok(RequestJwtResponse { access_token, id_token, backend_response }) - }, - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Unexpected response type from native task handler"), + let id_token_claims = AuthTokenClaims::new( + omni_account.to_hex(), + AUTH_TOKEN_ID_TYPE.to_string(), + params.client_id.to_string(), + auth_options, + ); + let id_token = + jwt::create(&id_token_claims, &ctx.jwt_rsa_private_key).map_err(|e| { + error!("Failed to create id token: {:?}", e); + PumpxRpcError::from(DetailedError::new( + INTERNAL_ERROR_CODE, + "Failed to create authentication token", )) - }, - }) - .await + })?; + + let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); + if storage + .insert(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE), access_token.clone()) + .is_err() + { + error!("Failed to insert pumpx_{}_jwt_token into storage", AUTH_TOKEN_ACCESS_TYPE); + }; + + if storage.insert(&(omni_account, AUTH_TOKEN_ID_TYPE), id_token.clone()).is_err() { + error!("Failed to insert pumpx_{}_jwt_token into storage", AUTH_TOKEN_ID_TYPE); + }; + + check_omni_api_response(backend_response.clone(), "Request pumpx jwt".into())?; + Ok(RequestJwtResponse { access_token, id_token, backend_response }) }) .expect("Failed to register omni_requestJwt method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_loan_test.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_loan_test.rs index 8c74338073..be48f504cd 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/request_loan_test.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/request_loan_test.rs @@ -1,18 +1,28 @@ -use super::common::handle_omni_native_task; use crate::detailed_error::DetailedError; -use crate::error_code::{INTERNAL_ERROR_CODE, PARSE_ERROR_CODE}; +use crate::error_code::{ + INTERNAL_ERROR_CODE, INVALID_CHAIN_ID_CODE, INVALID_USER_OPERATION_CODE, PARSE_ERROR_CODE, + SIGNATURE_SERVICE_UNAVAILABLE_CODE, +}; use crate::methods::omni::PumpxRpcError; use crate::server::RpcContext; -use alloy::primitives::Address; +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 executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::{NativeTask, NativeTaskWrapper}; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::{AccountId, ChainId}; +use hyperliquid::*; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use parity_scale_codec::Decode; use serde::{Deserialize, Serialize}; -use tracing::{debug, error}; +use signer_client::ChainType; +use std::sync::Arc; +use tracing::{debug, error, info}; #[derive(Debug, Deserialize)] pub struct RequestLoanTestParams { @@ -36,13 +46,9 @@ pub struct RequestLoanTestResponse { } pub fn register_request_loan_test< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_requestLoanTest", |params, ctx, _ext| async move { @@ -123,50 +129,905 @@ pub fn register_request_loan_test< )); } - let wrapper = NativeTaskWrapper::new( - NativeTask::RequestLoanTest( - 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"), - ) - })?, - params.user_operation.clone(), - params.chain_id, - params.wallet_index, - collateral_ticker, - params.collateral_size, - params.lending_ratio, - ), - None, - None, - params.client_id, - ); + 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"), + ) + })?; - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::RequestLoan { - spot_sell_cloid, - hedge_open_cloid, - usdc_received, - spot_sell_tx_hash, - hedge_open_tx_hash, - } => Ok(RequestLoanTestResponse { - spot_sell_cloid, - hedge_open_cloid, - usdc_received, - spot_sell_tx_hash, - hedge_open_tx_hash, - }), - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Unexpected response type from native task handler"), - )) - }, - }) + // Call the inlined handler logic + handle_request_loan_impl( + Arc::clone(&ctx), + omni_account, + params.user_operation.clone(), + params.chain_id, + params.wallet_index, + &collateral_ticker, + ¶ms.collateral_size, + params.lending_ratio, + ¶ms.client_id, + ) .await }) .expect("Failed to register omni_requestLoanTest method"); } + +// Helper function to print account state +async fn print_account_state(hypercore_client: &HyperCoreClient, user_address: &str, label: &str) { + info!("========== Account State: {} ==========", label); + + // Print spot balances + match hypercore_client.get_spot_clearinghouse_state(user_address).await { + Ok(spot_state) => { + info!("Spot Balances:"); + for balance in &spot_state.balances { + let total: f64 = balance.total.parse().unwrap_or(0.0); + let hold: f64 = balance.hold.parse().unwrap_or(0.0); + if total > 0.0 || hold > 0.0 { + info!(" {} - Total: {}, Hold: {}", balance.coin, balance.total, balance.hold); + } + } + }, + Err(e) => { + info!("Failed to fetch spot balances: {}", e); + }, + } + + // Print perp clearinghouse state + match hypercore_client.get_perp_clearinghouse_state(user_address).await { + Ok(perp_state) => { + info!("Perp Margin Summary:"); + info!( + " Account Value: {}, Total Margin Used: {}, Withdrawable: {}", + perp_state.margin_summary.account_value, + perp_state.margin_summary.total_margin_used, + perp_state.withdrawable + ); + + if !perp_state.asset_positions.is_empty() { + info!("Open Positions:"); + for asset_pos in &perp_state.asset_positions { + let pos = &asset_pos.position; + info!( + " {} - Size: {}, Entry Px: {}, Position Value: {}, Unrealized PnL: {}, Leverage: {}x", + pos.coin, + pos.szi, + pos.entry_px.as_ref().unwrap_or(&"N/A".to_string()), + pos.position_value, + pos.unrealized_pnl, + pos.leverage.value + ); + } + } else { + info!("Open Positions: None"); + } + }, + Err(e) => { + info!("Failed to fetch perp clearinghouse state: {}", e); + }, + } + + info!("=========================================="); +} + +// Helper function to validate loan request parameters +#[allow(clippy::too_many_arguments)] +async fn validate_loan_request_parameters( + hypercore_client: &HyperCoreClient, + smart_wallet_address: &str, + collateral_ticker: &str, + collateral_size: f64, + collateral_token: &SpotToken, + perp_asset: &PerpAsset, + lending_ratio: u32, + spot_market_price: f64, + perp_market_price: f64, +) -> Result<(f64, f64), PumpxRpcError> { + // 1. Validate collateral size for spot trading + validate_trade_size(collateral_size, collateral_token.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)), + ) + })?; + + info!( + "✓ Collateral size {} validated for spot trading (sz_decimals={})", + collateral_size, collateral_token.sz_decimals + ); + + // 2. Calculate estimated values for perp position + let lending_ratio_f64 = (lending_ratio as f64) / 100.0; + let estimated_usdc_from_spot = collateral_size * spot_market_price; + let estimated_usdc_for_perp = estimated_usdc_from_spot * (1.0 - lending_ratio_f64); + let estimated_leverage = (1.0 / (1.0 - lending_ratio_f64)).min(perp_asset.max_leverage as f64); + let estimated_perp_notional = estimated_usdc_for_perp * estimated_leverage; + + // 3. Validate minimum perp order notional value ($10 minimum) + const MIN_PERP_NOTIONAL: f64 = 10.0; + + if estimated_perp_notional < MIN_PERP_NOTIONAL { + error!( + "Perp order notional value too small: estimated ${:.2} (collateral_size={}, spot_price={:.2}, lending_ratio={}%, leverage={:.2}x) - minimum required: ${}", + estimated_perp_notional, collateral_size, spot_market_price, lending_ratio, estimated_leverage, MIN_PERP_NOTIONAL + ); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(format!( + "Perp order notional value too small: ${:.2} < ${} minimum. Increase collateral_size or decrease lending_ratio.", + estimated_perp_notional, MIN_PERP_NOTIONAL + )), + )); + } + + info!( + "✓ Perp notional value: ${:.2} (margin={:.2}, leverage={:.2}x) >= ${} minimum", + estimated_perp_notional, estimated_usdc_for_perp, estimated_leverage, MIN_PERP_NOTIONAL + ); + + // 4. Validate estimated hedge size can be properly rounded to perp sz_decimals + let estimated_hedge_size = estimated_perp_notional / perp_market_price; + + validate_trade_size(estimated_hedge_size, perp_asset.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={}): {}", + estimated_usdc_for_perp, estimated_leverage, perp_market_price, estimated_hedge_size, e + ), + )) + })?; + + info!( + "✓ Estimated hedge size {} validated for perp trading (sz_decimals={})", + estimated_hedge_size, perp_asset.sz_decimals + ); + + // 5. Validate user balance + let user_balance = hypercore_client + .get_spot_balance(smart_wallet_address, 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)), + ) + })?; + + if user_balance < collateral_size { + error!( + "Insufficient balance: user has {} but needs {} {}", + user_balance, collateral_size, collateral_ticker + ); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(format!( + "Insufficient balance: user has {} but needs {} {}", + user_balance, collateral_size, collateral_ticker + )), + )); + } + + info!( + "✓ Balance check passed: user has {} {} (required: {})", + user_balance, collateral_ticker, collateral_size + ); + + Ok((user_balance, lending_ratio_f64)) +} + +// Helper function to submit a CoreWriter userOp +#[allow(clippy::too_many_arguments)] +async fn submit_corewriter_userop< + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, +>( + ctx: Arc>, + omni_account: &AccountId, + skeleton_user_op: &SerializablePackedUserOperation, + chain_id: u64, + wallet_index: u32, + call_data: String, + client_id: &str, +) -> Result, PumpxRpcError> { + 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)), + ) + })?; + + let nonce = skeleton_user_op.nonce; + info!("Using nonce {} for smart wallet {}", nonce, smart_wallet_address); + + // Use gas settings from skeleton UserOp if provided, otherwise calculate + let (gas_fees, account_gas_limits, pre_verification_gas) = + if !skeleton_user_op.gas_fees.is_empty() + && skeleton_user_op.gas_fees != "0x" + && !skeleton_user_op.account_gas_limits.is_empty() + && skeleton_user_op.account_gas_limits != "0x" + { + info!("Using gas settings from skeleton UserOp"); + ( + skeleton_user_op.gas_fees.clone(), + skeleton_user_op.account_gas_limits.clone(), + skeleton_user_op.pre_verification_gas, + ) + } 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"), + ) + })?; + ( + pack_gas_fees(max_fee_per_gas.to::(), max_priority_fee_per_gas.to::()), + pack_account_gas_limits(1_000_000, 2_000_000), + 100_000, + ) + }; + + let init_code = skeleton_user_op.init_code.clone(); + + // Build UserOp + let user_op = SerializablePackedUserOperation { + sender: smart_wallet_address.to_string(), + nonce, + init_code, + call_data, + account_gas_limits, + pre_verification_gas, + gas_fees, + paymaster_and_data: if !skeleton_user_op.paymaster_and_data.is_empty() + && skeleton_user_op.paymaster_and_data != "0x" + { + skeleton_user_op.paymaster_and_data.clone() + } else { + encode_simple_paymaster() + }, + signature: None, // Will be signed below + }; + + // Now inline the submit user op logic + info!("Processing SubmitUserOp for 1 UserOperation on chain_id: {}", chain_id); + + 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)), + ) + })?; + + // 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 + )), + )); + } + } + + 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)), + )); + }, + } + } + + 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 = + 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"), + )); + }, + }; + + // 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." + ); + return Err(PumpxRpcError::from( + DetailedError::new(INVALID_USER_OPERATION_CODE, "Invalid user operation") + .with_reason("UserOperation is signed but specifies a paymaster"), + )); + } + 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(), + }; + + // 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 { + 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!("UserOperation passed simulation checks"); + }, + 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), + )); + }, + } + + // Submit UserOperation 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)); + }, + }; + + Ok(transaction_hash) +} + +// Main handler implementation +#[allow(clippy::too_many_arguments)] +async fn handle_request_loan_impl< + CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, +>( + ctx: Arc>, + omni_account: AccountId, + skeleton_user_op: SerializablePackedUserOperation, + chain_id: u64, + wallet_index: u32, + collateral_ticker: &str, + collateral_size_str: &str, + lending_ratio: u32, + client_id: &str, +) -> Result { + let smart_wallet_address_str = &skeleton_user_op.sender; + + let hypercore_client = HyperCoreClient::new(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", chain_id)), + ) + })?; + + let collateral_size = collateral_size_str.parse::().map_err(|e| { + error!("Failed to parse collateral_size: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Invalid collateral_size: {}", e)), + ) + })?; + + // Fetch metadata from HyperCore + let spot_meta = hypercore_client.get_spot_meta().await.map_err(|e| { + error!("Failed to get spot meta: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), + ) + })?; + + let meta = hypercore_client.get_meta().await.map_err(|e| { + error!("Failed to get perp meta: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), + ) + })?; + + // Get asset IDs + 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 perp_asset_id = get_perp_asset_id(collateral_ticker, &meta).map_err(|e| { + error!("Failed to get perp asset ID: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(e), + ) + })?; + + info!( + "Resolved asset IDs - spot: {}, perp: {} for ticker: {}", + spot_asset_id, perp_asset_id, collateral_ticker + ); + + // Get token metadata + let collateral_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)), + ) + })?; + + let _usdc_token = spot_meta + .tokens + .iter() + .find(|t| t.name.eq_ignore_ascii_case("USDC")) + .ok_or_else(|| { + error!("USDC token not found in spot meta"); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("USDC token not found in spot meta"), + ) + })?; + + let perp_asset = 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)), + ) + })?; + + // Fetch market prices + let spot_market_price = hypercore_client + .get_spot_mid_price(collateral_ticker, &spot_meta) + .await + .map_err(|e| { + error!("Failed to get spot market price for {}: {}", collateral_ticker, e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(format!( + "Failed to get spot market price for {}: {}", + collateral_ticker, e + )), + ) + })?; + + let perp_market_price = + hypercore_client.get_perp_mid_price(collateral_ticker).await.map_err(|e| { + error!("Failed to get perp market price for {}: {}", collateral_ticker, e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error").with_reason(format!( + "Failed to get perp market price for {}: {}", + collateral_ticker, e + )), + ) + })?; + + info!( + "Market prices for {} - spot: {} USDC, perp: {} USDC", + collateral_ticker, spot_market_price, perp_market_price + ); + + // Run all early validations + let (_user_balance, lending_ratio_f64) = validate_loan_request_parameters( + &hypercore_client, + smart_wallet_address_str, + collateral_ticker, + collateral_size, + collateral_token, + perp_asset, + lending_ratio, + spot_market_price, + perp_market_price, + ) + .await?; + + // Print initial account state + print_account_state(&hypercore_client, smart_wallet_address_str, "Before Actions").await; + + // Action 1: Sell collateral_size as spot to get X USDC + let clamped_size = clamp_size(collateral_size, collateral_token.sz_decimals); + let target_price = spot_market_price * SPOT_SELL_PRICE_RATIO; + let clamped_price = clamp_price(target_price, collateral_token.sz_decimals, true); + + info!( + "Clamped values for spot sell - size: {} -> {}, price: {} -> {}", + collateral_size, clamped_size, target_price, clamped_price + ); + + 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_price_f64 = clamped_price.parse::().map_err(|e| { + error!("Failed to parse clamped price: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Failed to parse clamped price: {}", e)), + ) + })?; + + let spot_sell_size_units = (clamped_size_f64 * 100_000_000.0) as u64; + let spot_sell_price_units = (clamped_price_f64 * 100_000_000.0) as u64; + + let spot_sell_cloid = generate_cloid(); + let hedge_open_cloid = generate_cloid() + 1; + + info!( + "Generated cloids - spot_sell: {} (hex: 0x{:032x}), hedge_open: {} (hex: 0x{:032x})", + spot_sell_cloid, spot_sell_cloid, hedge_open_cloid, hedge_open_cloid + ); + + // Build and submit spot sell action + let spot_sell_action = build_spot_sell_order( + spot_asset_id, + spot_sell_size_units, + spot_sell_price_units, + spot_sell_cloid, + ); + let spot_sell_corewriter_calldata = encode_send_raw_action(spot_sell_action); + let spot_sell_calldata = + encode_omni_account_execute(get_core_writer_address(), spot_sell_corewriter_calldata); + + let spot_sell_tx_hash = submit_corewriter_userop( + ctx.clone(), + &omni_account, + &skeleton_user_op, + chain_id, + wallet_index, + spot_sell_calldata, + client_id, + ) + .await?; + + info!("Action 1: Spot sell submitted with tx_hash: {:?}", spot_sell_tx_hash); + + // Wait for order to be filled + info!("Polling HyperCore API for spot sell order completion (cloid: {})...", spot_sell_cloid); + let order_filled = hypercore_client + .wait_for_order( + smart_wallet_address_str, + &spot_sell_cloid.to_string(), + 20, + OrderWaitCondition::Filled, + ) + .await + .map_err(|e| { + error!("Spot sell order did not complete: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Spot sell order failed: {}", e)), + ) + })?; + + if !order_filled { + error!("Spot sell order was rejected or canceled"); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("Spot sell order was rejected or canceled"), + )); + } + + info!("Action 1: Spot sell order filled successfully"); + + // Get the actual fill + let spot_sell_fill = hypercore_client + .get_fill_by_cloid(smart_wallet_address_str, spot_sell_cloid) + .await + .map_err(|e| { + error!("Failed to get fill for spot sell order: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Failed to get fill: {}", e)), + ) + })?; + + let usdc_received = calculate_usdc_received_from_spot_sell(&spot_sell_fill).map_err(|e| { + error!("Failed to calculate USDC received: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Failed to calculate USDC: {}", e)), + ) + })?; + + info!("Action 1: Spot sell completed - received {:.2} USDC", usdc_received); + + print_account_state(&hypercore_client, smart_wallet_address_str, "After Action 1 - Spot Sell") + .await; + + // Calculate USDC allocation + let usdc_for_perp = usdc_received * (1.0 - lending_ratio_f64); + let usdc_to_lend = usdc_received * lending_ratio_f64; + + info!( + "USDC allocation: total_received={:.2}, for_perp={:.2}, to_lend={:.2}", + usdc_received, usdc_for_perp, usdc_to_lend + ); + + // Action 2: Move USDC into perps + let mut current_nonce = skeleton_user_op.nonce + 1; + let usdc_for_perp_units = (usdc_for_perp * 1_000_000.0) as u64; + let usd_transfer_action = build_usd_class_transfer_to_perp(usdc_for_perp_units); + let usd_transfer_corewriter_calldata = encode_send_raw_action(usd_transfer_action); + let usd_transfer_calldata = + encode_omni_account_execute(get_core_writer_address(), usd_transfer_corewriter_calldata); + + let mut skeleton_action2 = skeleton_user_op.clone(); + skeleton_action2.nonce = current_nonce; + skeleton_action2.init_code = "0x".to_string(); + + let _usd_transfer_tx_hash = submit_corewriter_userop( + ctx.clone(), + &omni_account, + &skeleton_action2, + chain_id, + wallet_index, + usd_transfer_calldata, + client_id, + ) + .await?; + + info!("Action 2: USD class transfer submitted"); + + // Get initial perp balance + let initial_perp_balance = hypercore_client + .get_perp_clearinghouse_state(smart_wallet_address_str) + .await + .map_err(|e| { + error!("Failed to get initial perp balance: {}", 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| { + error!("Failed to parse initial perp balance: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Failed to parse perp balance: {}", e)), + ) + })?; + + // Wait for USD transfer to complete + let _actual_perp_balance = hypercore_client + .wait_for_perp_balance_increase( + smart_wallet_address_str, + initial_perp_balance, + usdc_for_perp, + 20, + ) + .await + .map_err(|e| { + error!("USD transfer to perp did not complete: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("USD transfer failed: {}", e)), + ) + })?; + + info!("Action 2: USD class transfer completed successfully"); + + print_account_state( + &hypercore_client, + smart_wallet_address_str, + "After Action 2 - USD Transfer to Perp", + ) + .await; + + // Action 3: Open hedge position + current_nonce += 1; + + let desired_leverage: f64 = 1.0 / (1.0 - lending_ratio_f64); + let effective_leverage = desired_leverage.min(perp_asset.max_leverage as f64); + let hedge_size = (usdc_for_perp * effective_leverage) / perp_market_price; + + let clamped_hedge_size = clamp_size(hedge_size, perp_asset.sz_decimals); + let target_hedge_price = perp_market_price * PERP_ENTRY_PRICE_RATIO; + let clamped_hedge_price = clamp_price(target_hedge_price, perp_asset.sz_decimals, false); + + let clamped_hedge_size_f64 = clamped_hedge_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_hedge_price_f64 = clamped_hedge_price.parse::().map_err(|e| { + error!("Failed to parse clamped hedge price: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Failed to parse clamped hedge price: {}", e)), + ) + })?; + + let hedge_size_units = (clamped_hedge_size_f64 * 100_000_000.0) as u64; + let hedge_price_units = (clamped_hedge_price_f64 * 100_000_000.0) as u64; + + let hedge_action = + build_perp_long_order(perp_asset_id, hedge_size_units, hedge_price_units, hedge_open_cloid); + let hedge_corewriter_calldata = encode_send_raw_action(hedge_action); + let hedge_calldata = + encode_omni_account_execute(get_core_writer_address(), hedge_corewriter_calldata); + + let mut skeleton_action3 = skeleton_user_op.clone(); + skeleton_action3.nonce = current_nonce; + skeleton_action3.init_code = "0x".to_string(); + + let _hedge_tx_hash = submit_corewriter_userop( + ctx.clone(), + &omni_account, + &skeleton_action3, + chain_id, + wallet_index, + hedge_calldata, + client_id, + ) + .await?; + + info!("Action 3: Hedge position submitted"); + + // Wait for hedge order to be opened + info!("Polling HyperCore API to verify hedge order is opened (cloid: {})...", hedge_open_cloid); + let order_opened = hypercore_client + .wait_for_order( + smart_wallet_address_str, + &hedge_open_cloid.to_string(), + 20, + OrderWaitCondition::Opened, + ) + .await + .map_err(|e| { + error!("Hedge order was not successfully opened: {}", e); + PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason(format!("Hedge order failed to open: {}", e)), + ) + })?; + + if !order_opened { + error!("Hedge order was rejected or canceled"); + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("Hedge order was rejected or canceled"), + )); + } + + info!("Action 3: Hedge order successfully opened"); + + print_account_state(&hypercore_client, smart_wallet_address_str, "After Action 3 - Hedge Open") + .await; + + let usdc_received_str = format!("{:.2}", usdc_to_lend); + + Ok(RequestLoanTestResponse { + spot_sell_cloid: spot_sell_cloid.to_string(), + hedge_open_cloid: hedge_open_cloid.to_string(), + usdc_received: usdc_received_str, + spot_sell_tx_hash, + hedge_open_tx_hash: None, + }) +} diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/sign_limit_order.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/sign_limit_order.rs index b85d05d2ec..669451d1d8 100644 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/sign_limit_order.rs +++ b/tee-worker/omni-executor/rpc-server/src/methods/omni/sign_limit_order.rs @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use super::common::handle_omni_native_task; use crate::detailed_error::DetailedError; use crate::error_code::{AUTH_VERIFICATION_FAILED_CODE, INTERNAL_ERROR_CODE, PARSE_ERROR_CODE}; use crate::methods::omni::common::check_auth; @@ -22,17 +21,15 @@ use crate::methods::omni::PumpxRpcError; use crate::server::RpcContext; use ethers::types::Bytes; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::NativeTask; -use executor_core::native_task::NativeTaskWrapper; -use executor_core::native_task::PumpxChainId; -use executor_core::native_task::PumxWalletIndex; +use executor_core::native_task::{PumpxChainId, PumxWalletIndex}; use executor_primitives::{utils::hex::FromHexPrefixed, AccountId}; use heima_primitives::Address32; use heima_primitives::IntentId; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; +use pumpx::signer_client::PumpxChainId as _; use serde::Deserialize; use serde::Serialize; +use signer_client::ChainType; use tracing::{debug, error}; #[derive(Debug, Deserialize)] @@ -53,17 +50,13 @@ pub struct SignLimitOrderResponse { } pub fn register_sign_limit_order_params< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_signLimitOrder", |params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + let omni_account = check_auth(&ext).map_err(|e| { error!("Authentication check failed: {:?}", e); PumpxRpcError::from( DetailedError::new( @@ -84,42 +77,55 @@ pub fn register_sign_limit_order_params< debug!("Received omni_signLimitOrder, params: {:?}", params); - let Ok(address) = Address32::from_hex(&user.omni_account) else { + let Ok(address) = Address32::from_hex(&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 omni_account_id = AccountId::from(address); - let wrapper = NativeTaskWrapper::new( - NativeTask::PumpxSignLimitOrder( - AccountId::from(address), - params.chain_id, + // 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)), + )); + }; + + let unsigned_tx_vec: Vec> = + params.unsigned_tx.iter().map(|tx| tx.to_vec()).collect(); + let Ok(signed_txs) = ctx + .signer_client + .request_signatures( + chain, params.wallet_index, - params.unsigned_tx.iter().map(|tx| tx.to_vec()).collect(), - ), - None, - None, - user.client_id, - ); + omni_account_id.into(), + 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"), + )); + }; - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::PumpxSignLimitOrder(signed_txs) => Ok(SignLimitOrderResponse { - intent_id: params.intent_id, - order_id: params.order_id, - chain_id: params.chain_id, - signed_tx: signed_txs.into_iter().map(Bytes::from).collect(), - }), - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Unexpected response type from native task handler"), - )) - }, + Ok(SignLimitOrderResponse { + intent_id: params.intent_id, + order_id: params.order_id, + chain_id: params.chain_id, + signed_tx: signed_txs.into_iter().map(Bytes::from).collect(), }) - .await }) .expect("Failed to register omni_signLimitOrder method"); } diff --git a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_native_task.rs b/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_native_task.rs deleted file mode 100644 index 9b01b41313..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/methods/omni/submit_native_task.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::{ - error_code::*, - hex_encode, - server::RpcContext, - task::{DecryptableTask, RawTask}, - verify_auth::*, - FromHexPrefixed, -}; -use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::{NativeTask, NativeTaskTrait, NativeTaskWrapper}; -use executor_crypto::aes256::{aes_encrypt_default, Aes256Key}; -use jsonrpsee::{ - types::{ErrorCode, ErrorObject, Params}, - RpcModule, -}; -use native_task_handler::handle_native_task; -use parity_scale_codec::{Decode, Encode}; -use std::sync::Arc; -use tracing::error; - -pub fn register_submit_native_task< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, -) { - module - .register_async_method("omni_submitNativeTask", |params, ctx, _| async move { - let (wrapper, maybe_aes_key) = parse(params, ctx.clone()).await.map_err(|e| { - error!("Failed to parse: {:?}", e); - ErrorCode::InternalError - })?; - - // We are directly handling the native task - let native_response = handle_native_task(ctx.to_task_handler_context(), wrapper).await; - - let response = if let Some(aes_key) = maybe_aes_key { - aes_encrypt_default(&aes_key, &native_response.encode()).encode() - } else { - native_response.encode() - }; - - Ok::(hex_encode(response.as_slice())) - }) - .expect("Failed to register omni_submitNativeTask method"); -} - -type ParseResult<'a> = Result<(NativeTaskWrapper, Option), ErrorObject<'a>>; - -async fn parse< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - params: Params<'static>, - ctx: Arc>, -) -> ParseResult<'static> { - let Ok(hex_request) = params.one::() else { - error!("Failed to parse params: {:?}", params); - return Err(ErrorCode::ParseError.into()); - }; - let Ok(request) = RawTask::::from_hex(&hex_request) else { - error!("Failed to parse request: {:?}", hex_request); - return Err(ErrorCode::ServerError(INVALID_RAW_REQUEST_CODE).into()); - }; - - let request_is_encrypted = request.is_encrypted(); - - let (wrapper, maybe_aes_key) = match request { - RawTask::Plain(w) => (w, None), - RawTask::Aes(mut r) => { - let key = r.decrypt_aes_key(Box::new(ctx.shielding_key.clone())).map_err(|_| { - error!("Failed to decrypt AES key"); - ErrorCode::ServerError(DECRYPT_REQUEST_FAILED_CODE) - })?; - let r = r.decrypt(Box::new(ctx.shielding_key.clone())).map_err(|_| { - error!("Failed to decrypt request"); - ErrorCode::ServerError(DECRYPT_REQUEST_FAILED_CODE) - })?; - ( - NativeTaskWrapper::::decode(&mut r.as_slice()).map_err(|_| { - error!("Failed to decode request"); - ErrorCode::ServerError(DECODE_REQUEST_FAILED_CODE) - })?, - Some(key), - ) - }, - }; - - if wrapper.task.require_encrypt() && !request_is_encrypted { - error!("Request is not encrypted, but it is required"); - return Err(ErrorCode::ServerError(REQUIRE_ENCRYPTED_REQUEST_CODE).into()); - } - - if wrapper.task.require_auth() { - let Some(ref auth) = wrapper.auth else { - error!("Request requires authentication, but no auth provided"); - return Err(ErrorCode::ServerError(REQUIRE_AUTHENTICATION_CODE).into()); - }; - verify_auth(ctx, auth).await.map_err(|_| { - error!("Failed to verify auth: {:?}", wrapper.auth); - ErrorCode::ServerError(AUTH_VERIFICATION_FAILED_CODE) - })?; - } - - Ok((wrapper, maybe_aes_key)) -} 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 668bce7e96..ea0756710f 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,4 +1,4 @@ -use super::common::{check_omni_api_response, handle_omni_native_task}; +use super::common::check_omni_api_response; use crate::{ detailed_error::DetailedError, error_code::{INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, PARSE_ERROR_CODE, *}, @@ -7,8 +7,7 @@ use crate::{ Decode, Deserialize, }; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::*; -use executor_storage::{HeimaJwtStorage, Storage}; +use executor_storage::{HeimaJwtStorage, IntentIdStorage, Storage}; use heima_authentication::constants::AUTH_TOKEN_ACCESS_TYPE; use heima_primitives::{ AccountId, Address20, Address32, BinanceConfig, BoundedVec, ChainAsset, CrossChainSwapProvider, @@ -17,13 +16,12 @@ use heima_primitives::{ }; use heima_utils::decode_hex; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use pumpx::constants::*; use pumpx::methods::common::{OrderInfoResponse, SwapType}; use pumpx::methods::send_order_tx::SendOrderTxResponse; use serde::Serialize; use std::str::FromStr; -use tracing::{debug, error}; +use tracing::{debug, error, info}; #[derive(Debug, Deserialize)] pub struct SubmitSwapOrderParams { @@ -106,17 +104,13 @@ struct BackendResponse { } pub fn register_submit_swap_order< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_submitSwapOrder", |params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + let omni_account = check_auth(&ext).map_err(|e| { error!("Authentication check failed: {:?}", e); PumpxRpcError::from( DetailedError::new( @@ -137,7 +131,7 @@ pub fn register_submit_swap_order< debug!("Received omni_submitSwapOrder, params: {:?}", params); - let Ok(omni_account) = AccountId::from_str(&user.omni_account) else { + 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") @@ -181,7 +175,7 @@ pub fn register_submit_swap_order< let storage = HeimaJwtStorage::new(ctx.storage_db.clone()); let Ok(Some(access_token)) = - storage.get(&(omni_account.clone(), AUTH_TOKEN_ACCESS_TYPE)) + storage.get(&(omni_account_id.clone(), AUTH_TOKEN_ACCESS_TYPE)) else { error!("Failed to get access token from storage"); return Err(PumpxRpcError::from( @@ -292,72 +286,116 @@ pub fn register_submit_swap_order< ) })?, ); - let wrapper = NativeTaskWrapper::new( - NativeTask::RequestIntent(omni_account, params.intent_id, Box::new(intent)), - None, - None, - user.client_id, - ); - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::IntentSwapResponse(swap_response) => { - 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(), - )?; - let response = PumpxSubmitSwapOrderResponse { - backend_response: BackendResponse { - limit_order_response: None, - market_order_response: Some(market_order_response), - }, - }; - 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(), - )?; - let response = PumpxSubmitSwapOrderResponse { - backend_response: BackendResponse { - limit_order_response: Some(limit_order_response), - market_order_response: None, - }, - }; - Ok(response) - } + // 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) { + 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"), + )); }, - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( + }; + + 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("Unexpected response type from native task handler"), - )) + .with_reason("Failed to save intent id"), + )); + } + } else { + error!( + "Intent id different than expected, 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"), + )); + } + + let swap_response = match intent { + Intent::SystemRemark(_) + | Intent::TransferNative(_) + | 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"), + )); + }, + Intent::Swap(..) => { + let response = match ctx + .cross_chain_intent_executor + .execute(&omni_account_id, params.intent_id, intent.clone()) + .await + { + Ok((response, _)) => response, + Err(e) => { + error!("Error executing intent: {:?}", e); + ctx.cross_chain_intent_executor.on_execution_error().await; + None + }, + }; + if let Some(response) = response { + response + } else { + return Err(PumpxRpcError::from( + DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") + .with_reason("Intent execution failed"), + )); + } }, - }) - .await + }; + + // 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())?; + let response = PumpxSubmitSwapOrderResponse { + backend_response: BackendResponse { + limit_order_response: None, + market_order_response: Some(market_order_response), + }, + }; + 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())?; + let response = PumpxSubmitSwapOrderResponse { + backend_response: BackendResponse { + limit_order_response: Some(limit_order_response), + market_order_response: None, + }, + }; + Ok(response) + } }) .expect("Failed to register omni_submitSwapOrder method"); } 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 index 3f895aae1f..b076a6b0b8 100644 --- 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 @@ -14,25 +14,31 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use super::common::handle_omni_native_task; 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::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_omni_account_hex, validate_omni_account_length, 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::native_task::{NativeTask, NativeTaskWrapper}; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::{AccountId, ChainId}; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use parity_scale_codec::Decode; use serde::{Deserialize, Serialize}; -use tracing::{debug, error}; +use signer_client::ChainType; +use tracing::{debug, error, info}; #[derive(Debug, Deserialize)] pub struct SubmitUserOpParams { @@ -46,18 +52,12 @@ pub struct SubmitUserOpResponse { pub transaction_hash: Option, } -pub fn register_submit_user_op< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_submit_user_op( + module: &mut RpcModule>, ) { module .register_async_method("omni_submitUserOp", |params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + let omni_account = check_auth(&ext).map_err(|e| { error!("Authentication check failed: {:?}", e); PumpxRpcError::from( DetailedError::new(AUTH_VERIFICATION_FAILED_CODE, "Authentication failed") @@ -81,40 +81,247 @@ pub fn register_submit_user_op< validate_user_operations(¶ms.user_operations).map_err(PumpxRpcError::from)?; - let address_bytes = validate_omni_account_hex(&user.omni_account, "omni_account") + let address_bytes = validate_omni_account_hex(&omni_account, "omni_account") .map_err(PumpxRpcError::from)?; validate_omni_account_length(&address_bytes, "omni_account") .map_err(PumpxRpcError::from)?; - let wrapper = NativeTaskWrapper::new( - NativeTask::SubmitUserOp( - AccountId::decode(&mut &address_bytes[..]).map_err(|e| { - error!("Failed to decode AccountId from bytes: {:?}", e); - PumpxRpcError::from(DetailedError::account_parse_error( - &user.omni_account, - &format!("Failed to decode account: {:?}", e), - )) - })?, - params.user_operations.clone(), - params.chain_id, - params.wallet_index, - ), - None, - None, - user.client_id, + let omni_account_id = AccountId::decode(&mut &address_bytes[..]).map_err(|e| { + error!("Failed to decode AccountId from bytes: {:?}", e); + PumpxRpcError::from(DetailedError::account_parse_error( + &omni_account, + &format!("Failed to decode account: {:?}", e), + )) + })?; + + // Inlined handler logic from handle_submit_user_op + info!( + "Processing SubmitUserOp for {} UserOperations on chain_id: {}", + params.user_operations.len(), + params.chain_id ); - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::SubmitUserOp(transaction_hash) => { - Ok(SubmitUserOpResponse { transaction_hash }) + // 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_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()); }, - _ => { - error!("Unexpected response type from native task handler"); - Err(DetailedError::unexpected_response_type("SubmitUserOp", "Unknown").into()) + 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), + )); }, - }) - .await + } + + // 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 eeedb1ed9b..930035004a 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,21 +14,26 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . -use super::common::handle_omni_native_task; 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 alloy::primitives::Address; +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 executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::{NativeTask, NativeTaskWrapper}; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::{AccountId, ChainId}; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use parity_scale_codec::Decode; use serde::{Deserialize, Serialize}; -use tracing::{debug, error}; +use signer_client::ChainType; +use tracing::{debug, error, info}; #[derive(Debug, Deserialize)] pub struct SubmitUserOpTestParams { @@ -45,13 +50,9 @@ pub struct SubmitUserOpTestResponse { } pub fn register_submit_user_op_test< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_submitUserOpTest", |params, ctx, _ext| async move { @@ -99,37 +100,241 @@ pub fn register_submit_user_op_test< })?; } - let wrapper = NativeTaskWrapper::new( - NativeTask::SubmitUserOp( - 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"), - ) - })?, - params.user_operations.clone(), - params.chain_id, - params.wallet_index, - ), - None, - None, - params.client_id, + 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 ); - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::SubmitUserOp(transaction_hash) => { - Ok(SubmitUserOpTestResponse { transaction_hash }) + // 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()); }, - _ => { - error!("Unexpected response type"); - Err(PumpxRpcError::from( - DetailedError::new(INTERNAL_ERROR_CODE, "Internal error") - .with_reason("Unexpected response type from native task handler"), - )) + 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), + )); }, - }) - .await + } + + // 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(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 a5b0d5de54..8d7750b44d 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,4 +1,3 @@ -use super::common::handle_omni_native_task; use crate::auth_utils::{ verify_payload_timestamp, verify_wildmeta_backend_signature, verify_wildmeta_signature, }; @@ -8,22 +7,27 @@ use crate::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 crate::validation_helpers::{ validate_chain_id, validate_user_operations, validate_wallet_index, }; -use alloy::primitives::{hex, Address}; +use aa_contracts_client::calculate_user_operation_hash; +use alloy::primitives::{hex, Address, Bytes}; +use binance_api::BinancePaymasterApi; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::{NativeTask, NativeTaskWrapper}; use executor_core::types::SerializablePackedUserOperation; use executor_primitives::{ChainId, ClientAuth, Identity, UserAuth, UserId}; use executor_storage::WildmetaTimestampStorage; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; use pumpx::pubkey_to_address; use serde::{Deserialize, Serialize}; use signer_client::ChainType; use std::sync::Arc; -use tracing::{debug, error}; +use tracing::{debug, error, info}; // Chain ID constants const ARBITRUM_MAINNET: ChainId = 42161; @@ -588,13 +592,9 @@ fn validate_backend_calldata( } pub fn register_submit_user_op_with_auth< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_submitUserOpWithAuth", |params, ctx, _ext| async move { @@ -869,28 +869,233 @@ pub fn register_submit_user_op_with_auth< })?; } - let wrapper = NativeTaskWrapper::new( - NativeTask::SubmitUserOp( - account_id, - params.user_operations.clone(), - params.chain_id, - params.wallet_index, - ), - None, - None, - params.client_id, + // Inlined handler logic from handle_submit_user_op + info!( + "Processing SubmitUserOp for {} UserOperations on chain_id: {}", + params.user_operations.len(), + params.chain_id ); - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::SubmitUserOp(transaction_hash) => { - Ok(SubmitUserOpWithAuthResponse { transaction_hash }) + // 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()); }, - _ => { - error!("Unexpected response type from native task handler"); - Err(DetailedError::unexpected_response_type("SubmitUserOp", "Unknown").into()) + 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), + )); }, - }) - .await + } + + // 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(SubmitUserOpWithAuthResponse { transaction_hash }) }) .expect("Failed to register omni_submitUserOpWithAuth method"); } @@ -952,8 +1157,8 @@ fn verify_wildmeta_backend_signature_wrapper( #[cfg(test)] mod tests { use super::*; + use crate::utils::user_op::convert_to_packed_user_op; use executor_storage::StorageDB; - use native_task_handler::convert_to_packed_user_op; use tempfile::tempdir; #[test] 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 fd16f2c803..a1ec73e6b4 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 @@ -5,18 +5,14 @@ use jsonrpsee::{types::ErrorObject, RpcModule}; #[cfg(test)] pub fn register_test_protected_method< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_method("omni_testProtectedMethod", |_, _, ext| { - if let Ok(user) = check_auth(ext) { - return Ok::(user.omni_account); + if let Ok(omni_account) = check_auth(ext) { + return Ok::(omni_account); } panic!("RpcExtensions not found in request extensions"); }) @@ -69,8 +65,6 @@ mod test { let wildmeta_api: Arc> = Arc::new(Box::new(MockWildmetaApi)); let wildmeta_timestamp_storage = Arc::new(WildmetaTimestampStorage::new(db.clone())); - let (solana_intent_executor, _solana_mock_recv) = MockedIntentExecutor::new(); - let (ethereum_intent_executor, _ethereum_mock_recv) = MockedIntentExecutor::new(); let (cross_chain_intent_executor, _cross_chain_mock_recv) = MockedIntentExecutor::new(); let aes_key = [0u8; 32]; let entry_point_clients = HashMap::new(); @@ -89,8 +83,6 @@ mod test { [0u8; 33], // Test ECDSA public key [0u8; 32], // Test bundler private key [0u8; 33], // Test bundler export authorized pubkey - Arc::new(ethereum_intent_executor), - Arc::new(solana_intent_executor), Arc::new(cross_chain_intent_executor), aes_key, Arc::new(entry_point_clients), 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 fe82d44cf3..c7c89cdd44 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,9 +1,10 @@ -use super::common::{check_omni_api_response, handle_omni_native_task}; +use super::common::check_omni_api_response; use crate::{ detailed_error::DetailedError, error_code::*, methods::omni::{common::check_auth, PumpxRpcError}, server::RpcContext, + utils::pumpx::verify_google_code, validation_helpers::{ validate_amount, validate_chain_id, validate_ethereum_address, validate_omni_account_hex, validate_omni_account_length, validate_token_address, validate_wallet_index, @@ -11,12 +12,13 @@ use crate::{ Deserialize, }; use executor_core::intent_executor::IntentExecutor; -use executor_core::native_task::*; +use executor_core::native_task::PumxWalletIndex; use executor_primitives::{utils::hex::FromHexPrefixed, AccountId}; +use executor_storage::{HeimaJwtStorage, Storage}; +use heima_authentication::constants::AUTH_TOKEN_ACCESS_TYPE; use heima_primitives::Address32; use jsonrpsee::RpcModule; -use native_task_handler::NativeTaskOk; -use pumpx::methods::create_transfer_tx::CreateTransferTxResponse; +use pumpx::methods::create_transfer_tx::{CreateTransferTxBody, CreateTransferTxResponse}; use serde::Serialize; use tracing::{debug, error}; @@ -37,43 +39,14 @@ pub struct TransferWithdrawResponse { pub backend_response: CreateTransferTxResponse, } -impl TransferWithdrawParams { - pub fn into_native_task_wrapper( - self, - client_id: String, - omni_account: AccountId, - ) -> NativeTaskWrapper { - NativeTaskWrapper::new( - NativeTask::PumpxTransferWidthdraw( - omni_account, - self.request_id, - self.chain_id, - self.wallet_index, - self.recipient_address, - self.token_ca, - self.amount, - self.google_code, - self.lang, - ), - None, - None, - client_id, - ) - } -} - pub fn register_transfer_withdraw< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method("omni_transferWithdraw", |params, ctx, ext| async move { - let user = check_auth(&ext).map_err(|e| { + let omni_account = check_auth(&ext).map_err(|e| { error!("Authentication check failed: {:?}", e); PumpxRpcError::from(DetailedError::new( AUTH_VERIFICATION_FAILED_CODE, @@ -107,37 +80,72 @@ pub fn register_transfer_withdraw< validate_amount(¶ms.amount, "amount") .map_err(PumpxRpcError::from)?; - let address_bytes = validate_omni_account_hex(&user.omni_account, "omni_account") + let address_bytes = validate_omni_account_hex(&omni_account, "omni_account") .map_err(PumpxRpcError::from)?; validate_omni_account_length(&address_bytes, "omni_account") .map_err(PumpxRpcError::from)?; - let Ok(address) = Address32::from_hex(&user.omni_account) else { + let Ok(address) = Address32::from_hex(&omni_account) else { error!("Failed to parse from omni account after validation"); return Err(DetailedError::account_parse_error( - &user.omni_account, + &omni_account, "Address32 conversion failed" ).into()); }; - let omni_account = AccountId::from(address); - - let wrapper = params.into_native_task_wrapper(user.client_id, omni_account); - - handle_omni_native_task(&ctx, wrapper, |task_ok| match task_ok { - NativeTaskOk::PumpxTransferWithdraw(response) => { - check_omni_api_response(response.clone(), "Transfer withdraw".into())?; - Ok(TransferWithdrawResponse { backend_response: response }) - }, - _ => { - error!("Unexpected response type from native task handler"); - Err(DetailedError::unexpected_response_type( - "PumpxTransferWithdraw", - "Unknown" - ).into()) - }, - }) - .await + let omni_account_id = AccountId::from(address); + + // 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_id.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"))); + }; + + // 2. Verify google code + let verify_success = verify_google_code( + ctx.pumpx_api.as_ref().as_ref(), + &access_token, + params.google_code, + params.lang.clone(), + ) + .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"))); + } + + // 3. Create a transfer tx and send to backend + let body = CreateTransferTxBody { + request_id: params.request_id, + chain_id: params.chain_id, + wallet_index: params.wallet_index, + recipient_address: params.recipient_address, + token_ca: params.token_ca, + amount: params.amount, + }; + + debug!("Calling pumpx create_transfer_tx, body {:?}", body); + 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")) + })?; + + check_omni_api_response(response.clone(), "Transfer withdraw".into())?; + 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 0880c38b94..b8386db386 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 @@ -44,14 +44,8 @@ impl TryFrom for OmniAuth { } } -pub fn register_user_login< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - module: &mut RpcModule< - RpcContext, - >, +pub fn register_user_login( + module: &mut RpcModule>, ) { module .register_async_method("omni_userLogin", |params, ctx, _| async move { 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 f4817f5919..a5f61923f3 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 @@ -15,13 +15,9 @@ pub struct VerifyEmailVerificationCodeTestParams { } pub fn register_verify_email_verification_code_test< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - module: &mut RpcModule< - RpcContext, - >, + module: &mut RpcModule>, ) { module .register_async_method( diff --git a/tee-worker/omni-executor/rpc-server/src/middlewares/rpc_middleware.rs b/tee-worker/omni-executor/rpc-server/src/middlewares/rpc_middleware.rs index 5cb95ea7ca..dbed433d99 100644 --- a/tee-worker/omni-executor/rpc-server/src/middlewares/rpc_middleware.rs +++ b/tee-worker/omni-executor/rpc-server/src/middlewares/rpc_middleware.rs @@ -15,6 +15,7 @@ use tower::layer::util::{Identity, Stack}; #[derive(Clone, Debug)] pub struct RpcExtensions { pub sender: String, + #[allow(dead_code)] pub client_id: String, } diff --git a/tee-worker/omni-executor/rpc-server/src/server.rs b/tee-worker/omni-executor/rpc-server/src/server.rs index 73884ac8de..722d755056 100644 --- a/tee-worker/omni-executor/rpc-server/src/server.rs +++ b/tee-worker/omni-executor/rpc-server/src/server.rs @@ -13,21 +13,15 @@ use executor_core::intent_executor::IntentExecutor; use executor_crypto::aes256::Aes256Key; use executor_storage::{StorageDB, WildmetaTimestampStorage}; use jsonrpsee::{server::Server, RpcModule}; -use native_task_handler::TaskHandlerContext; use pumpx::PumpxApi; use signer_client::SignerClient; use std::collections::HashMap; -// Removed unused PhantomData import use std::marker::{Send, Sync}; use std::{env, net::SocketAddr, sync::Arc}; use tracing::info; use wildmeta_api::WildmetaApi; -pub(crate) struct RpcContext< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, -> { +pub(crate) struct RpcContext { pub shielding_key: ShieldingKey, pub storage_db: Arc, pub mailer_factory: Arc, @@ -43,18 +37,13 @@ pub(crate) struct RpcContext< pub wildmeta_backend_ecdsa_pubkey: [u8; 33], // Compressed ECDSA public key for wildmeta backend signature verification pub bundler_private_key: [u8; 32], // Bundler (accounting ECDSA) private key for export pub bundler_key_export_authorized_pubkey: [u8; 33], // Compressed ECDSA public key authorized to export bundler key - pub ethereum_intent_executor: Arc, - pub solana_intent_executor: Arc, pub cross_chain_intent_executor: Arc, pub aes256_key: Aes256Key, pub entry_point_clients: Arc>>>, } -impl< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, - > RpcContext +impl + RpcContext { #[allow(clippy::too_many_arguments)] pub fn new( @@ -71,8 +60,6 @@ impl< wildmeta_backend_ecdsa_pubkey: [u8; 33], bundler_private_key: [u8; 32], bundler_key_export_authorized_pubkey: [u8; 33], - ethereum_intent_executor: Arc, - solana_intent_executor: Arc, cross_chain_intent_executor: Arc, aes256_key: Aes256Key, entry_point_clients: Arc>>>, @@ -91,40 +78,15 @@ impl< wildmeta_backend_ecdsa_pubkey, bundler_private_key, bundler_key_export_authorized_pubkey, - ethereum_intent_executor, - solana_intent_executor, cross_chain_intent_executor, aes256_key, entry_point_clients, } } - - pub fn to_task_handler_context( - &self, - ) -> Arc< - TaskHandlerContext, - > { - Arc::new(TaskHandlerContext::new( - self.storage_db.clone(), - self.jwt_rsa_private_key.clone(), - self.aes256_key, - self.ethereum_intent_executor.clone(), - self.solana_intent_executor.clone(), - self.cross_chain_intent_executor.clone(), - self.pumpx_api.clone(), - self.signer_client.clone(), - self.binance_api_client.clone(), - self.entry_point_clients.clone(), - )) - } } #[allow(clippy::too_many_arguments)] -pub async fn start_server< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( +pub async fn start_server( port: u16, shielding_key: ShieldingKey, pumpx_api: Arc>, @@ -138,8 +100,6 @@ pub async fn start_server< wildmeta_backend_ecdsa_pubkey: [u8; 33], bundler_private_key: [u8; 32], bundler_key_export_authorized_pubkey: [u8; 33], - ethereum_intent_executor: Arc, - solana_intent_executor: Arc, cross_chain_intent_executor: Arc, aes256_key: Aes256Key, entry_point_clients: Arc>>>, @@ -162,8 +122,6 @@ pub async fn start_server< wildmeta_backend_ecdsa_pubkey, bundler_private_key, bundler_key_export_authorized_pubkey, - ethereum_intent_executor, - solana_intent_executor, cross_chain_intent_executor, aes256_key, entry_point_clients, diff --git a/tee-worker/omni-executor/rpc-server/src/task.rs b/tee-worker/omni-executor/rpc-server/src/task.rs deleted file mode 100644 index f9a52198b5..0000000000 --- a/tee-worker/omni-executor/rpc-server/src/task.rs +++ /dev/null @@ -1,85 +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 . - -// This file contains task related definition that will be used in -// `omni_submitNativeTask` - -use crate::{Decode, Encode}; -use executor_core::native_task::{NativeTaskTrait, NativeTaskWrapper}; -use executor_crypto::{ - aes256::{aes_decrypt, Aes256Key, AesOutput}, - traits::Decrypt, -}; -use std::fmt::Debug; - -// RawTask is the data structure that should be passed into `omni_submitNativeTask` -// as parameters. -// It's of enum type: -// - either a plain (cleartext) NativeTaskWrapper, or -// - an opaque payload generated by aes-encrypting the NativeTaskWrapper, where the -// aes-key itself is also encrypted by enclave's RSA shielding key -#[derive(Encode, Decode, Clone, PartialEq, Eq, Debug)] -pub enum RawTask { - Plain(NativeTaskWrapper), - Aes(AesTask), -} - -impl RawTask { - pub fn is_encrypted(&self) -> bool { - matches!(self, Self::Aes(_)) - } -} - -// Represent a task that can be decrypted by the enclave -pub trait DecryptableTask { - type Error; - fn decrypt( - &mut self, - shielding_key: Box>, - ) -> Result, Self::Error>; -} - -#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)] -pub struct AesTask { - pub key: Vec, - pub payload: AesOutput, -} - -impl DecryptableTask for AesTask { - type Error = (); - - fn decrypt( - &mut self, - enclave_shielding_key: Box>, - ) -> core::result::Result, ()> { - let aes_key: Aes256Key = self.decrypt_aes_key(enclave_shielding_key)?; - aes_decrypt(&aes_key, &mut self.payload).ok_or(()) - } -} - -impl AesTask { - #[allow(clippy::result_unit_err)] - pub fn decrypt_aes_key( - &mut self, - enclave_shielding_key: Box>, - ) -> core::result::Result { - enclave_shielding_key - .decrypt(&self.key) - .map_err(|_| ())? - .try_into() - .map_err(|_| ()) - } -} diff --git a/tee-worker/omni-executor/rpc-server/src/utils/gas_estimation.rs b/tee-worker/omni-executor/rpc-server/src/utils/gas_estimation.rs new file mode 100644 index 0000000000..44695b4982 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/gas_estimation.rs @@ -0,0 +1,437 @@ +// 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::utils::paymaster::calculate_erc20_token_cost; +use crate::utils::types::GasEstimateResponse; +use crate::utils::user_op::pack_account_gas_limits; +use aa_contracts_client::EntryPointClient; +use alloy::primitives::{Address, Bytes, U256}; +use binance_api::BinancePaymasterApi; +use ethereum_rpc::AlloyRpcProvider; +use executor_primitives::ChainId; +use std::sync::Arc; +use tracing::{debug, info}; + +// Gas estimation constants +/// Maximum verification gas limit to prevent DoS attacks +pub const MAX_VERIFICATION_GAS: u128 = 3_000_000; +/// Default verification gas for testing during binary search +pub const DEFAULT_VERIFICATION_GAS_FOR_TESTING: u64 = 1_000_000; +/// Minimum transaction gas as per EIP-155 +pub const MIN_TRANSACTION_GAS: u64 = 21_000; +/// Maximum reasonable gas for normal operations +pub const MAX_NORMAL_GAS: u64 = 10_000_000; +/// Minimum gas for deployment operations +pub const MIN_DEPLOYMENT_GAS: u64 = 100_000; +/// Maximum gas for deployment operations +pub const MAX_DEPLOYMENT_GAS: u64 = 20_000_000; +/// Safety buffer percentage for verification gas +pub const VERIFICATION_GAS_BUFFER_PERCENT: u64 = 20; +/// Default paymaster verification gas limit +pub const DEFAULT_PAYMASTER_VERIFICATION_GAS: u128 = 100_000; +/// Default paymaster post-operation gas limit +pub const DEFAULT_PAYMASTER_POST_OP_GAS: u128 = 50_000; +/// Maximum allowed paymaster gas to prevent abuse +pub const MAX_PAYMASTER_GAS: u128 = 5_000_000; + +/// Main gas estimation function +pub async fn estimate_user_op_gas( + entry_point_client: Arc>, + user_op: aa_contracts_client::PackedUserOperation, + chain_id: ChainId, + binance_api: &dyn BinancePaymasterApi, +) -> Result { + // Step 1: Simulate validation to get base gas requirements + let validation_result = entry_point_client + .simulate_validation(user_op.clone()) + .await + .map_err(|e| format!("Validation simulation failed: {:?}", e))?; + + // Extract preOpGas from validation result + let pre_op_gas = validation_result.returnInfo.preOpGas; + debug!("Validation simulation preOpGas: {}", pre_op_gas); + + // Step 2: Calculate verification gas limit based on validation result + // Add buffer for safety + let buffer_multiplier = U256::from(100 + VERIFICATION_GAS_BUFFER_PERCENT); + let verification_gas_base = pre_op_gas.saturating_mul(buffer_multiplier) / U256::from(100); + let verification_gas_limit = verification_gas_base.min(U256::from(MAX_VERIFICATION_GAS)); + + // Step 3: Binary search for optimal call gas limit + let call_gas_limit = + estimate_call_gas_limit(entry_point_client.clone(), user_op.clone(), chain_id).await?; + + // Step 4: Calculate preVerificationGas (static + dynamic components) + let (static_pvg, dynamic_pvg) = calculate_pre_verification_gas(&user_op, chain_id); + let pre_verification_gas = static_pvg + dynamic_pvg; + + // Step 5: Extract paymaster gas limits if paymaster is present + let (paymaster_verification_gas_limit, paymaster_post_op_gas_limit) = + extract_paymaster_gas_limits(&user_op.paymasterAndData); + + // Step 6: Calculate gas fees with buffer + let (max_fee_per_gas, max_priority_fee_per_gas) = entry_point_client + .calculate_gas_fees_with_buffer(10) // Use 10% additional buffer for estimation + .await + .map_err(|e| format!("Failed to calculate gas fees: {:?}", e))?; + + // Convert to u128 for response, ensuring values are within bounds + let call_gas_limit = call_gas_limit.try_into().map_err(|_| { + format!("Call gas limit {} exceeds maximum supported value", call_gas_limit) + })?; + + let verification_gas_limit = verification_gas_limit.try_into().map_err(|_| { + format!("Verification gas limit {} exceeds maximum supported value", verification_gas_limit) + })?; + + let pre_verification_gas = pre_verification_gas.try_into().map_err(|_| { + format!("Pre-verification gas {} exceeds maximum supported value", pre_verification_gas) + })?; + + let max_fee_per_gas = max_fee_per_gas.try_into().map_err(|_| { + format!("Max fee per gas {} exceeds maximum supported value", max_fee_per_gas) + })?; + + let max_priority_fee_per_gas = max_priority_fee_per_gas.try_into().map_err(|_| { + format!( + "Max priority fee per gas {} exceeds maximum supported value", + max_priority_fee_per_gas + ) + })?; + + // Build initial response with gas estimates + let gas_response = GasEstimateResponse { + call_gas_limit, + verification_gas_limit, + pre_verification_gas, + paymaster_verification_gas_limit, + paymaster_post_op_gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + estimated_token_cost: None, // Will be updated if ERC20 paymaster is detected + }; + + let estimated_token_cost = if !user_op.paymasterAndData.is_empty() { + // Step 7: Calculate token cost if ERC20 paymaster is present + calculate_erc20_token_cost( + binance_api, + &user_op.paymasterAndData, + &gas_response, + &user_op, + chain_id, + ) + .await + } else { + None + }; + + // Update response with token cost estimate + let final_response = GasEstimateResponse { + call_gas_limit, + verification_gas_limit, + pre_verification_gas, + paymaster_verification_gas_limit, + paymaster_post_op_gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + estimated_token_cost, + }; + + info!("Gas estimation complete: {:?}", final_response); + Ok(final_response) +} + +/// Binary search for optimal call gas limit following Rundler's approach +async fn estimate_call_gas_limit( + entry_point_client: Arc>, + user_op: aa_contracts_client::PackedUserOperation, + chain_id: ChainId, +) -> Result { + // Determine gas limits based on operation type + let (min_gas, max_gas) = if !user_op.initCode.is_empty() { + // Deployment operation requires higher gas limits + (U256::from(MIN_DEPLOYMENT_GAS), U256::from(MAX_DEPLOYMENT_GAS)) + } else { + // Normal operation + (U256::from(MIN_TRANSACTION_GAS), U256::from(MAX_NORMAL_GAS)) + }; + + // Step 1: Initial simulation at maximum to get baseline gas usage + let mut test_user_op = user_op.clone(); + let verification_gas = U256::from(DEFAULT_VERIFICATION_GAS_FOR_TESTING); + test_user_op.accountGasLimits = + pack_account_gas_limits(verification_gas.to::(), max_gas.to::()); + + let initial_result = entry_point_client + .simulate_handle_ops(&vec![test_user_op], Address::ZERO) + .await + .map_err(|e| format!("Initial simulation failed: {:?}", e))?; + + if initial_result.is_empty() || !initial_result[0].targetSuccess { + return Err("UserOperation validation failed at maximum gas limit".to_string()); + } + + // Extract actual gas used from the successful simulation + let initial_gas_used = initial_result[0].paid; + + // Step 2: Set initial guess as 2x the gas used (accounts for 63/64ths rule) + let initial_guess = initial_gas_used.saturating_mul(U256::from(2)).min(max_gas); + + info!("Initial gas simulation: used={}, initial_guess={}", initial_gas_used, initial_guess); + + // Step 3: Binary search with 10% tolerance (following Rundler's approach) + let mut lower_bound = min_gas; + let mut upper_bound = initial_guess; + let mut iterations = 0; + const MAX_ITERATIONS: u32 = 20; + const TOLERANCE_PERCENT: u64 = 10; // Stop when bounds are within 10% + + while iterations < MAX_ITERATIONS { + // Check if we've converged within tolerance + if lower_bound > U256::ZERO { + let range = upper_bound - lower_bound; + let tolerance_threshold = lower_bound / U256::from(TOLERANCE_PERCENT); + + if range <= tolerance_threshold { + debug!( + "Binary search converged after {} iterations: range={}, threshold={}", + iterations, range, tolerance_threshold + ); + break; + } + } + + let mid_gas = (lower_bound + upper_bound) / U256::from(2); + + // Test if this gas limit works + let mut test_user_op = user_op.clone(); + test_user_op.accountGasLimits = + pack_account_gas_limits(verification_gas.to::(), mid_gas.to::()); + + let test_result = + entry_point_client.simulate_handle_ops(&vec![test_user_op], Address::ZERO).await; + + match test_result { + Ok(results) if !results.is_empty() && results[0].targetSuccess => { + // Simulation succeeded, try lower gas + upper_bound = mid_gas; + debug!("Binary search iteration {}: gas {} succeeded", iterations, mid_gas); + }, + _ => { + // Simulation failed, need more gas + lower_bound = mid_gas + U256::from(1); + debug!("Binary search iteration {}: gas {} failed", iterations, mid_gas); + }, + } + + iterations += 1; + } + + // Use the upper bound as our estimate (ensures success) + let optimal_gas = upper_bound; + + // Add safety buffer based on chain + let buffer_percent = match chain_id { + 1 => 50, // Mainnet: 50% buffer + 42161 => 30, // Arbitrum: 30% buffer + 8453 => 30, // Base: 30% buffer + 56 => 40, // BSC: 40% buffer + 80084 => 20, // HyperEVM: 20% buffer + _ => 40, // Default: 40% buffer + }; + + let final_gas = optimal_gas.saturating_mul(U256::from(100 + buffer_percent)) / U256::from(100); + + info!("Call gas limit estimation: optimal={}, with_buffer={}", optimal_gas, final_gas); + Ok(final_gas) +} + +/// Calculate preVerificationGas split into static and dynamic components +fn calculate_pre_verification_gas( + user_op: &aa_contracts_client::PackedUserOperation, + chain_id: ChainId, +) -> (U256, U256) { + // EIP-2028 gas costs + const GAS_PER_ZERO_BYTE: u64 = 4; + const GAS_PER_NON_ZERO_BYTE: u64 = 16; + const BASE_TRANSACTION_GAS: u64 = 21_000; + const CREATE2_OVERHEAD_GAS: u64 = 32_000; + const BUNDLE_OVERHEAD_GAS: u64 = 5_000; // Per-UserOp share of bundle transaction overhead + + // Helper function to calculate gas for bytes + let calculate_bytes_gas = |data: &[u8]| -> U256 { + let zero_bytes = data.iter().filter(|&&b| b == 0).count() as u64; + let non_zero_bytes = (data.len() as u64) - zero_bytes; + U256::from(zero_bytes * GAS_PER_ZERO_BYTE + non_zero_bytes * GAS_PER_NON_ZERO_BYTE) + }; + + // === STATIC PVG === + // These costs don't change based on network conditions + let mut static_gas = U256::from(BASE_TRANSACTION_GAS + BUNDLE_OVERHEAD_GAS); + + // Calculate gas for UserOp calldata that will be included in the bundle + static_gas += calculate_bytes_gas(&user_op.callData); + static_gas += calculate_bytes_gas(&user_op.initCode); + static_gas += calculate_bytes_gas(&user_op.paymasterAndData); + static_gas += calculate_bytes_gas(&user_op.signature); + + // Add gas for fixed-size fields + // sender (address as bytes20 padded to bytes32) + static_gas += U256::from(20 * GAS_PER_NON_ZERO_BYTE + 12 * GAS_PER_ZERO_BYTE); + // nonce (usually has many zero bytes) + static_gas += U256::from(32 * GAS_PER_ZERO_BYTE); + // accountGasLimits (bytes32) + static_gas += U256::from(16 * GAS_PER_NON_ZERO_BYTE + 16 * GAS_PER_ZERO_BYTE); + // preVerificationGas (uint256) + static_gas += U256::from(8 * GAS_PER_NON_ZERO_BYTE + 24 * GAS_PER_ZERO_BYTE); + // gasFees (bytes32) + static_gas += U256::from(16 * GAS_PER_NON_ZERO_BYTE + 16 * GAS_PER_ZERO_BYTE); + + // Add deployment overhead if initCode is present + if !user_op.initCode.is_empty() { + static_gas += U256::from(CREATE2_OVERHEAD_GAS); + } + + // === DYNAMIC PVG === + // L2-specific costs that can change based on L1 gas prices + let dynamic_gas = calculate_l2_data_cost(user_op, chain_id); + + // Apply buffers + let static_with_buffer = static_gas.saturating_mul(U256::from(110)) / U256::from(100); // 10% buffer + let dynamic_with_buffer = if dynamic_gas > U256::ZERO { + // L2s need higher buffer due to L1 gas price volatility + dynamic_gas.saturating_mul(U256::from(125)) / U256::from(100) // 25% buffer for L2 + } else { + U256::ZERO + }; + + debug!( + "PreVerificationGas: static={}, dynamic={} (chain_id={}, total_bytes={})", + static_with_buffer, + dynamic_with_buffer, + chain_id, + user_op.callData.len() + user_op.initCode.len() + user_op.paymasterAndData.len() + ); + + (static_with_buffer, dynamic_with_buffer) +} + +/// Calculate L2-specific data availability costs +fn calculate_l2_data_cost( + user_op: &aa_contracts_client::PackedUserOperation, + chain_id: ChainId, +) -> U256 { + // Check if this is an L2 network + let is_l2 = matches!( + chain_id, + 42161 | 421614 | // Arbitrum One, Arbitrum Sepolia + 10 | 11155420 | // Optimism, Optimism Sepolia + 8453 | 84532 | // Base, Base Sepolia + 137 | 80001 // Polygon, Mumbai + ); + + if !is_l2 { + return U256::ZERO; + } + + // Calculate total calldata size that needs to be posted to L1 + let total_bytes = user_op.callData.len() + + user_op.initCode.len() + + user_op.paymasterAndData.len() + + user_op.signature.len() + + 32 * 5; // Fixed fields + + // L2-specific multipliers (these would ideally come from an oracle) + // These are rough estimates - production should use actual L1 gas price oracles + let l1_data_cost_per_byte = match chain_id { + 42161 | 421614 => U256::from(140), // Arbitrum (uses Nitro compression) + 10 | 11155420 => U256::from(160), // Optimism (uses bedrock compression) + 8453 | 84532 => U256::from(160), // Base (same as Optimism) + 137 | 80001 => U256::from(50), // Polygon (cheaper as sidechain) + _ => U256::from(100), // Default for unknown L2s + }; + + let dynamic_cost = U256::from(total_bytes) * l1_data_cost_per_byte; + + debug!( + "L2 data cost calculation: chain_id={}, bytes={}, cost_per_byte={}, total={}", + chain_id, total_bytes, l1_data_cost_per_byte, dynamic_cost + ); + + dynamic_cost +} + +/// Extract and validate paymaster gas limits from paymasterAndData field +fn extract_paymaster_gas_limits(paymaster_and_data: &Bytes) -> (u128, u128) { + // If no paymaster, return zeros + if paymaster_and_data.len() < 20 { + return (0, 0); + } + + // paymasterAndData format: + // [0:20] - paymaster address + // [20:36] - paymaster verification gas limit (uint128) + // [36:52] - paymaster post-op gas limit (uint128) + // [52:] - paymaster data + // check UserOperationLib.sol + + if paymaster_and_data.len() >= 52 { + // Extract verification gas limit (bytes 20-36) + let mut verification_bytes = [0u8; 16]; + verification_bytes.copy_from_slice(&paymaster_and_data[20..36]); + let verification_gas_limit = u128::from_be_bytes(verification_bytes); + + // Extract post-op gas limit (bytes 36-52) + let mut post_op_bytes = [0u8; 16]; + post_op_bytes.copy_from_slice(&paymaster_and_data[36..52]); + let post_op_gas_limit = u128::from_be_bytes(post_op_bytes); + + // Validate gas limits to prevent abuse + let validated_verification = if verification_gas_limit > MAX_PAYMASTER_GAS { + debug!( + "Paymaster verification gas {} exceeds maximum, using default", + verification_gas_limit + ); + DEFAULT_PAYMASTER_VERIFICATION_GAS + } else if verification_gas_limit == 0 { + debug!("Paymaster verification gas is zero, using default"); + DEFAULT_PAYMASTER_VERIFICATION_GAS + } else { + verification_gas_limit + }; + + let validated_post_op = if post_op_gas_limit > MAX_PAYMASTER_GAS { + debug!("Paymaster post-op gas {} exceeds maximum, using default", post_op_gas_limit); + DEFAULT_PAYMASTER_POST_OP_GAS + } else if post_op_gas_limit == 0 { + debug!("Paymaster post-op gas is zero, using default"); + DEFAULT_PAYMASTER_POST_OP_GAS + } else { + post_op_gas_limit + }; + + debug!( + "Validated paymaster gas limits: verification={}, post_op={}", + validated_verification, validated_post_op + ); + + (validated_verification, validated_post_op) + } else { + // Paymaster present but no gas limits specified, use defaults + debug!("Paymaster present but gas limits not specified, using defaults"); + (DEFAULT_PAYMASTER_VERIFICATION_GAS, DEFAULT_PAYMASTER_POST_OP_GAS) + } +} diff --git a/tee-worker/omni-executor/rpc-server/src/utils/mod.rs b/tee-worker/omni-executor/rpc-server/src/utils/mod.rs new file mode 100644 index 0000000000..dee2c18eb7 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/mod.rs @@ -0,0 +1,5 @@ +pub mod gas_estimation; +pub mod paymaster; +pub mod pumpx; +pub mod types; +pub mod user_op; diff --git a/tee-worker/omni-executor/rpc-server/src/utils/paymaster.rs b/tee-worker/omni-executor/rpc-server/src/utils/paymaster.rs new file mode 100644 index 0000000000..c6a29cc0b4 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/paymaster.rs @@ -0,0 +1,418 @@ +// 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 alloy::primitives::{Address, Bytes}; +use binance_api::BinancePaymasterApi; +use std::collections::HashMap; +use tracing::{debug, error, info}; + +use crate::utils::types::{GasEstimateResponse, TokenCostEstimate}; + +// ============================================================================ +// ERC20 Paymaster Exchange Rate Processing +// ============================================================================ + +// Constants for ERC20 paymaster processing +// paymasterAndData format: paymaster_address (20) + validation_gas_limit (16) + postop_gas_limit (16) + paymaster_data +// paymaster_data: token(20) + exchangeRate(32) + validUntil(32) + validAfter(32) +pub const MIN_ERC20_PAYMASTER_DATA_LENGTH: usize = 52 + 20 + 32 + 32 + 32; // 168 bytes minimum +pub const PAYMASTER_DATA_OFFSET: usize = 52; // paymaster address (20) + validation_gas_limit (16) + postop_gas_limit (16) +pub const EXCHANGE_RATE_FEE_PERCENT: f64 = 0.0; // 0% fee for now +pub const WEI_PER_ETH: u128 = 1_000_000_000_000_000_000; // 1e18 wei + +// Whitelisted paymaster addresses that we support (deployed by heima), the logic is: +// - Unsigned userOp: If paymaster specified, **must** be whitelisted +// - Signed userOp: Only allowed if paymasterAndData is empty (technically we could still relay it, but we are a private bundler and don't want to relay arbitrary userOps) +// +// These addresses are assumed to be the same across all EVM chains +// +// Note: the SimplePaymaster should be gradually deprecated in favor of the ERC20PaymasterV1. +// Theoretically user could construct an unsiged userOp to use SimplePaymaster "freely" +// +// TODO: add ERC20 paymaster addresses +pub const WHITELISTED_PAYMASTER_ADDRESSES: &[&str] = &[ + // SimplePaymaster + "0x6255B9F4A4E80BC20eE389fD35DE9d2c029D5912", // staging-v1 + "0xD4dCB31763CBA7295bA4023E9411CB6db607DE07", // prod-v1 + // ERC20PaymasterV1 + "0xA8535e013236E04FAD5dc03eCc4c05A464c01f38", // staging +]; + +// Decode ERC20 paymaster data from paymasterAndData +// Format: paymaster_address(20) + validation_gas_limit(16) + postop_gas_limit(16) + token(20) + exchangeRate(32) + validUntil(32) + validAfter(32) +pub fn decode_erc20_paymaster_data(paymaster_and_data: &[u8]) -> Option<(Address, u128, u64, u64)> { + if paymaster_and_data.len() < MIN_ERC20_PAYMASTER_DATA_LENGTH { + return None; + } + + // Extract token address from paymaster data (bytes 52-71) + let token_address = + Address::from_slice(&paymaster_and_data[PAYMASTER_DATA_OFFSET..PAYMASTER_DATA_OFFSET + 20]); + + // Extract exchange rate (bytes 72-103, 32 bytes, full u256 but we take as u128) + let rate_bytes = &paymaster_and_data[PAYMASTER_DATA_OFFSET + 20..PAYMASTER_DATA_OFFSET + 52]; + let mut exchange_rate_bytes = [0u8; 16]; + exchange_rate_bytes.copy_from_slice(&rate_bytes[16..32]); // Last 16 bytes for u128 + let exchange_rate = u128::from_be_bytes(exchange_rate_bytes); + + // Extract validUntil (bytes 104-135, 32 bytes, take as u64) + let valid_until_bytes = + &paymaster_and_data[PAYMASTER_DATA_OFFSET + 52..PAYMASTER_DATA_OFFSET + 84]; + let mut until_bytes = [0u8; 8]; + until_bytes.copy_from_slice(&valid_until_bytes[24..32]); // Last 8 bytes for u64 + let valid_until = u64::from_be_bytes(until_bytes); + + // Extract validAfter (bytes 136-167, 32 bytes, take as u64) + let valid_after_bytes = + &paymaster_and_data[PAYMASTER_DATA_OFFSET + 84..PAYMASTER_DATA_OFFSET + 116]; + let mut after_bytes = [0u8; 8]; + after_bytes.copy_from_slice(&valid_after_bytes[24..32]); // Last 8 bytes for u64 + let valid_after = u64::from_be_bytes(after_bytes); + + Some((token_address, exchange_rate, valid_until, valid_after)) +} + +// Encode ERC20 paymaster data with updated exchange rate, preserving validUntil and validAfter +pub fn encode_erc20_paymaster_data( + original_data: &[u8], + new_exchange_rate: u128, + valid_until: u64, + valid_after: u64, +) -> Vec { + let mut updated_data = original_data.to_vec(); + + // Skip token address (20 bytes), update exchange rate (32 bytes, big-endian u128 in last 16 bytes) + let rate_start = PAYMASTER_DATA_OFFSET + 20; + let rate_bytes = [0u8; 16] + .iter() + .chain(&new_exchange_rate.to_be_bytes()) + .copied() + .collect::>(); + updated_data[rate_start..rate_start + 32].copy_from_slice(&rate_bytes); + + // Update validUntil (32 bytes) + let valid_until_start = rate_start + 32; + let valid_until_bytes = + [0u8; 24].iter().chain(&valid_until.to_be_bytes()).copied().collect::>(); + updated_data[valid_until_start..valid_until_start + 32].copy_from_slice(&valid_until_bytes); + + // Update validAfter (32 bytes) + let valid_after_start = valid_until_start + 32; + let valid_after_bytes = + [0u8; 24].iter().chain(&valid_after.to_be_bytes()).copied().collect::>(); + updated_data[valid_after_start..valid_after_start + 32].copy_from_slice(&valid_after_bytes); + + updated_data +} + +/// Extract paymaster address from paymasterAndData field +/// Returns None if the data is too short to contain a valid paymaster address +pub fn extract_paymaster_address(paymaster_and_data: &Bytes) -> Option
{ + if paymaster_and_data.len() < 20 { + return None; + } + + // First 20 bytes contain the paymaster address + Some(Address::from_slice(&paymaster_and_data[0..20])) +} + +/// Check if a paymaster address is in our whitelist +/// If so, the userOp must be unsigned to allow exchange rate processing +pub fn is_whitelisted_paymaster(paymaster_address: &Address, whitelisted: &[Address]) -> bool { + whitelisted.contains(paymaster_address) +} + +/// Parse whitelisted paymaster addresses from const strings to Address types +/// Returns empty vec if any address fails to parse (defensive programming) +pub fn parse_whitelisted_paymasters() -> Vec
{ + WHITELISTED_PAYMASTER_ADDRESSES + .iter() + .filter_map(|addr_str| addr_str.parse().ok()) + .collect() +} + +// Comprehensive token mapping with expanded support +#[derive(Debug, Clone)] +pub struct TokenInfo { + pub decimals: u8, + pub binance_pair: &'static str, +} + +// Get supported tokens - organized by chain for better scalability +pub fn get_supported_tokens() -> HashMap<(u64, &'static str), TokenInfo> { + let mut tokens = HashMap::new(); + + // Ethereum mainnet (chain_id 1) + tokens.insert( + (1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC + TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, + ); + tokens.insert( + (1, "0xdac17f958d2ee523a2206206994597c13d831ec7"), // USDT + TokenInfo { decimals: 6, binance_pair: "ETHUSDT" }, + ); + tokens.insert( + (1, "0x6b175474e89094c44da98b954eedeac495271d0f"), // DAI + TokenInfo { decimals: 18, binance_pair: "ETHDAI" }, + ); + + // Arbitrum One (chain_id 42161) + tokens.insert( + (42161, "0xaf88d065e77c8cc2239327c5edb3a432268e5831"), // USDC + TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, + ); + tokens.insert( + (42161, "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9"), // USDT + TokenInfo { decimals: 6, binance_pair: "ETHUSDT" }, + ); + + // Arbitrum Sepolia (chain_id 421614) + tokens.insert( + (421614, "0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d"), // USDC + TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, + ); + + // BNB Smart Chain (chain_id 56) + tokens.insert( + (56, "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d"), // USDC + TokenInfo { decimals: 18, binance_pair: "ETHUSDC" }, + ); + tokens.insert( + (56, "0x55d398326f99059ff775485246999027b3197955"), // USDT + TokenInfo { decimals: 18, binance_pair: "ETHUSDT" }, + ); + + // Base (Chain ID 8453) + tokens.insert( + (8453, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"), // USDC + TokenInfo { decimals: 6, binance_pair: "ETHUSDC" }, + ); + + tokens +} + +// Map token address to Binance symbol and return (symbol, decimals) from hardcoded mapping +pub async fn get_token_info_from_mapping( + token_address: &Address, + chain_id: u64, +) -> Option<(String, u8)> { + let tokens = get_supported_tokens(); + let address_str = token_address.to_string().to_lowercase(); + + // Look up token info from our comprehensive mapping + if let Some(token_info) = tokens.get(&(chain_id, address_str.as_str())) { + // Use hardcoded values from our mapping + return Some((token_info.binance_pair.to_string(), token_info.decimals)); + } + + debug!( + "Token address {} on chain {} is not supported. Supported tokens: {:?}", + token_address, + chain_id, + tokens.keys().filter(|(cid, _)| *cid == chain_id).collect::>() + ); + None +} + +// Calculate exchange rate using Binance API +pub async fn calculate_exchange_rate_with_binance( + binance_api: &dyn BinancePaymasterApi, + token_symbol: &str, + token_decimals: u8, +) -> Result { + // Query price from Binance + // For ETHUSDC, this returns how many USDC for 1 ETH (e.g., 4479.99) + let price_str = binance_api + .get_symbol_price(token_symbol) + .await + .map_err(|e| format!("Failed to get price for {}: {:?}", token_symbol, e))?; + + let tokens_per_eth: f64 = price_str + .parse() + .map_err(|e| format!("Failed to parse price '{}': {}", price_str, e))?; + + if tokens_per_eth <= 0.0 { + return Err(format!("Invalid price from Binance: {}", tokens_per_eth)); + } + + // Apply fee percentage (increase rate to charge more tokens) + let tokens_per_eth_with_fee = tokens_per_eth * (1.0 + EXCHANGE_RATE_FEE_PERCENT / 100.0); + + // Calculate exchange rate according to ERC20PaymasterV1.sol: + // exchangeRate = tokensPerEth * 10^tokenDecimals + // Example: For 6-decimal USDC at $4000/ETH: rate = 4000 * 10^6 = 4000000000 + // This rate is used as: requiredTokenAmount = (maxCost * exchangeRate) / 1e18 + // Where maxCost is in wei, so 1 ETH of gas = 10^18 wei + // Result: (10^18 * 4000000000) / 10^18 = 4000000000 USDC units = 4000 USDC ✓ + + let exchange_rate_f64 = tokens_per_eth_with_fee * (10_f64.powi(token_decimals as i32)); + + if exchange_rate_f64 <= 0.0 || exchange_rate_f64 >= u128::MAX as f64 { + return Err(format!("Exchange rate {} is out of valid range", exchange_rate_f64)); + } + + info!( + "Calculated exchange rate for {}: {} tokens per ETH -> rate {} (with {}% fee)", + token_symbol, tokens_per_eth, exchange_rate_f64 as u128, EXCHANGE_RATE_FEE_PERCENT + ); + + Ok(exchange_rate_f64 as u128) +} + +// Process ERC20 paymaster data +pub async fn process_erc20_paymaster_data( + binance_api: &dyn BinancePaymasterApi, + paymaster_and_data: &Bytes, + chain_id: u64, +) -> Result, String> { + let data_bytes = paymaster_and_data.as_ref(); + + // Try to decode as ERC20 paymaster data + if let Some((token_address, original_rate, valid_until, valid_after)) = + decode_erc20_paymaster_data(data_bytes) + { + info!( + "Detected ERC20 paymaster with token {} (original rate: {}, valid_until: {}, valid_after: {})", + token_address, original_rate, valid_until, valid_after + ); + + // Get token symbol and decimals from hardcoded mapping + let (token_symbol, token_decimals) = match get_token_info_from_mapping( + &token_address, + chain_id, + ) + .await + { + Some((symbol, decimals)) => (symbol, decimals), + None => { + return Err(format!( + "Token address {} on chain {} is not supported for price queries. Consider adding it to the supported tokens list or ensure it has a trading pair on Binance.", + token_address, chain_id + )); + }, + }; + + // Calculate new exchange rate + let new_exchange_rate = + calculate_exchange_rate_with_binance(binance_api, &token_symbol, token_decimals) + .await?; + + // Encode updated paymaster data with new exchange rate, keeping original timestamps + let updated_paymaster_data = encode_erc20_paymaster_data( + data_bytes, + new_exchange_rate, + valid_until, // Keep original validUntil + valid_after, // Keep original validAfter + ); + + info!( + "Updated ERC20 paymaster exchange rate: {} -> {} for token {}", + original_rate, new_exchange_rate, token_address + ); + + Ok(Some(Bytes::from(updated_paymaster_data))) + } else { + // Not ERC20 paymaster data or insufficient length, return as-is + debug!("PaymasterAndData is not ERC20 paymaster format, skipping processing"); + Ok(None) + } +} + +/// Calculate estimated token cost for ERC20 paymaster operations +pub async fn calculate_erc20_token_cost( + binance_api: &dyn BinancePaymasterApi, + paymaster_and_data: &Bytes, + gas_estimates: &GasEstimateResponse, + user_op: &aa_contracts_client::PackedUserOperation, + chain_id: u64, +) -> Option { + // Decode paymaster data + let (token_address, _, valid_until, valid_after) = + decode_erc20_paymaster_data(paymaster_and_data.as_ref())?; + + // Check if timestamps are valid + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok()? + .as_secs(); + + if current_time < valid_after || current_time > valid_until { + debug!("Exchange rate timestamps not valid for estimation"); + return None; + } + + // Get token info from mapping + let (token_symbol, token_decimals) = + get_token_info_from_mapping(&token_address, chain_id).await?; + + // Get fresh exchange rate from Binance + let exchange_rate = match calculate_exchange_rate_with_binance( + binance_api, + &token_symbol, + token_decimals, + ) + .await + { + Ok(rate) => rate, + Err(e) => { + error!("Failed to get exchange rate for token cost estimation: {}", e); + return None; + }, + }; + + // Extract gas limits from estimates + let call_gas = gas_estimates.call_gas_limit; + let verification_gas = gas_estimates.verification_gas_limit; + let pre_verification_gas = gas_estimates.pre_verification_gas; + let paymaster_verification_gas = gas_estimates.paymaster_verification_gas_limit; + let paymaster_post_op_gas = gas_estimates.paymaster_post_op_gas_limit; + + // Extract gas price from the UserOp's gasFees field + // gasFees format: maxFeePerGas (16 bytes) | maxPriorityFeePerGas (16 bytes) + let gas_fees_bytes = user_op.gasFees.0; + let mut max_fee_bytes = [0u8; 16]; + max_fee_bytes.copy_from_slice(&gas_fees_bytes[0..16]); // First 16 bytes for maxFeePerGas + let max_fee_per_gas = u128::from_be_bytes(max_fee_bytes); + + // Calculate total gas cost in wei + // Include all gas components including paymaster overhead + let total_gas = call_gas + + verification_gas + + pre_verification_gas + + paymaster_verification_gas + + paymaster_post_op_gas; + + let total_cost_wei = total_gas.saturating_mul(max_fee_per_gas); + + // Calculate token amount using same formula as the contract with ceil rounding + // tokenAmount = ceil((gasCost * exchangeRate) / 1e18) + let numerator = total_cost_wei.saturating_mul(exchange_rate); + let token_amount = if numerator == 0 { + 0 + } else { + numerator.saturating_add(WEI_PER_ETH - 1).saturating_div(WEI_PER_ETH) + }; + + // Add 10% safety buffer for price fluctuations + let token_amount_with_buffer = token_amount.saturating_mul(110).saturating_div(100); + + Some(TokenCostEstimate { + token_address: token_address.to_string(), + amount: token_amount_with_buffer, + decimals: token_decimals, + exchange_rate, + }) +} diff --git a/tee-worker/omni-executor/rpc-server/src/utils/pumpx.rs b/tee-worker/omni-executor/rpc-server/src/utils/pumpx.rs new file mode 100644 index 0000000000..3cd49b0bb1 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/pumpx.rs @@ -0,0 +1,44 @@ +// 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 pumpx::PumpxApi; +use tracing::{debug, error}; + +/// Verify a Google authentication code using the Pumpx API +pub async fn verify_google_code( + pumpx_api: &dyn PumpxApi, + access_token: &str, + google_code: String, + language: Option, +) -> bool { + debug!("Calling pumpx verify_google_code, code: {}", google_code); + let verify_result = pumpx_api.verify_google_code(access_token, google_code, language).await; + verify_result.map_or_else( + |e| { + error!("Google code verification request failed: {:?}", e); + false + }, + |res| { + res.data.result.map_or_else( + || { + error!("Google code verification response result is none"); + false + }, + |success| success, + ) + }, + ) +} diff --git a/tee-worker/omni-executor/rpc-server/src/utils/types.rs b/tee-worker/omni-executor/rpc-server/src/utils/types.rs new file mode 100644 index 0000000000..04844bd48e --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/types.rs @@ -0,0 +1,28 @@ +use parity_scale_codec::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +/// Information about estimated token cost for ERC20 paymaster operations +#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq)] +pub struct TokenCostEstimate { + /// The ERC20 token address used for gas payment + pub token_address: String, + /// Token amount in smallest unit (e.g., wei for 18 decimal tokens) + pub amount: u128, + /// Token decimals for frontend formatting + pub decimals: u8, + /// Exchange rate used for calculation (tokens per ETH * 10^18) + pub exchange_rate: u128, +} + +/// Gas estimation response for UserOperation +#[derive(Debug, Serialize, Deserialize, Clone, Encode, Decode, PartialEq, Eq)] +pub struct GasEstimateResponse { + pub call_gas_limit: u128, + pub verification_gas_limit: u128, + pub pre_verification_gas: u128, + pub paymaster_verification_gas_limit: u128, + pub paymaster_post_op_gas_limit: u128, + pub max_fee_per_gas: u128, + pub max_priority_fee_per_gas: u128, + pub estimated_token_cost: Option, +} 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 new file mode 100644 index 0000000000..d0e94a0d22 --- /dev/null +++ b/tee-worker/omni-executor/rpc-server/src/utils/user_op.rs @@ -0,0 +1,126 @@ +// 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 alloy::primitives::{Address, Bytes, FixedBytes, U256}; +use executor_core::types::SerializablePackedUserOperation; +use executor_primitives::utils::hex::decode_hex; + +/// Pack verification and call gas limits into a single 32-byte value +/// Format: verification_gas (16 bytes) | call_gas (16 bytes) +pub fn pack_account_gas_limits(verification_gas: u128, call_gas: u128) -> FixedBytes<32> { + let packed: U256 = (U256::from(verification_gas) << 128) | U256::from(call_gas); + FixedBytes::from(packed.to_be_bytes()) +} + +#[cfg(test)] +pub fn unpack_verification_gas_limit(packed: FixedBytes<32>) -> u128 { + let value = U256::from_be_bytes(packed.0); + let result: U256 = (value >> 128) & U256::from(u128::MAX); + result.to::() +} + +#[cfg(test)] +pub fn unpack_call_gas_limit(packed: FixedBytes<32>) -> u128 { + let value = U256::from_be_bytes(packed.0); + let result: U256 = value & U256::from(u128::MAX); + result.to::() +} + +/// Convert Substrate signature to Ethereum ECDSA format +/// Returns signature in format: [r (32 bytes), s (32 bytes), v (1 byte)] +pub fn substrate_to_ethereum_signature(substrate_sig: &[u8]) -> Result<[u8; 65], &'static str> { + if substrate_sig.len() != 65 { + return Err("Invalid signature length"); + } + + // Parse as (r, s, v) format - most common + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r.copy_from_slice(&substrate_sig[0..32]); + s.copy_from_slice(&substrate_sig[32..64]); + let substrate_v = substrate_sig[64]; + + // Convert recovery parameter: 0/1 -> 27/28 + let ethereum_v = match substrate_v { + 0 => 27, + 1 => 28, + 27 => 27, // Already Ethereum format + 28 => 28, // Already Ethereum format + _ => return Err("Invalid recovery parameter"), + }; + + // Build Ethereum signature: [r, s, v] + let mut ethereum_sig = [0u8; 65]; + ethereum_sig[0..32].copy_from_slice(&r); + ethereum_sig[32..64].copy_from_slice(&s); + ethereum_sig[64] = ethereum_v; + + Ok(ethereum_sig) +} + +/// Convert SerializablePackedUserOperation to aa_contracts_client::PackedUserOperation +pub fn convert_to_packed_user_op( + user_op: SerializablePackedUserOperation, +) -> Result { + use std::str::FromStr; + + // Helper function to parse hex string to fixed bytes + let parse_hex_fixed = + |hex_str: &str, expected_len: usize, name: &str| -> Result, String> { + let bytes = decode_hex(hex_str) + .map_err(|e| format!("Invalid hex string '{}' '{}': {}", hex_str, name, e))?; + if bytes.len() != expected_len { + return Err(format!( + "Expected {} bytes, got {} for '{}'", + expected_len, + bytes.len(), + hex_str + )); + } + Ok(bytes) + }; + + Ok(aa_contracts_client::PackedUserOperation { + sender: Address::from_str(&user_op.sender) + .map_err(|e| format!("Invalid sender address '{}': {}", user_op.sender, e))?, + nonce: U256::from(user_op.nonce), + initCode: Bytes::from( + decode_hex(&user_op.init_code).map_err(|e| format!("Invalid init_code hex: {}", e))?, + ), + callData: Bytes::from( + decode_hex(&user_op.call_data).map_err(|e| format!("Invalid call_data hex: {}", e))?, + ), + accountGasLimits: { + let bytes = parse_hex_fixed(&user_op.account_gas_limits, 32, "account_gas_limits")?; + FixedBytes::from_slice(&bytes) + }, + preVerificationGas: U256::from(user_op.pre_verification_gas), + gasFees: { + let bytes = parse_hex_fixed(&user_op.gas_fees, 32, "gas_fees")?; + FixedBytes::from_slice(&bytes) + }, + paymasterAndData: Bytes::from( + decode_hex(&user_op.paymaster_and_data) + .map_err(|e| format!("Invalid paymaster_and_data hex: {}", e))?, + ), + signature: match user_op.signature { + Some(sig) => { + Bytes::from(decode_hex(&sig).map_err(|e| format!("Invalid signature hex: {}", e))?) + }, + None => Bytes::new(), // Empty signature for unsigned operations + }, + }) +} diff --git a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs index 769ec829a1..512cc10ce1 100644 --- a/tee-worker/omni-executor/rpc-server/src/verify_auth.rs +++ b/tee-worker/omni-executor/rpc-server/src/verify_auth.rs @@ -49,12 +49,8 @@ impl Display for AuthenticationError { } } -pub async fn verify_auth< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, - CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, ->( - ctx: Arc>, +pub async fn verify_auth( + ctx: Arc>, auth: &OmniAuth, ) -> Result<(), AuthenticationError> { match auth { @@ -110,11 +106,9 @@ pub fn verify_web3_authentication( } pub fn verify_email_authentication< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - ctx: Arc>, + ctx: Arc>, client_id: &str, email: &str, verification_code: &VerificationCode, @@ -147,11 +141,9 @@ pub fn verify_auth_token_authentication( } pub async fn verify_oauth2_authentication< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - ctx: Arc>, + ctx: Arc>, client_id: &str, payload: &OAuth2Data, ) -> Result { @@ -159,11 +151,9 @@ pub async fn verify_oauth2_authentication< } async fn verify_oauth2_provider< - EthereumIntentExecutor: IntentExecutor + Send + Sync + 'static, - SolanaIntentExecutor: IntentExecutor + Send + Sync + 'static, CrossChainIntentExecutor: IntentExecutor + Send + Sync + 'static, >( - ctx: Arc>, + ctx: Arc>, client_id: &str, payload: &OAuth2Data, ) -> Result {