diff --git a/.env.example b/.env.example index 12a1f74..4e8b0f1 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,8 @@ BOT_PRIVATE_KEY= # rpc BLOXROUTE_AUTH_HEADER= -WSS_RPC_URL= +WSS_RPC_URL_FOURMEME= +WSS_RPC_URL_PANCAKE= HTTP_RPC_URL= HTTP_SEND_TX_URL= diff --git a/src/pvp/config.rs b/src/pvp/config.rs index a5e8406..e99b5c6 100644 --- a/src/pvp/config.rs +++ b/src/pvp/config.rs @@ -67,7 +67,8 @@ pub struct EnvConfig { // -------- rpc config /// The WebSocket RPC URL - pub wss_rpc_url: String, + pub wss_rpc_url_fourmeme: String, + pub wss_rpc_url_pancake: String, /// The HTTP RPC URL pub http_rpc_url: String, /// The HTTP send tx URL: used to prevent rate limiting @@ -130,7 +131,8 @@ impl Default for EnvConfig { tx_simulation: true, // -------- rpc config - wss_rpc_url: env::var("WSS_RPC_URL").expect("WSS_RPC_URL not set"), + wss_rpc_url_fourmeme: env::var("WSS_RPC_URL_FOURMEME").expect("WSS_RPC_URL_FOURMEME not set"), + wss_rpc_url_pancake: env::var("WSS_RPC_URL_PANCAKE").expect("WSS_RPC_URL_PANCAKE not set"), http_rpc_url: env::var("HTTP_RPC_URL").expect("HTTP_RPC_URL not set"), http_send_tx_url: env::var("HTTP_SEND_TX_URL").expect("HTTP_SEND_TX_URL not set"), bloxroute_auth_header: env::var("BLOXROUTE_AUTH_HEADER").expect("BLOXROUTE_AUTH_HEADER not set"), @@ -263,8 +265,13 @@ impl EnvConfig { self } - pub fn with_wss_rpc_url(mut self, url: String) -> Self { - self.wss_rpc_url = url; + pub fn with_wss_rpc_url_fourmeme(mut self, url: String) -> Self { + self.wss_rpc_url_fourmeme = url; + self + } + + pub fn with_wss_rpc_url_pancake(mut self, url: String) -> Self { + self.wss_rpc_url_pancake = url; self } diff --git a/src/pvp/service/auto_sell.rs b/src/pvp/service/auto_sell.rs index c357fff..dc0251e 100644 --- a/src/pvp/service/auto_sell.rs +++ b/src/pvp/service/auto_sell.rs @@ -6,33 +6,35 @@ //! Known issues: When checking latest_trade_info, there is a competitive relationship between auto_sell and strategy_pancake //! modules, so the same token may be sold continuously in one minute. -use chrono::Duration as ChronoDuration; -use std::marker::PhantomData; -use std::time::Duration; -use std::{str::FromStr, sync::Arc}; +use std::{marker::PhantomData, str::FromStr, sync::Arc, time::Duration}; -use crate::pvp::abi::IERC20::IERC20Instance; -use crate::pvp::config::EnvConfig; -use crate::pvp::interface::outer_pool_trader::IOuterPoolTrader; -use crate::pvp::misc::constant::{ONE_ETHER, WBNB_CONTRACT}; -use crate::pvp::misc::error::CustomError; -use crate::pvp::misc::logger::Logger; -use crate::pvp::strategy::helper::{ - StrategyType, get_diff_time_since_migration, get_initial_fourmeme_buy_amount, get_pair_wbnb_balance, - get_wbnb_amount_when_bought_token, -}; -use crate::pvp::trade::outer_pool_trader::OuterPoolTrader; -use crate::pvp::{interface::db::IDatabase, misc::constant::BSC_CHAIN_ID}; -use alloy::primitives::{Address, U256}; use alloy::{ network::Network, + primitives::{Address, U256}, providers::{Provider, WalletProvider}, rpc::types::{TransactionReceipt, TransactionRequest}, }; -use chrono::Utc; +use chrono::{Duration as ChronoDuration, Utc}; use harpoon::bloXroute_private_tx::BloXrouteService; use tokio::{sync::Mutex, time}; +use crate::pvp::{ + abi::IERC20::IERC20Instance, + config::EnvConfig, + db::db::TradeInfo, + interface::{db::IDatabase, outer_pool_trader::IOuterPoolTrader}, + misc::{ + constant::{BSC_CHAIN_ID, ONE_ETHER, WBNB_CONTRACT}, + error::CustomError, + logger::Logger, + }, + strategy::helper::{ + StrategyType, get_diff_time_since_migration, get_initial_fourmeme_buy_amount, get_pair_wbnb_balance, + get_wbnb_amount_when_bought_token, + }, + trade::outer_pool_trader::OuterPoolTrader, +}; + pub struct AutoSeller { db: Arc, http_provider: P, @@ -68,6 +70,7 @@ where } } + /// Entry point of the auto sell service pub async fn start(&self) { let mut interval = time::interval(Duration::from_secs(self.interval_seconds)); @@ -75,221 +78,225 @@ where interval.tick().await; let all_bought_tokens = self.db.get_all_token_addresses(BSC_CHAIN_ID).await.unwrap_or_default(); + for token in all_bought_tokens { let meme_token = Address::from_str(&token).unwrap_or_default(); - match self.sell_token(meme_token).await { - Ok(_) => { - // prevent bloxroute rate limite - // known issue: it will conflict with the executor module when we use private transactions type - time::sleep(Duration::from_secs(5)).await; - } - Err(_) => {} + + if let Ok(_) = self.sell_token(meme_token).await { + // prevent bloxroute rate limit + // known issue: it will conflict with the executor module when we use private transactions type + time::sleep(Duration::from_secs(5)).await; } } } } + /// Sell meme tokens based on the strategy async fn sell_token(&self, meme_token: Address) -> Result<(), CustomError> { - // 1. get the latest trade info of that token let latest_trade_info = self .db .get_latest_trade_info(BSC_CHAIN_ID, &meme_token.to_string()) .await .unwrap_or_default(); - // 2. check if the latest trade has exceeded interval_seconds in the past, no trade also counts as exceeding interval_seconds. - let now = Utc::now(); - let exceeded_one_minute = match latest_trade_info.clone() { - None => true, // no trade also counts as exceeding interval_seconds - Some(trade) => { - let duration = now.signed_duration_since(trade.updated_at); - duration.num_seconds() >= 60 - } - }; - if !exceeded_one_minute { + // check we have passed the time threshold + if !self.has_exceeded_time_threshold(&latest_trade_info).await { return Err(CustomError::StrategyDoNothing()); } - // 3. check if the bot still holds the token - let erc20_contract = IERC20Instance::new(meme_token, self.http_provider.clone()); - let bot_meme_balance = erc20_contract - .balanceOf(self.env_config.bot_address) - .call() - .await - .unwrap_or_default(); + // check the bot's meme balance + let bot_meme_balance = self.get_bot_meme_balance(meme_token).await?; if bot_meme_balance == U256::from(0) { return Err(CustomError::StrategyDoNothing()); } - // 4. get the current pair's wbnb balance + // check the current pair's wbnb balance let current_pair_wbnb_balance = get_pair_wbnb_balance(&self.http_provider, meme_token) .await .unwrap_or_default(); - // the pair's wbnb balance is 0, it means it is still in the fourmeme stage, so skip it if current_pair_wbnb_balance == U256::from(0) { return Err(CustomError::StrategyDoNothing()); } - // 4. different strategies, different sell logic let strategy = latest_trade_info.map(|trade| trade.strategy).unwrap_or_default(); match strategy.as_str() { s if s == StrategyType::Bundle.to_string() || s == StrategyType::TokenMigrated.to_string() => { - let wbnb_amount_when_bought = get_wbnb_amount_when_bought_token(&*self.db, meme_token) + self.handle_bundle_or_migrated_strategy( + meme_token, + bot_meme_balance, + current_pair_wbnb_balance, + &strategy, + ) + .await + } + s if s == StrategyType::Progress.to_string() => { + self.handle_progress_strategy(meme_token, bot_meme_balance, current_pair_wbnb_balance, &strategy) .await - .unwrap_or_default(); - - if current_pair_wbnb_balance < wbnb_amount_when_bought - U256::from(3) * ONE_ETHER { - let mut trader = self.outer_pool_trader.lock().await; - trader - .sell_token( - strategy.clone(), - &self.bloxroute_service, - self.env_config.tx_submit_type, - meme_token, - WBNB_CONTRACT, - bot_meme_balance, - self.env_config.slippage, - ) - .await - .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; - - Logger.event(&format!("Auto sell token {}: sell all", meme_token)); - return Ok(()); - } else if current_pair_wbnb_balance > wbnb_amount_when_bought { - let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&*self.db, meme_token) - .await - .unwrap_or_default(); - let five_percentage = initial_fourmeme_buy_amount / U256::from(20); + } + _ => Err(CustomError::StrategyDoNothing()), + } + } - if bot_meme_balance < five_percentage { - if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { - return Err(CustomError::StrategyDoNothing()); - } else { - let mut trader = self.outer_pool_trader.lock().await; - trader - .sell_token( - strategy.clone(), - &self.bloxroute_service, - self.env_config.tx_submit_type, - meme_token, - WBNB_CONTRACT, - bot_meme_balance, - self.env_config.slippage, - ) - .await - .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; + async fn handle_bundle_or_migrated_strategy( + &self, + meme_token: Address, + bot_meme_balance: U256, + current_pair_wbnb_balance: U256, + strategy: &str, + ) -> Result<(), CustomError> { + let wbnb_amount_when_bought = get_wbnb_amount_when_bought_token(&*self.db, meme_token) + .await + .unwrap_or_default(); - Logger.event(&format!( - "Auto sell token {}: pair reaches 100 WBNB, sell all", - meme_token - )); - return Ok(()); - } - } + if current_pair_wbnb_balance < wbnb_amount_when_bought - U256::from(3) * ONE_ETHER { + self.sell_all_tokens(meme_token, bot_meme_balance, strategy, "sell all") + .await + } else if current_pair_wbnb_balance > wbnb_amount_when_bought { + let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&*self.db, meme_token) + .await + .unwrap_or_default(); + let five_percentage = initial_fourmeme_buy_amount / U256::from(20); - let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(50); - let mut trader = self.outer_pool_trader.lock().await; - trader - .sell_token( - strategy.clone(), - &self.bloxroute_service, - self.env_config.tx_submit_type, + if bot_meme_balance < five_percentage { + if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { + return Err(CustomError::StrategyDoNothing()); + } else { + return self + .sell_all_tokens( meme_token, - WBNB_CONTRACT, - meme_amount_to_sell, - self.env_config.slippage, + bot_meme_balance, + strategy, + "pair reaches 100 WBNB, sell all", ) - .await - .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; - - Logger.event(&format!("Auto sell token {}: (X, +∞): sell 2%", meme_token)); - return Ok(()); + .await; } + } + let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(50); + self.sell_partial_tokens(meme_token, meme_amount_to_sell, strategy, "(X, +∞): sell 2%") + .await + } else { + Err(CustomError::StrategyDoNothing()) + } + } + + async fn handle_progress_strategy( + &self, + meme_token: Address, + bot_meme_balance: U256, + current_pair_wbnb_balance: U256, + strategy: &str, + ) -> Result<(), CustomError> { + if let Some(time_since_migration) = get_diff_time_since_migration(&*self.db, meme_token) + .await + .unwrap_or_default() + { + if time_since_migration < ChronoDuration::minutes(2) { return Err(CustomError::StrategyDoNothing()); } - s if s == StrategyType::Progress.to_string() => { - // we don't trade within 2 minutes after the token migration - if let Some(time_since_migration) = get_diff_time_since_migration(&*self.db, meme_token) - .await - .unwrap_or_default() - { - if time_since_migration < ChronoDuration::minutes(2) { - return Err(CustomError::StrategyDoNothing()); - } - } + } - if current_pair_wbnb_balance < U256::from(self.env_config.pancake_sl_wbnb_amount) { - let mut trader = self.outer_pool_trader.lock().await; - trader - .sell_token( - strategy.clone(), - &self.bloxroute_service, - self.env_config.tx_submit_type, - meme_token, - WBNB_CONTRACT, - bot_meme_balance, - self.env_config.slippage, - ) - .await - .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; + if current_pair_wbnb_balance < U256::from(self.env_config.pancake_sl_wbnb_amount) { + self.sell_all_tokens( + meme_token, + bot_meme_balance, + strategy, + "(-∞, pancake_sl_wbnb_amount): sell all", + ) + .await + } else { + let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&*self.db, meme_token) + .await + .unwrap_or_default(); + let five_percentage = initial_fourmeme_buy_amount / U256::from(20); - Logger.event(&format!( - "Auto sell token {}: (-∞, pancake_sl_wbnb_amount): sell all", - meme_token - )); - return Ok(()); + if bot_meme_balance < five_percentage { + if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { + return Err(CustomError::StrategyDoNothing()); } else { - let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&*self.db, meme_token) - .await - .unwrap_or_default(); - let five_percentage = initial_fourmeme_buy_amount / U256::from(20); - - if bot_meme_balance < five_percentage { - if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { - return Err(CustomError::StrategyDoNothing()); - } else { - let mut trader = self.outer_pool_trader.lock().await; - trader - .sell_token( - strategy.clone(), - &self.bloxroute_service, - self.env_config.tx_submit_type, - meme_token, - WBNB_CONTRACT, - bot_meme_balance, - self.env_config.slippage, - ) - .await - .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; - - Logger.event(&format!("Auto sell token {}: >= 100 WBNB: sell all", meme_token)); - return Ok(()); - } - } - - let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(50); - let mut trader = self.outer_pool_trader.lock().await; - trader - .sell_token( - strategy.clone(), - &self.bloxroute_service, - self.env_config.tx_submit_type, - meme_token, - WBNB_CONTRACT, - meme_amount_to_sell, - self.env_config.slippage, - ) - .await - .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; - - Logger.event(&format!("Auto sell token {}: (X, +∞): sell 2%", meme_token)); - return Ok(()); + return self + .sell_all_tokens(meme_token, bot_meme_balance, strategy, ">= 100 WBNB: sell all") + .await; } } - _ => { - return Err(CustomError::StrategyDoNothing()); + + let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(50); + self.sell_partial_tokens(meme_token, meme_amount_to_sell, strategy, "(X, +∞): sell 2%") + .await + } + } + + /// Helper function: check if we have passed the time threshold + async fn has_exceeded_time_threshold(&self, latest_trade_info: &Option) -> bool { + let now = Utc::now(); + match latest_trade_info { + None => true, + Some(trade) => { + let duration = now.signed_duration_since(trade.updated_at); + duration.num_seconds() >= 60 } } } + + /// Helper function: get the bot's meme balance + async fn get_bot_meme_balance(&self, meme_token: Address) -> Result { + let erc20_contract = IERC20Instance::new(meme_token, self.http_provider.clone()); + erc20_contract + .balanceOf(self.env_config.bot_address) + .call() + .await + .map_err(|e| CustomError::TxExecutionFailed(e.to_string())) + } + + /// Helper function: sell all the tokens + async fn sell_all_tokens( + &self, + meme_token: Address, + bot_meme_balance: U256, + strategy: &str, + log_message: &str, + ) -> Result<(), CustomError> { + let mut trader = self.outer_pool_trader.lock().await; + trader + .sell_token( + strategy.to_string(), + &self.bloxroute_service, + self.env_config.tx_submit_type, + meme_token, + WBNB_CONTRACT, + bot_meme_balance, + self.env_config.slippage, + ) + .await + .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; + + Logger.event(&format!("Auto sell token {}: {}", meme_token, log_message)); + Ok(()) + } + + /// Helper function: sell partial tokens + async fn sell_partial_tokens( + &self, + meme_token: Address, + amount_to_sell: U256, + strategy: &str, + log_message: &str, + ) -> Result<(), CustomError> { + let mut trader = self.outer_pool_trader.lock().await; + trader + .sell_token( + strategy.to_string(), + &self.bloxroute_service, + self.env_config.tx_submit_type, + meme_token, + WBNB_CONTRACT, + amount_to_sell, + self.env_config.slippage, + ) + .await + .map_err(|e| CustomError::TxExecutionFailed(e.to_string()))?; + + Logger.event(&format!("Auto sell token {}: {}", meme_token, log_message)); + Ok(()) + } } diff --git a/src/pvp/start.rs b/src/pvp/start.rs index 4ad2734..ff076f4 100644 --- a/src/pvp/start.rs +++ b/src/pvp/start.rs @@ -114,7 +114,14 @@ fn print_config(env_config: &EnvConfig) { ("TX Simulation".to_string(), env_config.tx_simulation.to_string()), ("Log Level".to_string(), format!("{:?}", env_config.log_level)), // -------- rpc config - ("WSS RPC URL".to_string(), env_config.wss_rpc_url.clone()), + ( + "WSS RPC URL Fourmeme".to_string(), + env_config.wss_rpc_url_fourmeme.clone(), + ), + ( + "WSS RPC URL PancakeSwap".to_string(), + env_config.wss_rpc_url_pancake.clone(), + ), ("HTTP RPC URL".to_string(), env_config.http_rpc_url.clone()), ("HTTP Send TX URL".to_string(), env_config.http_send_tx_url.clone()), // -------- database config @@ -209,10 +216,15 @@ pub async fn run() -> eyre::Result<()> { // 4. Setup providers let wallet = alloy::network::EthereumWallet::from(signer.clone()); - let wss_provider = ProviderBuilder::new() + let wss_provider_fourmeme = ProviderBuilder::new() .wallet(wallet.clone()) - .connect_ws(WsConnect::new(&env_config.wss_rpc_url)) + .connect_ws(WsConnect::new(&env_config.wss_rpc_url_fourmeme)) .await?; + let wss_provider_pancake = ProviderBuilder::new() + .wallet(wallet.clone()) + .connect_ws(WsConnect::new(&env_config.wss_rpc_url_pancake)) + .await?; + let http_provider = ProviderBuilder::new() .wallet(wallet.clone()) .connect_http(env_config.http_rpc_url.parse()?); @@ -228,8 +240,11 @@ pub async fn run() -> eyre::Result<()> { )); // 6. Create contract instances - let fourmeme_contract = Arc::new(FourmemeInstance::new(FOURMEME_CONTRACT, wss_provider.clone())); - let pancake_router_contract = Arc::new(PancakeSwapRouterInstance::new(PANCAKESWAP_ROUTER, wss_provider.clone())); + let fourmeme_contract = Arc::new(FourmemeInstance::new(FOURMEME_CONTRACT, wss_provider_fourmeme.clone())); + let pancake_router_contract = Arc::new(PancakeSwapRouterInstance::new( + PANCAKESWAP_ROUTER, + wss_provider_pancake.clone(), + )); // 7. Create nonce manager for Private transaction mode let nonce_manager: Option>> = if env_config.tx_submit_type == TxSubmitType::Private { @@ -303,11 +318,11 @@ pub async fn run() -> eyre::Result<()> { .from_block(BlockNumberOrTag::Latest); engine.add_collector(map_collector!( - LogCollector::new(Arc::new(wss_provider.clone()), fourmeme_filter), + LogCollector::new(Arc::new(wss_provider_fourmeme.clone()), fourmeme_filter), Event::FourmemeLog )); engine.add_collector(map_collector!( - LogCollector::new(Arc::new(wss_provider.clone()), pancake_filter), + LogCollector::new(Arc::new(wss_provider_pancake.clone()), pancake_filter), Event::PancakeSwapLog )); diff --git a/src/pvp/strategy/strategy_fourmeme.rs b/src/pvp/strategy/strategy_fourmeme.rs index 40bba1b..9207c1a 100644 --- a/src/pvp/strategy/strategy_fourmeme.rs +++ b/src/pvp/strategy/strategy_fourmeme.rs @@ -40,7 +40,6 @@ pub async fn on_fourmeme_token_migrated_event< ) -> Result<(), CustomError> { Logger.event(&format!("Token migrated event: {}", meme_address)); - // calculate the pair address using CREATE2, and add to tracking list let pair_address = calculate_pair_address( meme_address, WBNB_CONTRACT, @@ -48,59 +47,42 @@ pub async fn on_fourmeme_token_migrated_event< PANCAKESWAP_INIT_CODE_HASH, ); - // if the pair info is not in the database, then insert it - let pair_info = db - .get_migrated_token_info(BSC_CHAIN_ID, pair_address.to_string().as_str()) - .await - .map_err(|e| CustomError::Database(format!("Get migrated token info failed: {}", e).into()))?; - if pair_info.is_none() { - let reserve0_address = meme_address.min(WBNB_CONTRACT); - let reserve1_address = meme_address.max(WBNB_CONTRACT); - db.insert_migrated_token_info(&MigratedTokenInfo { - chain_id: BSC_CHAIN_ID, - reserve0_address: reserve0_address.to_string(), - reserve1_address: reserve1_address.to_string(), - pair_address: pair_address.to_string(), - updated_at: chrono::Utc::now(), - }) - .await - .map_err(|e| CustomError::Database(format!("Insert migrated token info failed: {}", e).into()))?; - } + ensure_pair_info_in_db(&db, pair_address, meme_address).await?; + // [STRATEGY TOKEN MIGRATED] + // if: + // - 2 minutes after the token is migrated + // - the pair's wbnb balance is greater than `meme_migrated_model_b_threshold` + // then buy the token let http_provider_clone = http_provider.clone(); let pancake_model_b_threshold = env_config.meme_migrated_model_b_threshold; let meme_migrated_model_b_buy_amount = env_config.meme_migrated_model_b_buy_amount; tokio::spawn(async move { Logger.event(&format!( - "Token migrated event: {}, sleep for 2 minutes to avoid RugPull", + "Token migrated event: {}, sleep for 2.5 minutes to avoid RugPull", meme_address )); - // sleep for 2.5 minutes to avoid RugPull tokio::time::sleep(Duration::from_secs(150)).await; - // [STRATEGY TOKEN MIGRATED] - // if: - // - 2 minutes after the token is migrated - // - the pair's wbnb balance is greater than `meme_migrated_model_b_threshold` - // then buy the token - let pair_wbnb_balance = get_pair_wbnb_balance(&http_provider_clone, meme_address).await.unwrap(); - Logger.event(&format!( - "Token migrated, pair wbnb balance: {}, meme address: {}", - pair_wbnb_balance, meme_address - )); - if pair_wbnb_balance > pancake_model_b_threshold { - Logger.event(&format!("Token migrated, buy token, meme address: {}", meme_address)); - let pending_action = PendingAction { - strategy: StrategyType::TokenMigrated.to_string(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: pair_wbnb_balance, - is_buy: true, - meme_address, - meme_amount: U256::from(0), - bnb_amount: meme_migrated_model_b_buy_amount, - more_info: format!("Migrated"), - }; - submitter.submit(pending_action); + if let Ok(pair_wbnb_balance) = get_pair_wbnb_balance(&http_provider_clone, meme_address).await { + Logger.event(&format!( + "Token migrated, pair wbnb balance: {}, meme address: {}", + pair_wbnb_balance, meme_address + )); + if pair_wbnb_balance > pancake_model_b_threshold { + Logger.event(&format!("Token migrated, buy token, meme address: {}", meme_address)); + let pending_action = PendingAction { + strategy: StrategyType::TokenMigrated.to_string(), + pool_type: PoolType::PancakeV2, + pancake_wbnb_balance: pair_wbnb_balance, + is_buy: true, + meme_address, + meme_amount: U256::from(0), + bnb_amount: meme_migrated_model_b_buy_amount, + more_info: "Migrated".to_string(), + }; + submitter.submit(pending_action); + } } }); @@ -121,7 +103,7 @@ pub async fn consider_selling + Clone + Send + Sync ) -> Result { let has_bought = check_token_has_bought(&*db, meme_address).await?; - if has_bought.is_some() { + if let Some(_) = has_bought { if bnb_amount < env_config.user_act_bnb_amount { return Err(CustomError::StrategyDoNothing()); } @@ -132,23 +114,20 @@ pub async fn consider_selling + Clone + Send + Sync if bot_meme_balance == U256::from(0) { return Err(CustomError::StrategyDoNothing()); } - let pending_action = PendingAction { - strategy: StrategyType::Bundle.to_string(), - pool_type: PoolType::Fourmeme, - pancake_wbnb_balance: U256::from(0), - is_buy: false, + + let more_info = format!("curve progress: {}", curve_progress); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {}, bot meme balance: {}, curve progress: {} - sell all", + StrategyType::Bundle.to_string(), meme_address, - meme_amount: bot_meme_balance, - bnb_amount: U256::from(0), - more_info: format!("curve progress: {}", curve_progress), - }; - Logger.process(&format!("Strategy {} considering selling: meme address: {}, bot meme balance: {}, curve progress: {} - sell all", StrategyType::Bundle.to_string(), meme_address, bot_meme_balance, curve_progress)); - return Ok(pending_action); + bot_meme_balance, + curve_progress + )); + return Ok(build_sell_pending_action(meme_address, bot_meme_balance, more_info)); } } if is_buy { - // we apply none strategy, then add market trading record to the database db.insert_market_trade_info(&MarketTradeInfo { chain_id: BSC_CHAIN_ID, block_number, @@ -162,10 +141,7 @@ pub async fn consider_selling + Clone + Send + Sync .map_err(|e| CustomError::Database(format!("Insert market trade info failed: {}", e).into()))?; } - Logger.item(&format!("No strategy applied, meme address: {}", meme_address)); - Logger.sub_item(&format!("Tx hash: {}", tx_hash)); - Logger.sub_item(&format!("Block number: {}", block_number)); - Logger.sub_item(&format!("Tx index: {}", tx_index)); + log_no_strategy_applied(meme_address, block_number, tx_index, &tx_hash); Err(CustomError::StrategyDoNothing()) } @@ -177,56 +153,37 @@ pub async fn consider_buying + Clone + Send + Sync meme_address: Address, block_number: u64, ) -> Result { - // if we have bought that token before, we should no longer buy it again let has_bought = check_token_has_bought(&*db, meme_address).await?; - if has_bought.is_some() { return Err(CustomError::StrategyDoNothing()); } - // [STRATEGY CURVE PROGRESS] - // if: - // - the curve progress is greater than `fourmeme_progress_curve_threshold` - // then buy the token let curve_progress = cal_curve_progress(meme_address, &http_provider).await?; if curve_progress > env_config.fourmeme_progress_buy_curve_threshold { let pair_wbnb_balance = get_pair_wbnb_balance(&http_provider, meme_address).await?; - let pending_action = PendingAction { - strategy: StrategyType::Progress.to_string(), - pool_type: PoolType::Fourmeme, - pancake_wbnb_balance: pair_wbnb_balance, - is_buy: true, + let more_info = format!("curve progress: {}", curve_progress); + return Ok(build_progress_buy_pending_action( meme_address, - meme_amount: U256::from(0), - bnb_amount: env_config.fourmeme_progress_buy_amount, - more_info: format!("curve progress: {}", curve_progress), - }; - return Ok(pending_action); + pair_wbnb_balance, + env_config.fourmeme_progress_buy_amount, + more_info, + )); } - // [STRATEGY BUNDLE] - // if: - // - there is a bundle transaction - // - the curve progress is greater than `fourmeme_bundle_threshold` - // then buy the token let bundle_info = get_bundle_info(&*db, meme_address, block_number, env_config.bundle_tx_count).await?; if let Some((bundle_sender, bundle_block_number)) = bundle_info { if curve_progress > env_config.fourmeme_bundle_threshold { let pair_wbnb_balance = get_pair_wbnb_balance(&http_provider, meme_address).await?; - let pending_action = PendingAction { - strategy: StrategyType::Bundle.to_string(), - pool_type: PoolType::Fourmeme, - pancake_wbnb_balance: pair_wbnb_balance, - is_buy: true, + let more_info = format!( + "bundle tx block: {}, bundle sender: {}", + bundle_block_number, bundle_sender + ); + return Ok(build_bundle_buy_pending_action( meme_address, - meme_amount: U256::from(0), - bnb_amount: env_config.fourmeme_bundle_buy_amount, - more_info: format!( - "bundle tx block: {}, bundle sender: {}", - bundle_block_number, bundle_sender - ), - }; - return Ok(pending_action); + pair_wbnb_balance, + env_config.fourmeme_bundle_buy_amount, + more_info, + )); } } @@ -235,3 +192,92 @@ pub async fn consider_buying + Clone + Send + Sync Err(CustomError::StrategyDoNothing()) } + +/// Helper function: ensure the pair info is in the database. +/// If the pair info is not in the database, insert it +async fn ensure_pair_info_in_db( + db: &Arc, + pair_address: Address, + meme_address: Address, +) -> Result<(), CustomError> { + let pair_info = db + .get_migrated_token_info(BSC_CHAIN_ID, pair_address.to_string().as_str()) + .await + .map_err(|e| CustomError::Database(format!("Get migrated token info failed: {}", e).into()))?; + + if pair_info.is_none() { + let reserve0_address = meme_address.min(WBNB_CONTRACT); + let reserve1_address = meme_address.max(WBNB_CONTRACT); + db.insert_migrated_token_info(&MigratedTokenInfo { + chain_id: BSC_CHAIN_ID, + reserve0_address: reserve0_address.to_string(), + reserve1_address: reserve1_address.to_string(), + pair_address: pair_address.to_string(), + updated_at: chrono::Utc::now(), + }) + .await + .map_err(|e| CustomError::Database(format!("Insert migrated token info failed: {}", e).into()))?; + } + + Ok(()) +} + +/// Helper function: build the sell pending action +fn build_sell_pending_action(meme_address: Address, meme_amount: U256, more_info: String) -> PendingAction { + PendingAction { + strategy: StrategyType::Bundle.to_string(), + pool_type: PoolType::Fourmeme, + pancake_wbnb_balance: U256::from(0), + is_buy: false, + meme_address, + meme_amount, + bnb_amount: U256::from(0), + more_info, + } +} + +/// Helper function: build the progress buy pending action +fn build_progress_buy_pending_action( + meme_address: Address, + pancake_wbnb_balance: U256, + bnb_amount: U256, + more_info: String, +) -> PendingAction { + PendingAction { + strategy: StrategyType::Progress.to_string(), + pool_type: PoolType::Fourmeme, + pancake_wbnb_balance, + is_buy: true, + meme_address, + meme_amount: U256::from(0), + bnb_amount, + more_info, + } +} + +/// Helper function: build the bundle buy pending action +fn build_bundle_buy_pending_action( + meme_address: Address, + pancake_wbnb_balance: U256, + bnb_amount: U256, + more_info: String, +) -> PendingAction { + PendingAction { + strategy: StrategyType::Bundle.to_string(), + pool_type: PoolType::Fourmeme, + pancake_wbnb_balance, + is_buy: true, + meme_address, + meme_amount: U256::from(0), + bnb_amount, + more_info, + } +} + +/// Helper function: log the no strategy applied +fn log_no_strategy_applied(meme_address: Address, block_number: u64, tx_index: u64, tx_hash: &str) { + Logger.item(&format!("No strategy applied, meme address: {}", meme_address)); + Logger.sub_item(&format!("Tx hash: {}", tx_hash)); + Logger.sub_item(&format!("Block number: {}", block_number)); + Logger.sub_item(&format!("Tx index: {}", tx_index)); +} diff --git a/src/pvp/strategy/strategy_pancake.rs b/src/pvp/strategy/strategy_pancake.rs index 32c3012..82e5cbf 100644 --- a/src/pvp/strategy/strategy_pancake.rs +++ b/src/pvp/strategy/strategy_pancake.rs @@ -36,210 +36,234 @@ pub async fn consider_selling + Clone + Send + Sync block_number: u64, tx_index: u64, ) -> Result { - // if we have bought that token before, we should no longer buy it again let has_bought = check_token_has_bought(&*db, meme_address).await?; - if has_bought.is_some() { - // check if the bnb amount is greater than the user act bnb amount + if let Some(strategy) = has_bought { if bnb_amount < env_config.user_act_bnb_amount { return Err(CustomError::StrategyDoNothing()); } - // check if the internal time interval has passed let is_pass_internal = is_pass_internal(&*db, meme_address, env_config.internal_each_trade).await?; if !is_pass_internal { return Err(CustomError::StrategyDoNothing()); } - // get the bot's meme balance let bot_meme_balance = get_bot_meme_balance(&http_provider, meme_address, env_config.bot_address).await?; if bot_meme_balance == U256::from(0) { return Err(CustomError::StrategyDoNothing()); } - // get the current pair's wbnb balance let current_pair_wbnb_balance = get_pair_wbnb_balance(&http_provider, meme_address).await?; - let strategy = has_bought.clone().unwrap_or_default(); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {}, bot meme balance: {}, current pair wbnb balance: {}", + strategy, meme_address, bot_meme_balance, current_pair_wbnb_balance + )); match strategy.as_str() { s if s == StrategyType::Bundle.to_string() || s == StrategyType::TokenMigrated.to_string() => { - // [STRATEGY BUNDLE OR TOKEN MIGRATED] - // - (-∞, X-3): sell all - // - (X, +∞): sell 1% of initial bought amount each trade; do nothing if only left 5% of the initial bought - // amount, sell all when the pair's wbnb balance is reached 100 WBNB - Logger.process(&format!("Strategy {} considering selling: meme address: {}, bot meme balance: {}, current pair wbnb balance: {}", strategy, meme_address, bot_meme_balance, current_pair_wbnb_balance)); + return handle_bundle_or_migrated_sell( + &db, + meme_address, + bot_meme_balance, + current_pair_wbnb_balance, + &strategy, + ) + .await; + } + s if s == StrategyType::Progress.to_string() => { + return handle_progress_sell( + &db, + &env_config, + meme_address, + bot_meme_balance, + current_pair_wbnb_balance, + &strategy, + ) + .await; + } + _ => {} + } + } + + if is_buy { + (&*db) + .insert_market_trade_info(&MarketTradeInfo { + chain_id: BSC_CHAIN_ID, + block_number, + tx_index, + tx_hash: tx_hash.clone(), + token_address: meme_address.to_string(), + updated_at: chrono::Utc::now(), + tx_sender: tx_sender.to_string(), + }) + .await + .map_err(|e| CustomError::Database(format!("Insert market trade info failed: {}", e).into()))?; + } + + log_no_strategy_applied(meme_address, block_number, tx_index, &tx_hash); - let wbnb_amount_when_bought = get_wbnb_amount_when_bought_token(&*db, meme_address).await?; + Err(CustomError::StrategyDoNothing()) +} - if current_pair_wbnb_balance < wbnb_amount_when_bought - U256::from(3) * ONE_ETHER { - let pending_action = PendingAction { - strategy: strategy.clone(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: current_pair_wbnb_balance, - is_buy: false, - meme_address, - meme_amount: bot_meme_balance, - bnb_amount: U256::from(0), - more_info: format!("{} (-∞, X-3): sell all", strategy), - }; - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} - (-∞, X-3): sell all", - strategy, meme_address - )); - return Ok(pending_action); - } else if current_pair_wbnb_balance > wbnb_amount_when_bought { - let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&*db, meme_address).await?; - let five_percentage = initial_fourmeme_buy_amount / U256::from(20); +async fn handle_bundle_or_migrated_sell( + db: &Arc, + meme_address: Address, + bot_meme_balance: U256, + current_pair_wbnb_balance: U256, + strategy: &str, +) -> Result { + let wbnb_amount_when_bought = get_wbnb_amount_when_bought_token(&**db, meme_address).await?; - if bot_meme_balance < five_percentage { - if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} < 100 WBNB: do nothing", - strategy, meme_address - )); - return Err(CustomError::StrategyDoNothing()); - } else { - let pending_action = PendingAction { - strategy: strategy.clone(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: current_pair_wbnb_balance, - is_buy: false, - meme_address, - meme_amount: bot_meme_balance, - bnb_amount: U256::from(0), - more_info: format!("{}: pair reaches 100 WBNB, sell all", strategy), - }; - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} >= 100 WBNB: sell all", - strategy, meme_address - )); - return Ok(pending_action); - } - } + if current_pair_wbnb_balance < wbnb_amount_when_bought - U256::from(3) * ONE_ETHER { + let more_info = format!("{} (-∞, X-3): sell all", strategy); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - (-∞, X-3): sell all", + strategy, meme_address + )); + return Ok(build_sell_pending_action( + strategy, + current_pair_wbnb_balance, + meme_address, + bot_meme_balance, + more_info, + )); + } else if current_pair_wbnb_balance > wbnb_amount_when_bought { + let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&**db, meme_address).await?; + let five_percentage = initial_fourmeme_buy_amount / U256::from(20); - let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(100); - let pending_action = PendingAction { - strategy: strategy.clone(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: current_pair_wbnb_balance, - is_buy: false, - meme_address, - meme_amount: meme_amount_to_sell, - bnb_amount: U256::from(0), - more_info: format!("{} (X, +∞): sell 1%", strategy), - }; - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} - (X, +∞): sell 1%", - strategy, meme_address - )); - return Ok(pending_action); - } + if bot_meme_balance < five_percentage { + if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} < 100 WBNB: do nothing", + strategy, meme_address + )); + return Err(CustomError::StrategyDoNothing()); + } else { + let more_info = format!("{}: pair reaches 100 WBNB, sell all", strategy); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} >= 100 WBNB: sell all", + strategy, meme_address + )); + return Ok(build_sell_pending_action( + strategy, + current_pair_wbnb_balance, + meme_address, + bot_meme_balance, + more_info, + )); } - s if s == StrategyType::Progress.to_string() => { - // [STRATEGY PROGRESS] - // - (-∞, pancake_sl_wbnb_amount): sell all - // - (pancake_sl_wbnb_amount, +∞): sell 1% of initial bought amount each trade; do nothing if - // only left 5% of the initial bought amount, sell all when the pair's wbnb balance is reached 100 WBNB - Logger.process(&format!("Strategy {} considering selling: meme address: {}, bot meme balance: {}, current pair wbnb balance: {}", strategy, meme_address, bot_meme_balance, current_pair_wbnb_balance)); + } - // we don't trade within 2 minutes after the token migration - if let Some(time_since_migration) = get_diff_time_since_migration(&*db, meme_address).await? { - if time_since_migration < Duration::minutes(2) { - Logger.process(&format!("Strategy {} considering selling: meme address: {} - within 2 minutes after the token migration: do nothing", strategy, meme_address)); - return Err(CustomError::StrategyDoNothing()); - } - } + let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(100); + let more_info = format!("{} (X, +∞): sell 1%", strategy); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - (X, +∞): sell 1%", + strategy, meme_address + )); + return Ok(build_sell_pending_action( + strategy, + current_pair_wbnb_balance, + meme_address, + meme_amount_to_sell, + more_info, + )); + } + + Err(CustomError::StrategyDoNothing()) +} - if current_pair_wbnb_balance < U256::from(env_config.pancake_sl_wbnb_amount) { - let pending_action = PendingAction { - strategy: StrategyType::Progress.to_string(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: current_pair_wbnb_balance, - is_buy: false, - meme_address, - meme_amount: bot_meme_balance, - bnb_amount: U256::from(0), - more_info: format!("{}: sell all", strategy), - }; - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} - (-∞, pancake_sl_wbnb_amount): sell all", - strategy, meme_address - )); - return Ok(pending_action); - } else { - let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&*db, meme_address).await?; - let five_percentage = initial_fourmeme_buy_amount / U256::from(20); +async fn handle_progress_sell( + db: &Arc, + env_config: &Arc, + meme_address: Address, + bot_meme_balance: U256, + current_pair_wbnb_balance: U256, + strategy: &str, +) -> Result { + if let Some(time_since_migration) = get_diff_time_since_migration(&**db, meme_address).await? { + if time_since_migration < Duration::minutes(2) { + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - within 2 minutes after the token migration: do nothing", + strategy, meme_address + )); + return Err(CustomError::StrategyDoNothing()); + } + } - if bot_meme_balance < five_percentage { - if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} - < 100 WBNB: do nothing", - strategy, meme_address - )); - return Err(CustomError::StrategyDoNothing()); - } else { - let pending_action = PendingAction { - strategy: strategy.clone(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: current_pair_wbnb_balance, - is_buy: false, - meme_address, - meme_amount: bot_meme_balance, - bnb_amount: U256::from(0), - more_info: format!("{}: pair reaches 100 WBNB, sell all", strategy), - }; - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} - >= 100 WBNB: sell all", - strategy, meme_address - )); - return Ok(pending_action); - } - } + if current_pair_wbnb_balance < U256::from(env_config.pancake_sl_wbnb_amount) { + let more_info = format!("{}: sell all", strategy); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - (-∞, pancake_sl_wbnb_amount): sell all", + strategy, meme_address + )); + return Ok(build_sell_pending_action( + strategy, + current_pair_wbnb_balance, + meme_address, + bot_meme_balance, + more_info, + )); + } else { + let initial_fourmeme_buy_amount = get_initial_fourmeme_buy_amount(&**db, meme_address).await?; + let five_percentage = initial_fourmeme_buy_amount / U256::from(20); - let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(100); - let pending_action = PendingAction { - strategy: strategy.clone(), - pool_type: PoolType::PancakeV2, - pancake_wbnb_balance: current_pair_wbnb_balance, - is_buy: false, - meme_address, - meme_amount: meme_amount_to_sell, - bnb_amount: U256::from(0), - more_info: format!("{} (X, +∞): sell 1%", strategy), - }; - Logger.process(&format!( - "Strategy {} considering selling: meme address: {} - (X, +∞): sell 1%", - strategy, meme_address - )); - return Ok(pending_action); - } + if bot_meme_balance < five_percentage { + if current_pair_wbnb_balance < ONE_ETHER * U256::from(100) { + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - < 100 WBNB: do nothing", + strategy, meme_address + )); + return Err(CustomError::StrategyDoNothing()); + } else { + let more_info = format!("{}: pair reaches 100 WBNB, sell all", strategy); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - >= 100 WBNB: sell all", + strategy, meme_address + )); + return Ok(build_sell_pending_action( + strategy, + current_pair_wbnb_balance, + meme_address, + bot_meme_balance, + more_info, + )); } - _ => {} } - } - if is_buy { - // we apply none strategy, then add market trading record to the database - db.insert_market_trade_info(&MarketTradeInfo { - chain_id: BSC_CHAIN_ID, - block_number, - tx_index, - tx_hash: tx_hash.clone(), - token_address: meme_address.to_string(), - updated_at: chrono::Utc::now(), - tx_sender: tx_sender.to_string(), - }) - .await - .map_err(|e| CustomError::Database(format!("Insert market trade info failed: {}", e).into()))?; + let meme_amount_to_sell = initial_fourmeme_buy_amount / U256::from(100); + let more_info = format!("{} (X, +∞): sell 1%", strategy); + Logger.process(&format!( + "Strategy {} considering selling: meme address: {} - (X, +∞): sell 1%", + strategy, meme_address + )); + return Ok(build_sell_pending_action( + strategy, + current_pair_wbnb_balance, + meme_address, + meme_amount_to_sell, + more_info, + )); } +} - Logger.item(&format!("No strategy applied, meme address: {}", meme_address)); - Logger.sub_item(&format!("Block number: {}", block_number)); - Logger.sub_item(&format!("Tx index: {}", tx_index)); - Logger.sub_item(&format!("Tx hash: {}", tx_hash)); - - Err(CustomError::StrategyDoNothing()) +fn build_sell_pending_action( + strategy: &str, + pancake_wbnb_balance: U256, + meme_address: Address, + meme_amount: U256, + more_info: String, +) -> PendingAction { + PendingAction { + strategy: strategy.to_string(), + pool_type: PoolType::PancakeV2, + pancake_wbnb_balance, + is_buy: false, + meme_address, + meme_amount, + bnb_amount: U256::from(0), + more_info, + } } pub async fn consider_buying + Clone + Send + Sync + 'static, N: Network>( @@ -249,18 +273,12 @@ pub async fn consider_buying + Clone + Send + Sync meme_address: Address, block_number: u64, ) -> Result { - // if we have bought that token before, we should no longer buy it again let has_bought = check_token_has_bought(&*db, meme_address).await?; if has_bought.is_some() { return Err(CustomError::StrategyDoNothing()); } - // [STRATEGY BUNDLE] - // if: - // - there is a bundle transaction - // - the pair's wbnb balance is greater than `pancake_bundle_threshold` - // then buy the token let bundle_info = get_bundle_info(&*db, meme_address, block_number, env_config.bundle_tx_count).await?; if let Some((bundle_sender, bundle_block_number)) = bundle_info { let pair_wbnb_balance = get_pair_wbnb_balance(&http_provider, meme_address).await?; @@ -268,7 +286,11 @@ pub async fn consider_buying + Clone + Send + Sync return Err(CustomError::StrategyDoNothing()); } - let pending_action = PendingAction { + let more_info = format!( + "bundle tx block: {}, bundle sender: {}", + bundle_block_number, bundle_sender + ); + return Ok(PendingAction { strategy: StrategyType::Bundle.to_string(), pool_type: PoolType::PancakeV2, pancake_wbnb_balance: pair_wbnb_balance, @@ -276,12 +298,8 @@ pub async fn consider_buying + Clone + Send + Sync meme_address, meme_amount: U256::from(0), bnb_amount: env_config.pancake_bundle_buy_amount, - more_info: format!( - "bundle tx block: {}, bundle sender: {}", - bundle_block_number, bundle_sender - ), - }; - return Ok(pending_action); + more_info, + }); } Logger.item(&format!("No strategy applied, meme address: {}", meme_address)); @@ -289,3 +307,10 @@ pub async fn consider_buying + Clone + Send + Sync Err(CustomError::StrategyDoNothing()) } + +fn log_no_strategy_applied(meme_address: Address, block_number: u64, tx_index: u64, tx_hash: &str) { + Logger.item(&format!("No strategy applied, meme address: {}", meme_address)); + Logger.sub_item(&format!("Block number: {}", block_number)); + Logger.sub_item(&format!("Tx index: {}", tx_index)); + Logger.sub_item(&format!("Tx hash: {}", tx_hash)); +}