@@ -47,10 +47,10 @@ use crate::rpc_command::init_scan_for_new_addresses::{InitScanAddressesRpcOps, S
4747use crate :: rpc_command:: init_withdraw:: { InitWithdrawCoin , WithdrawTaskHandleShared } ;
4848use crate :: rpc_command:: { account_balance, get_new_address, init_account_balance, init_create_account,
4949 init_scan_for_new_addresses} ;
50- use crate :: { coin_balance, scan_for_new_addresses_impl , BalanceResult , CoinWithDerivationMethod , DerivationMethod ,
51- DexFee , Eip1559Ops , MakerNftSwapOpsV2 , ParseCoinAssocTypes , ParseNftAssocTypes , PayForGasParams ,
52- PrivKeyPolicy , RpcCommonOps , SendNftMakerPaymentArgs , SpendNftMakerPaymentArgs , ToBytes ,
53- ValidateNftMakerPaymentArgs , ValidateWatcherSpendInput , WatcherSpendType } ;
50+ use crate :: { coin_balance, BalanceResult , CoinWithDerivationMethod , DerivationMethod , DexFee , Eip1559Ops ,
51+ MakerNftSwapOpsV2 , ParseCoinAssocTypes , ParseNftAssocTypes , PayForGasParams , PrivKeyPolicy , RpcCommonOps ,
52+ SendNftMakerPaymentArgs , SpendNftMakerPaymentArgs , ToBytes , ValidateNftMakerPaymentArgs ,
53+ ValidateWatcherSpendInput , WatcherSpendType } ;
5454use async_trait:: async_trait;
5555use bitcrypto:: { dhash160, keccak256, ripemd160, sha256} ;
5656use common:: custom_futures:: repeatable:: { Ready , Retry , RetryOnError } ;
@@ -62,7 +62,7 @@ use common::number_type_casting::SafeTypeCastingNumbers;
6262use common:: wait_until_sec;
6363use common:: { now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY } ;
6464use crypto:: privkey:: key_pair_from_secret;
65- use crypto:: { Bip44Chain , CryptoCtx , CryptoCtxError , GlobalHDAccountArc , KeyPairPolicy } ;
65+ use crypto:: { Bip44Chain , CryptoCtx , CryptoCtxError , GlobalHDAccountArc , KeyPairPolicy , StandardHDPath } ;
6666use derive_more:: Display ;
6767use enum_derives:: EnumFromStringify ;
6868
@@ -899,6 +899,10 @@ pub struct EthCoinImpl {
899899 pub ( crate ) gas_limit : EthGasLimit ,
900900 /// Config provided gas limits v2 for swap v2 transactions
901901 pub ( crate ) gas_limit_v2 : EthGasLimitV2 ,
902+ /// A local cache for transactions sent from this wallet. Only kept in memory for a running KDF instance.
903+ /// This allows replacing a transaction even if it was sent through a private node
904+ /// and is not yet publicly visible.
905+ local_tx_cache : Arc < AsyncMutex < HashMap < H256 , BytesJson > > > ,
902906 /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation
903907 /// and on [`MmArc::stop`].
904908 pub abortable_system : AbortableQueue ,
@@ -5848,6 +5852,200 @@ impl MmCoin for EthCoin {
58485852 Box :: new ( get_tx_hex_by_hash_impl ( self . clone ( ) , tx_hash) . boxed ( ) . compat ( ) )
58495853 }
58505854
5855+ async fn replace_transaction (
5856+ & self ,
5857+ tx_hash : String ,
5858+ fee : WithdrawFee ,
5859+ action : ReplacementAction ,
5860+ broadcast : bool ,
5861+ ) -> Result < TransactionDetails , MmError < ReplaceTxError > > {
5862+ // 1. Try to fetch the transaction from the local cache first.
5863+ // This allows replacing transactions sent via a private node
5864+ // and are not yet publicly visible.
5865+ let tx_hash_h256 = H256 :: from_str ( & tx_hash) . map_to_mm ( |e| ReplaceTxError :: InvalidTxHash ( e. to_string ( ) ) ) ?;
5866+ let tx_hex_from_cache = self . local_tx_cache . lock ( ) . await . get ( & tx_hash_h256) . cloned ( ) ;
5867+
5868+ let original_tx_raw = if let Some ( tx_hex) = tx_hex_from_cache {
5869+ RawTransactionRes { tx_hex }
5870+ } else {
5871+ // 2. If not in the cache, fall back to fetching from the RPC node.
5872+ self . get_raw_transaction ( RawTransactionRequest {
5873+ coin : self . ticker ( ) . to_string ( ) ,
5874+ tx_hash : tx_hash. clone ( ) ,
5875+ } )
5876+ . compat ( )
5877+ . await
5878+ . map_err ( |e| ReplaceTxError :: TxNotFound ( e. to_string ( ) ) ) ?
5879+ } ;
5880+
5881+ let original_tx: UnverifiedTransactionWrapper =
5882+ rlp:: decode ( & original_tx_raw. tx_hex . 0 ) . map_to_mm ( |e| ReplaceTxError :: TxDecodeError ( e. to_string ( ) ) ) ?;
5883+
5884+ let signed_original_tx =
5885+ SignedEthTx :: new ( original_tx. clone ( ) ) . map_to_mm ( |e| ReplaceTxError :: TxDecodeError ( e. to_string ( ) ) ) ?;
5886+
5887+ let from_addr = signed_original_tx. sender ( ) ;
5888+ let nonce_to_replace = original_tx. unsigned ( ) . nonce ( ) ;
5889+
5890+ let from = match self . derivation_method ( ) {
5891+ DerivationMethod :: SingleAddress ( my_address) => {
5892+ if * my_address != from_addr {
5893+ return MmError :: err ( ReplaceTxError :: TxNotSentFromAddress {
5894+ address : from_addr. display_address ( ) ,
5895+ } ) ;
5896+ }
5897+ None
5898+ } ,
5899+ DerivationMethod :: HDWallet ( hd_wallet) => {
5900+ let hd_address = self
5901+ . find_wallet_address ( hd_wallet, & from_addr)
5902+ . await
5903+ . map_err ( |e| ReplaceTxError :: InternalError ( e. to_string ( ) ) ) ?
5904+ . ok_or_else ( || {
5905+ MmError :: new ( ReplaceTxError :: TxNotSentFromAddress {
5906+ address : from_addr. display_address ( ) ,
5907+ } )
5908+ } ) ?;
5909+
5910+ let standard_path =
5911+ StandardHDPath :: from_str ( & hd_address. derivation_path ( ) . to_string ( ) ) . map_to_mm ( |e| {
5912+ ReplaceTxError :: InternalError ( format ! (
5913+ "Invalid HD path for the original transaction's sender address: {:?}" ,
5914+ e,
5915+ ) )
5916+ } ) ?;
5917+
5918+ Some ( HDAddressSelector :: AddressId ( standard_path. into ( ) ) )
5919+ } ,
5920+ } ;
5921+
5922+ // Check if the transaction is a swap contract interaction and disallow replacement if so.
5923+ // This is crucial because the counterparty in a swap would not be aware of the new transaction hash.
5924+ // Note: This check is a temporary measure until we implement a proper replacement mechanism for swaps.
5925+ // Replacing spending transactions from swap contracts is allowed though.
5926+ if let Call ( to_addr) = original_tx. unsigned ( ) . action ( ) {
5927+ let is_swap_v1 = * to_addr == self . swap_contract_address || self . fallback_swap_contract == Some ( * to_addr) ;
5928+ let is_swap_v2 = self . swap_v2_contracts . map_or ( false , |c| {
5929+ c. maker_swap_v2_contract == * to_addr
5930+ || c. taker_swap_v2_contract == * to_addr
5931+ || c. nft_maker_swap_v2_contract == * to_addr
5932+ } ) ;
5933+
5934+ if is_swap_v1 || is_swap_v2 {
5935+ return MmError :: err ( ReplaceTxError :: NotSupported (
5936+ "Replacing transactions for swaps is not yet supported." . to_string ( ) ,
5937+ ) ) ;
5938+ }
5939+ }
5940+
5941+ // 3. Determine parameters for the new transaction.
5942+ let ( to_addr_str, amount, max) = match action {
5943+ ReplacementAction :: SpeedUp => {
5944+ let to_addr = match original_tx. unsigned ( ) . action ( ) {
5945+ Call ( addr) => addr,
5946+ Create => {
5947+ return MmError :: err ( ReplaceTxError :: NotSupported (
5948+ "Speeding up contract creation is not yet supported." . to_string ( ) ,
5949+ ) )
5950+ } ,
5951+ } ;
5952+
5953+ match self . coin_type {
5954+ EthCoinType :: Eth => {
5955+ let original_amount_wei = original_tx. unsigned ( ) . value ( ) ;
5956+
5957+ // Try to estimate the new fee to see if we have enough balance.
5958+ let details_res = get_eth_gas_details_from_withdraw_fee (
5959+ self ,
5960+ Some ( fee. clone ( ) ) ,
5961+ original_amount_wei,
5962+ original_tx. unsigned ( ) . data ( ) . clone ( ) . into ( ) ,
5963+ from_addr,
5964+ * to_addr,
5965+ false , // We don't know if it's a max withdrawal yet
5966+ )
5967+ . await ;
5968+
5969+ match details_res {
5970+ Ok ( _) => {
5971+ // If successful, it means there's enough balance to cover the original amount and the new fee.
5972+ let amount_dec = u256_to_big_decimal ( original_amount_wei, self . decimals ( ) )
5973+ . mm_err ( |e| ReplaceTxError :: InternalError ( e. to_string ( ) ) ) ?;
5974+ ( to_addr. display_address ( ) , amount_dec, false )
5975+ } ,
5976+ Err ( e) => match e. into_inner ( ) {
5977+ // This error indicates we don't have enough funds for the original amount + new fee.
5978+ // This is a strong indicator that the original transaction was a "max" withdrawal.
5979+ // In this case, we switch to a max withdrawal for the replacement transaction.
5980+ EthGasDetailsErr :: AmountTooLow { .. } => {
5981+ ( to_addr. display_address ( ) , BigDecimal :: from ( 0 ) , true )
5982+ } ,
5983+ // For any other error, we propagate it.
5984+ other => {
5985+ let withdraw_error: WithdrawError = other. into ( ) ;
5986+ return Err ( withdraw_error. into ( ) ) ;
5987+ } ,
5988+ } ,
5989+ }
5990+ } ,
5991+ EthCoinType :: Erc20 { token_addr, .. } => {
5992+ if to_addr != & token_addr {
5993+ return MmError :: err ( ReplaceTxError :: NotSupported (
5994+ "Transaction does not belong to this ERC20 token." . to_string ( ) ,
5995+ ) ) ;
5996+ }
5997+ let function = ERC20_CONTRACT
5998+ . function ( "transfer" )
5999+ . map_to_mm ( |e| ReplaceTxError :: TxDecodeError ( e. to_string ( ) ) ) ?;
6000+ let tokens = function
6001+ . decode_input ( & original_tx. unsigned ( ) . data ( ) [ 4 ..] )
6002+ . map_to_mm ( |e| ReplaceTxError :: TxDecodeError ( e. to_string ( ) ) ) ?;
6003+
6004+ let recipient_addr =
6005+ tokens. first ( ) . and_then ( |t| t. clone ( ) . into_address ( ) ) . ok_or_else ( || {
6006+ MmError :: new ( ReplaceTxError :: TxDecodeError ( "Couldn't decode recipient" . into ( ) ) )
6007+ } ) ?;
6008+ let token_amount = tokens. get ( 1 ) . and_then ( |t| t. clone ( ) . into_uint ( ) ) . ok_or_else ( || {
6009+ MmError :: new ( ReplaceTxError :: TxDecodeError ( "Couldn't decode amount" . into ( ) ) )
6010+ } ) ?;
6011+
6012+ let amount_dec = u256_to_big_decimal ( token_amount, self . decimals ( ) )
6013+ . map_err ( |e| MmError :: new ( ReplaceTxError :: InternalError ( e. to_string ( ) ) ) ) ?;
6014+
6015+ ( recipient_addr. display_address ( ) , amount_dec, false )
6016+ } ,
6017+ // Todo: Handle NFT transactions
6018+ EthCoinType :: Nft { .. } => {
6019+ return MmError :: err ( ReplaceTxError :: NotSupported (
6020+ "Speeding up NFT transactions is not yet supported." . to_string ( ) ,
6021+ ) ) ;
6022+ } ,
6023+ }
6024+ } ,
6025+ ReplacementAction :: Cancel => ( from_addr. display_address ( ) , 0 . into ( ) , false ) ,
6026+ } ;
6027+
6028+ // 4. Create the WithdrawRequest.
6029+ let withdraw_req = WithdrawRequest {
6030+ coin : self . ticker ( ) . to_string ( ) ,
6031+ to : to_addr_str,
6032+ amount,
6033+ from,
6034+ max,
6035+ fee : Some ( fee) ,
6036+ memo : None ,
6037+ broadcast,
6038+ ibc_source_channel : None ,
6039+ } ;
6040+
6041+ // 5. Use the builder pattern to set the nonce and build the transaction.
6042+ let mut withdraw_flow = StandardEthWithdraw :: new ( self . clone ( ) , withdraw_req) . map_mm_err ( ) ?;
6043+ withdraw_flow. set_nonce_override ( nonce_to_replace) ;
6044+
6045+ let result = withdraw_flow. build ( ) . await . map_mm_err ( ) ?;
6046+ Ok ( result)
6047+ }
6048+
58516049 fn withdraw ( & self , req : WithdrawRequest ) -> WithdrawFut {
58526050 Box :: new ( Box :: pin ( withdraw_impl ( self . clone ( ) , req) ) . compat ( ) )
58536051 }
@@ -6673,6 +6871,7 @@ pub async fn eth_coin_from_conf_and_request(
66736871 nfts_infos : Default :: default ( ) ,
66746872 gas_limit,
66756873 gas_limit_v2,
6874+ local_tx_cache : Arc :: new ( AsyncMutex :: new ( HashMap :: new ( ) ) ) ,
66766875 abortable_system,
66776876 } ;
66786877
@@ -7540,6 +7739,7 @@ impl EthCoin {
75407739 nfts_infos : Arc :: clone ( & self . nfts_infos ) ,
75417740 gas_limit : EthGasLimit :: default ( ) ,
75427741 gas_limit_v2 : EthGasLimitV2 :: default ( ) ,
7742+ local_tx_cache : Arc :: new ( AsyncMutex :: new ( HashMap :: new ( ) ) ) ,
75437743 abortable_system : self . abortable_system . create_subsystem ( ) . unwrap ( ) ,
75447744 } ;
75457745 EthCoin ( Arc :: new ( coin) )
0 commit comments