diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 72cee7bfa3..884cdd5234 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -59,14 +59,14 @@ use super::{BalanceError, BalanceFut, CoinBalance, CoinProtocol, CoinTransportMe NumConversResult, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, Transaction, - TransactionDetails, TransactionEnum, TransactionFut, ValidateAddressResult, WithdrawError, WithdrawFee, - WithdrawFut, WithdrawRequest, WithdrawResult}; + TransactionDetails, TransactionEnum, ValidateAddressResult, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest, WithdrawResult}; pub use ethcore_transaction::SignedTransaction as SignedEthTx; pub use rlp; mod web3_transport; -use crate::ValidatePaymentInput; +use crate::{TransactionErr, TransactionFut, ValidatePaymentInput}; use common::mm_number::MmNumber; use common::privkey::key_pair_from_secret; use web3_transport::{EthFeeHistoryNamespace, Web3Transport}; @@ -685,10 +685,10 @@ impl Deref for EthCoin { #[async_trait] impl SwapOps for EthCoin { fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { - let address = try_fus!(addr_from_raw_pubkey(fee_addr)); + let address = try_tx_fus!(addr_from_raw_pubkey(fee_addr)); Box::new( - self.send_to_address(address, try_fus!(wei_from_big_decimal(&amount, self.decimals))) + self.send_to_address(address, try_tx_fus!(wei_from_big_decimal(&amount, self.decimals))) .map(TransactionEnum::from), ) } @@ -702,13 +702,13 @@ impl SwapOps for EthCoin { amount: BigDecimal, swap_contract_address: &Option, ) -> TransactionFut { - let taker_addr = try_fus!(addr_from_raw_pubkey(taker_pub)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let taker_addr = try_tx_fus!(addr_from_raw_pubkey(taker_pub)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( self.etomic_swap_id(time_lock, secret_hash), - try_fus!(wei_from_big_decimal(&amount, self.decimals)), + try_tx_fus!(wei_from_big_decimal(&amount, self.decimals)), time_lock, secret_hash, taker_addr, @@ -727,13 +727,13 @@ impl SwapOps for EthCoin { amount: BigDecimal, swap_contract_address: &Option, ) -> TransactionFut { - let maker_addr = try_fus!(addr_from_raw_pubkey(maker_pub)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let maker_addr = try_tx_fus!(addr_from_raw_pubkey(maker_pub)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.send_hash_time_locked_payment( self.etomic_swap_id(time_lock, secret_hash), - try_fus!(wei_from_big_decimal(&amount, self.decimals)), + try_tx_fus!(wei_from_big_decimal(&amount, self.decimals)), time_lock, secret_hash, maker_addr, @@ -752,9 +752,9 @@ impl SwapOps for EthCoin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(taker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address(), signed); Box::new( self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) @@ -771,9 +771,9 @@ impl SwapOps for EthCoin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(maker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.spend_hash_time_locked_payment(signed, swap_contract_address, secret) .map(TransactionEnum::from), @@ -789,9 +789,9 @@ impl SwapOps for EthCoin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(taker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.refund_hash_time_locked_payment(swap_contract_address, signed) @@ -808,9 +808,9 @@ impl SwapOps for EthCoin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let tx: UnverifiedTransaction = try_fus!(rlp::decode(maker_payment_tx)); - let signed = try_fus!(SignedEthTx::new(tx)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let signed = try_tx_fus!(SignedEthTx::new(tx)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); Box::new( self.refund_hash_time_locked_payment(swap_contract_address, signed) @@ -1138,6 +1138,16 @@ impl MarketCoinOps for EthCoin { ) } + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + Box::new( + self.web3 + .eth() + .send_raw_transaction(tx.into()) + .map(|res| format!("{:02x}", res)) + .map_err(|e| ERRL!("{}", e)), + ) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -1215,17 +1225,17 @@ impl MarketCoinOps for EthCoin { from_block: u64, swap_contract_address: &Option, ) -> TransactionFut { - let unverified: UnverifiedTransaction = try_fus!(rlp::decode(tx_bytes)); - let tx = try_fus!(SignedEthTx::new(unverified)); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let unverified: UnverifiedTransaction = try_tx_fus!(rlp::decode(tx_bytes)); + let tx = try_tx_fus!(SignedEthTx::new(unverified)); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let func_name = match self.coin_type { EthCoinType::Eth => "ethPayment", EthCoinType::Erc20 { .. } => "erc20Payment", }; - let payment_func = try_fus!(SWAP_CONTRACT.function(func_name)); - let decoded = try_fus!(payment_func.decode_input(&tx.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function(func_name)); + let decoded = try_tx_fus!(payment_func.decode_input(&tx.data)); let id = match &decoded[0] { Token::FixedBytes(bytes) => bytes.clone(), _ => panic!(), @@ -1280,15 +1290,15 @@ impl MarketCoinOps for EthCoin { }, }; - return Ok(TransactionEnum::from(try_s!(signed_tx_from_web3_tx(transaction)))); + return Ok(TransactionEnum::from(try_tx_s!(signed_tx_from_web3_tx(transaction)))); } } if now_ms() / 1000 > wait_until { - return ERR!( + return TX_PLAIN_ERR!( "Waited too long until {} for transaction {:?} to be spent ", wait_until, - tx + tx, ); } Timer::sleep(5.).await; @@ -1336,7 +1346,7 @@ lazy_static! { static ref NONCE_LOCK: TimedAsyncMutex<()> = TimedAsyncMutex::new(()); } -type EthTxFut = Box + Send + 'static>; +type EthTxFut = Box + Send + 'static>; async fn sign_and_send_transaction_impl( ctx: MmArc, @@ -1345,7 +1355,7 @@ async fn sign_and_send_transaction_impl( action: Action, data: Vec, gas: U256, -) -> Result { +) -> Result { let mut status = ctx.log.status_handle(); macro_rules! tags { () => { @@ -1364,13 +1374,13 @@ async fn sign_and_send_transaction_impl( }) .await; status.status(tags!(), "get_addr_nonce…"); - let nonce = try_s!( + let nonce = try_tx_s!( get_addr_nonce(coin.my_address, coin.web3_instances.clone()) .compat() .await ); status.status(tags!(), "get_gas_price…"); - let gas_price = try_s!(coin.get_gas_price().compat().await); + let gas_price = try_tx_s!(coin.get_gas_price().compat().await); let tx = UnSignedEthTx { nonce, gas_price, @@ -1382,14 +1392,17 @@ async fn sign_and_send_transaction_impl( let signed = tx.sign(coin.key_pair.secret(), coin.chain_id); let bytes = web3::types::Bytes(rlp::encode(&signed).to_vec()); status.status(tags!(), "send_raw_transaction…"); - try_s!( + + try_tx_s!( coin.web3 .eth() .send_raw_transaction(bytes) .map_err(|e| ERRL!("{}", e)) .compat() - .await + .await, + signed ); + status.status(tags!(), "get_addr_nonce…"); loop { // Check every second till ETH nodes recognize that nonce is increased @@ -2165,7 +2178,7 @@ impl EthCoin { #[cfg_attr(test, mockable)] impl EthCoin { fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { - let ctx = try_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); + let ctx = try_tx_fus!(MmArc::from_weak(&self.ctx).ok_or("!ctx")); let fut = Box::pin(sign_and_send_transaction_impl( ctx, self.clone(), @@ -2184,9 +2197,9 @@ impl EthCoin { platform: _, token_addr, } => { - let abi = try_fus!(Contract::load(ERC20_ABI.as_bytes())); - let function = try_fus!(abi.function("transfer")); - let data = try_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); + let abi = try_tx_fus!(Contract::load(ERC20_ABI.as_bytes())); + let function = try_tx_fus!(abi.function("transfer")); + let data = try_tx_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); self.sign_and_send_transaction(0.into(), Action::Call(*token_addr), data, U256::from(210_000)) }, } @@ -2203,8 +2216,8 @@ impl EthCoin { ) -> EthTxFut { match &self.coin_type { EthCoinType::Eth => { - let function = try_fus!(SWAP_CONTRACT.function("ethPayment")); - let data = try_fus!(function.encode_input(&[ + let function = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let data = try_tx_fus!(function.encode_input(&[ Token::FixedBytes(id), Token::Address(receiver_addr), Token::FixedBytes(secret_hash.to_vec()), @@ -2216,10 +2229,12 @@ impl EthCoin { platform: _, token_addr, } => { - let allowance_fut = self.allowance(swap_contract_address).map_err(|e| ERRL!("{}", e)); + let allowance_fut = self + .allowance(swap_contract_address) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))); - let function = try_fus!(SWAP_CONTRACT.function("erc20Payment")); - let data = try_fus!(function.encode_input(&[ + let function = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let data = try_tx_fus!(function.encode_input(&[ Token::FixedBytes(id), Token::Uint(value), Token::Address(*token_addr), @@ -2261,144 +2276,160 @@ impl EthCoin { swap_contract_address: Address, secret: &[u8], ) -> EthTxFut { - let spend_func = try_fus!(SWAP_CONTRACT.function("receiverSpend")); + let spend_func = try_tx_fus!(SWAP_CONTRACT.function("receiverSpend")); let clone = self.clone(); let secret_vec = secret.to_vec(); match self.coin_type { EthCoinType::Eth => { - let payment_func = try_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } - let value = payment.value; - let data = try_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - Token::FixedBytes(secret_vec), - Token::Address(Address::default()), - Token::Address(payment.sender()), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + let value = payment.value; + let data = try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + Token::FixedBytes(secret_vec), + Token::Address(Address::default()), + Token::Address(payment.sender()), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, EthCoinType::Erc20 { platform: _, token_addr, } => { - let payment_func = try_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } - let data = try_fus!(spend_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - Token::FixedBytes(secret_vec), - Token::Address(token_addr), - Token::Address(payment.sender()), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } + let data = try_tx_fus!(spend_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + Token::FixedBytes(secret_vec), + Token::Address(token_addr), + Token::Address(payment.sender()), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, } } fn refund_hash_time_locked_payment(&self, swap_contract_address: Address, payment: SignedEthTx) -> EthTxFut { - let refund_func = try_fus!(SWAP_CONTRACT.function("senderRefund")); + let refund_func = try_tx_fus!(SWAP_CONTRACT.function("senderRefund")); let clone = self.clone(); match self.coin_type { EthCoinType::Eth => { - let payment_func = try_fus!(SWAP_CONTRACT.function("ethPayment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("ethPayment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } - let value = payment.value; - let data = try_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - Token::Uint(value), - decoded[2].clone(), - Token::Address(Address::default()), - decoded[1].clone(), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + let value = payment.value; + let data = try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + Token::Uint(value), + decoded[2].clone(), + Token::Address(Address::default()), + decoded[1].clone(), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, EthCoinType::Erc20 { platform: _, token_addr, } => { - let payment_func = try_fus!(SWAP_CONTRACT.function("erc20Payment")); - let decoded = try_fus!(payment_func.decode_input(&payment.data)); + let payment_func = try_tx_fus!(SWAP_CONTRACT.function("erc20Payment")); + let decoded = try_tx_fus!(payment_func.decode_input(&payment.data)); let state_f = self.payment_status(swap_contract_address, decoded[0].clone()); - Box::new(state_f.and_then(move |state| -> EthTxFut { - if state != PAYMENT_STATE_SENT.into() { - return Box::new(futures01::future::err(ERRL!( - "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", - payment, - state - ))); - } + Box::new( + state_f + .map_err(TransactionErr::Plain) + .and_then(move |state| -> EthTxFut { + if state != PAYMENT_STATE_SENT.into() { + return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + "Payment {:?} state is not PAYMENT_STATE_SENT, got {}", + payment, + state + )))); + } - let data = try_fus!(refund_func.encode_input(&[ - decoded[0].clone(), - decoded[1].clone(), - decoded[4].clone(), - Token::Address(token_addr), - decoded[3].clone(), - ])); - - clone.sign_and_send_transaction( - 0.into(), - Action::Call(swap_contract_address), - data, - U256::from(150_000), - ) - })) + let data = try_tx_fus!(refund_func.encode_input(&[ + decoded[0].clone(), + decoded[1].clone(), + decoded[4].clone(), + Token::Address(token_addr), + decoded[3].clone(), + ])); + + clone.sign_and_send_transaction( + 0.into(), + Action::Call(swap_contract_address), + data, + U256::from(150_000), + ) + }), + ) }, } } @@ -2518,13 +2549,13 @@ impl EthCoin { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { - EthCoinType::Eth => return ERR!("'approve' is expected to be call for ERC20 coins only"), + EthCoinType::Eth => return TX_PLAIN_ERR!("'approve' is expected to be call for ERC20 coins only"), EthCoinType::Erc20 { token_addr, .. } => token_addr, }; - let function = try_s!(ERC20_CONTRACT.function("approve")); - let data = try_s!(function.encode_input(&[Token::Address(spender), Token::Uint(amount)])); + let function = try_tx_s!(ERC20_CONTRACT.function("approve")); + let data = try_tx_s!(function.encode_input(&[Token::Address(spender), Token::Uint(amount)])); - let gas_limit = try_s!( + let gas_limit = try_tx_s!( coin.estimate_gas_for_contract_call(token_addr, Bytes::from(data.clone())) .compat() .await @@ -2533,7 +2564,6 @@ impl EthCoin { coin.sign_and_send_transaction(0.into(), Action::Call(token_addr), data, gas_limit) .compat() .await - .map_err(|e| ERRL!("{}", e)) }; Box::new(fut.boxed().compat()) } diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index 13a7cf54ae..a7a8324e1e 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -412,6 +412,15 @@ impl MarketCoinOps for LightningCoin { )) } + fn send_raw_tx_bytes(&self, _tx: &[u8]) -> Box + Send> { + Box::new(futures01::future::err( + MmError::new( + "send_raw_tx is not supported for lightning, please use send_payment method instead.".to_string(), + ) + .to_string(), + )) + } + // Todo: Implement this when implementing swaps for lightning as it's is used mainly for swaps fn wait_for_confirmations( &self, diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index 3214c033c5..09d92b9c51 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -467,7 +467,7 @@ impl Platform { ) .compat() .await - .map_to_mm(SaveChannelClosingError::WaitForFundingTxSpendError)?; + .map_to_mm(|e| SaveChannelClosingError::WaitForFundingTxSpendError(e.get_plain_text_format()))?; let closing_tx_hash = format!("{:02x}", closing_tx.tx_hash()); diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 827b4c4bc3..a6809d6122 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -96,6 +96,72 @@ macro_rules! try_f { }; } +/// `TransactionErr` compatible `try_fus` macro. +macro_rules! try_tx_fus { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => return Box::new(futures01::future::err(crate::TransactionErr::Plain(ERRL!("{:?}", err)))), + } + }; + ($e: expr, $tx: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + return Box::new(futures01::future::err(crate::TransactionErr::TxRecoverable( + TransactionEnum::from($tx), + ERRL!("{:?}", err), + ))) + }, + } + }; +} + +/// `TransactionErr` compatible `try_s` macro. +macro_rules! try_tx_s { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + return Err(crate::TransactionErr::Plain(format!( + "{}:{}] {:?}", + file!(), + line!(), + err + ))) + }, + } + }; + ($e: expr, $tx: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + return Err(crate::TransactionErr::TxRecoverable( + TransactionEnum::from($tx), + format!("{}:{}] {:?}", file!(), line!(), err), + )) + }, + } + }; +} + +/// `TransactionErr:Plain` compatible `ERR` macro. +macro_rules! TX_PLAIN_ERR { + ($format: expr, $($args: tt)+) => { Err(crate::TransactionErr::Plain((ERRL!($format, $($args)+)))) }; + ($format: expr) => { Err(crate::TransactionErr::Plain(ERRL!($format))) } +} + +/// `TransactionErr:TxRecoverable` compatible `ERR` macro. +#[allow(unused_macros)] +macro_rules! TX_RECOVERABLE_ERR { + ($tx: expr, $format: expr, $($args: tt)+) => { + Err(crate::TransactionErr::TxRecoverable(TransactionEnum::from($tx), ERRL!($format, $($args)+))) + }; + ($tx: expr, $format: expr) => { + Err(crate::TransactionErr::TxRecoverable(TransactionEnum::from($tx), ERRL!($format))) + }; +} + macro_rules! ok_or_continue_after_sleep { ($e:expr, $delay: ident) => { match $e { @@ -318,7 +384,36 @@ impl Deref for TransactionEnum { } } -pub type TransactionFut = Box + Send>; +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub enum TransactionErr { + /// Keeps transactions while throwing errors. + TxRecoverable(TransactionEnum, String), + /// Simply for plain error messages. + Plain(String), +} + +impl TransactionErr { + /// Returns transaction if the error includes it. + #[inline] + pub fn get_tx(&self) -> Option { + match self { + TransactionErr::TxRecoverable(tx, _) => Some(tx.clone()), + _ => None, + } + } + + #[inline] + /// Returns plain text part of error. + pub fn get_plain_text_format(&self) -> String { + match self { + TransactionErr::TxRecoverable(_, err) => err.to_string(), + TransactionErr::Plain(err) => err.to_string(), + } + } +} + +pub type TransactionFut = Box + Send>; #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { @@ -516,6 +611,9 @@ pub trait MarketCoinOps { /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send>; + /// Receives raw transaction bytes as input and returns tx hash in hexadecimal format + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send>; + fn wait_for_confirmations( &self, tx: &[u8], @@ -778,7 +876,7 @@ impl Default for TransactionType { /// Transaction details #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TransactionDetails { - /// Raw bytes of signed transaction in hexadecimal string, this should be sent as is to send_raw_transaction RPC to broadcast the transaction + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction pub tx_hex: BytesJson, /// Transaction hash in hexadecimal format tx_hash: String, @@ -1631,6 +1729,21 @@ impl Deref for MmCoinEnum { } } +impl MmCoinEnum { + pub fn is_utxo_in_native_mode(&self) -> bool { + match self { + MmCoinEnum::UtxoCoin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::QtumCoin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::Qrc20Coin(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::Bch(ref c) => c.as_ref().rpc_client.is_native(), + MmCoinEnum::SlpToken(ref c) => c.as_ref().rpc_client.is_native(), + #[cfg(all(not(target_arch = "wasm32"), feature = "zhtlc"))] + MmCoinEnum::ZCoin(ref c) => c.as_ref().rpc_client.is_native(), + _ => false, + } + } +} + #[async_trait] pub trait BalanceTradeFeeUpdatedHandler { async fn balance_updated(&self, coin: &MmCoinEnum, new_balance: &BigDecimal); diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index 245e1ea3d3..c636790507 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -14,9 +14,9 @@ use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, BroadcastTxErr, FeePolicy use crate::{BalanceError, BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionEnum, TransactionFut, TransactionType, UnexpectedDerivationMethod, - ValidateAddressResult, ValidatePaymentInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, - WithdrawResult}; + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionType, + UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, WithdrawError, WithdrawFee, + WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bigdecimal::BigDecimal; use bitcrypto::{dhash160, sha256}; @@ -444,19 +444,19 @@ impl Qrc20Coin { /// Generate and send a transaction with the specified UTXO outputs. /// Note this function locks the `UTXO_LOCK`. - pub async fn send_contract_calls(&self, outputs: Vec) -> Result { + pub async fn send_contract_calls( + &self, + outputs: Vec, + ) -> Result { // TODO: we need to somehow refactor it using RecentlySpentOutpoints cache // Move over all QRC20 tokens should share the same cache with each other and base QTUM coin let _utxo_lock = UTXO_LOCK.lock().await; - let platform = self.platform.clone(); - let decimals = self.utxo.decimals; let GenerateQrc20TxResult { signed, .. } = self .generate_qrc20_transaction(outputs) .await - .mm_err(|e| e.into_withdraw_error(platform, decimals)) - .map_err(|e| ERRL!("{}", e))?; - let _tx = try_s!(self.utxo.rpc_client.send_transaction(&signed).compat().await); + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + try_tx_s!(self.utxo.rpc_client.send_transaction(&signed).compat().await, signed); Ok(signed.into()) } @@ -710,10 +710,10 @@ impl UtxoCommonOps for Qrc20Coin { #[async_trait] impl SwapOps for Qrc20Coin { fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { - let to_address = try_fus!(self.contract_address_from_raw_pubkey(fee_addr)); - let amount = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); + let to_address = try_tx_fus!(self.contract_address_from_raw_pubkey(fee_addr)); + let amount = try_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let transfer_output = - try_fus!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); + try_tx_fus!(self.transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT)); let outputs = vec![transfer_output]; let selfi = self.clone(); @@ -731,11 +731,11 @@ impl SwapOps for Qrc20Coin { amount: BigDecimal, swap_contract_address: &Option, ) -> TransactionFut { - let taker_addr = try_fus!(self.contract_address_from_raw_pubkey(taker_pub)); + let taker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(taker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); - let value = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); + let value = try_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let secret_hash = Vec::from(secret_hash); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -755,11 +755,11 @@ impl SwapOps for Qrc20Coin { amount: BigDecimal, swap_contract_address: &Option, ) -> TransactionFut { - let maker_addr = try_fus!(self.contract_address_from_raw_pubkey(maker_pub)); + let maker_addr = try_tx_fus!(self.contract_address_from_raw_pubkey(maker_pub)); let id = qrc20_swap_id(time_lock, secret_hash); - let value = try_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); + let value = try_tx_fus!(wei_from_big_decimal(&amount, self.utxo.decimals)); let secret_hash = Vec::from(secret_hash); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -779,8 +779,8 @@ impl SwapOps for Qrc20Coin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let secret = secret.to_vec(); let selfi = self.clone(); @@ -801,9 +801,9 @@ impl SwapOps for Qrc20Coin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); let secret = secret.to_vec(); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -823,8 +823,8 @@ impl SwapOps for Qrc20Coin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -844,8 +844,8 @@ impl SwapOps for Qrc20Coin { _htlc_privkey: &[u8], swap_contract_address: &Option, ) -> TransactionFut { - let payment_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); - let swap_contract_address = try_fus!(swap_contract_address.try_to_address()); + let payment_tx: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let swap_contract_address = try_tx_fus!(swap_contract_address.try_to_address()); let selfi = self.clone(); let fut = async move { @@ -1054,10 +1054,16 @@ impl MarketCoinOps for Qrc20Coin { fn platform_ticker(&self) -> &str { &self.0.platform } + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -1083,10 +1089,15 @@ impl MarketCoinOps for Qrc20Coin { from_block: u64, _swap_contract_address: &Option, ) -> TransactionFut { - let tx: UtxoTx = try_fus!(deserialize(transaction).map_err(|e| ERRL!("{:?}", e))); + let tx: UtxoTx = try_tx_fus!(deserialize(transaction).map_err(|e| ERRL!("{:?}", e))); let selfi = self.clone(); - let fut = async move { selfi.wait_for_tx_spend_impl(tx, wait_until, from_block).await }; + let fut = async move { + selfi + .wait_for_tx_spend_impl(tx, wait_until, from_block) + .map_err(TransactionErr::Plain) + .await + }; Box::new(fut.boxed().compat()) } diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index e1199be303..2def78c339 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -7,6 +7,7 @@ use common::{block_on, DEX_FEE_ADDR_RAW_PUBKEY}; use itertools::Itertools; use mocktopus::mocking::{MockResult, Mockable}; use rpc::v1::types::ToTxHash; +use std::mem::discriminant; const EXPECTED_TX_FEE: i64 = 1000; const CONTRACT_CALL_GAS_FEE: i64 = (QRC20_GAS_LIMIT_DEFAULT * QRC20_GAS_PRICE_DEFAULT) as i64; @@ -925,3 +926,35 @@ fn test_negotiate_swap_contract_addr_has_fallback() { let result = coin.negotiate_swap_contract_addr(Some(slice)).unwrap(); assert_eq!(Some(fallback_addr.to_vec().into()), result); } + +#[test] +fn test_send_contract_calls_recoverable_tx() { + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let (_ctx, coin) = qrc20_coin_for_test(&priv_key, None); + + let tx = TransactionEnum::UtxoTx("010000000160fd74b5714172f285db2b36f0b391cd6883e7291441631c8b18f165b0a4635d020000006a47304402205d409e141111adbc4f185ae856997730de935ac30a0d2b1ccb5a6c4903db8171022024fc59bbcfdbba283556d7eeee4832167301dc8e8ad9739b7865f67b9676b226012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000625403a08601012844a9059cbb000000000000000000000000ca1e04745e8ca0c60d8c5881531d51bec470743f00000000000000000000000000000000000000000000000000000000000f424014d362e096e873eb7907e205fadc6175c6fec7bc44c200ada205000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88acfe967d5f".into()); + + let fee_addr = hex::decode("03bc2c7ba671bae4a6fc835244c9762b41647b9827d4780a89a949b984a8ddcc05").unwrap(); + let to_address = coin.contract_address_from_raw_pubkey(&fee_addr).unwrap(); + let amount = BigDecimal::from(0.2); + let amount = wei_from_big_decimal(&amount, coin.utxo.decimals).unwrap(); + let mut transfer_output = coin + .transfer_output(to_address, amount, QRC20_GAS_LIMIT_DEFAULT, QRC20_GAS_PRICE_DEFAULT) + .unwrap(); + + // break the transfer output + transfer_output.value = 777; + transfer_output.gas_limit = 777; + transfer_output.gas_price = 777; + + let tx_err = block_on(coin.send_contract_calls(vec![transfer_output])).unwrap_err(); + + // The error variant should equal to `TxRecoverable` + assert_eq!( + discriminant(&tx_err), + discriminant(&TransactionErr::TxRecoverable(TransactionEnum::from(tx), String::new())) + ); +} diff --git a/mm2src/coins/qrc20/swap.rs b/mm2src/coins/qrc20/swap.rs index 185f96a2dd..aa5ab12c1b 100644 --- a/mm2src/coins/qrc20/swap.rs +++ b/mm2src/coins/qrc20/swap.rs @@ -37,16 +37,16 @@ impl Qrc20Coin { secret_hash: Vec, receiver_addr: H160, swap_contract_address: H160, - ) -> Result { - let balance = try_s!(self.my_spendable_balance().compat().await); - let balance = try_s!(wei_from_big_decimal(&balance, self.utxo.decimals)); + ) -> Result { + let balance = try_tx_s!(self.my_spendable_balance().compat().await); + let balance = try_tx_s!(wei_from_big_decimal(&balance, self.utxo.decimals)); // Check the balance to avoid unnecessary burning of gas if balance < value { - return ERR!("Balance {} is less than value {}", balance, value); + return TX_PLAIN_ERR!("Balance {} is less than value {}", balance, value); } - let outputs = try_s!( + let outputs = try_tx_s!( self.generate_swap_payment_outputs( balance, id, @@ -67,17 +67,18 @@ impl Qrc20Coin { payment_tx: UtxoTx, swap_contract_address: H160, secret: Vec, - ) -> Result { + ) -> Result { let Erc20PaymentDetails { swap_id, value, sender, .. - } = try_s!(self.erc20_payment_details_from_tx(&payment_tx).await); + } = try_tx_s!(self.erc20_payment_details_from_tx(&payment_tx).await); - let status = try_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); + let status = try_tx_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); if status != eth::PAYMENT_STATE_SENT.into() { - return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); + return TX_PLAIN_ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); } - let spend_output = try_s!(self.receiver_spend_output(&swap_contract_address, swap_id, value, secret, sender)); + let spend_output = + try_tx_s!(self.receiver_spend_output(&swap_contract_address, swap_id, value, secret, sender)); self.send_contract_calls(vec![spend_output]).await } @@ -85,22 +86,22 @@ impl Qrc20Coin { &self, swap_contract_address: H160, payment_tx: UtxoTx, - ) -> Result { + ) -> Result { let Erc20PaymentDetails { swap_id, value, receiver, secret_hash, .. - } = try_s!(self.erc20_payment_details_from_tx(&payment_tx).await); + } = try_tx_s!(self.erc20_payment_details_from_tx(&payment_tx).await); - let status = try_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); + let status = try_tx_s!(self.payment_status(&swap_contract_address, swap_id.clone()).await); if status != eth::PAYMENT_STATE_SENT.into() { - return ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); + return TX_PLAIN_ERR!("Payment state is not PAYMENT_STATE_SENT, got {}", status); } let refund_output = - try_s!(self.sender_refund_output(&swap_contract_address, swap_id, value, secret_hash, receiver)); + try_tx_s!(self.sender_refund_output(&swap_contract_address, swap_id, value, secret_hash, receiver)); self.send_contract_calls(vec![refund_output]).await } diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index 1f4c86aba5..324d4d71a1 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -1,10 +1,10 @@ -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, TransactionFut}; +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum}; use crate::solana::solana_common::{lamports_to_sol, PrepareTransferData, SufficientBalanceError}; use crate::solana::spl::SplTokenInfo; use crate::{BalanceError, BalanceFut, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, RawTransactionFut, RawTransactionRequest, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionType, ValidateAddressResult, ValidatePaymentInput, WithdrawError, - WithdrawFut, WithdrawRequest, WithdrawResult}; + TransactionDetails, TransactionFut, TransactionType, ValidateAddressResult, ValidatePaymentInput, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use base58::ToBase58; use bigdecimal::BigDecimal; @@ -239,7 +239,6 @@ async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> With let to = solana_sdk::pubkey::Pubkey::try_from(&*req.to)?; let tx = solana_sdk::system_transaction::transfer(&coin.key_pair, &to, res.lamports_to_send, hash); let serialized_tx = serialize(&tx).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let encoded_tx = hex::encode(&serialized_tx); let total_amount = lamports_to_sol(res.lamports_to_send); let received_by_me = if req.to == coin.my_address { total_amount.clone() @@ -248,7 +247,7 @@ async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> With }; let spent_by_me = &total_amount + &res.sol_required; Ok(TransactionDetails { - tx_hex: encoded_tx.as_bytes().into(), + tx_hex: serialized_tx.into(), tx_hash: tx.signatures[0].to_string(), from: vec![coin.my_address.clone()], to: vec![req.to], @@ -374,6 +373,17 @@ impl MarketCoinOps for SolanaCoin { Box::new(fut.boxed().compat()) } + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + let coin = self.clone(); + let tx = tx.to_owned(); + let fut = async move { + let tx = try_s!(deserialize(tx.as_slice())); + let signature = coin.rpc().send_transaction(&tx).await.map_err(|e| format!("{:?}", e))?; + Ok(signature.to_string()) + }; + Box::new(fut.boxed().compat()) + } + fn wait_for_confirmations( &self, _tx: &[u8], diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs index a92e37338e..1c482da102 100644 --- a/mm2src/coins/solana/solana_tests.rs +++ b/mm2src/coins/solana/solana_tests.rs @@ -6,7 +6,6 @@ use crate::MarketCoinOps; use base58::ToBase58; use solana_client::rpc_request::TokenAccountsFilter; use solana_sdk::signature::Signer; -use std::str; use std::str::FromStr; mod tests { @@ -257,9 +256,17 @@ mod tests { .await .unwrap(); println!("{:?}", valid_tx_details); - let tx_str = str::from_utf8(&*valid_tx_details.tx_hex.0).unwrap(); - let res = sol_coin.send_raw_tx(tx_str).compat().await; - assert_eq!(res.is_err(), false); + + let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let res = sol_coin.send_raw_tx(&tx_str).compat().await.unwrap(); + + let res2 = sol_coin + .send_raw_tx_bytes(&*valid_tx_details.tx_hex.0) + .compat() + .await + .unwrap(); + assert_eq!(res, res2); + //println!("{:?}", res); } diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index ab26e2726b..27abfea39a 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -1,10 +1,10 @@ -use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum, TransactionFut}; +use super::{CoinBalance, HistorySyncState, MarketCoinOps, MmCoin, SwapOps, TradeFee, TransactionEnum}; use crate::solana::solana_common::{ui_amount_to_amount, PrepareTransferData, SufficientBalanceError}; use crate::solana::{solana_common, AccountError, SolanaCommonOps, SolanaFeeDetails}; use crate::{BalanceFut, FeeApproxStage, FoundSwapTxSpend, NegotiateSwapContractAddrErr, RawTransactionFut, RawTransactionRequest, SolanaCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionType, ValidateAddressResult, ValidatePaymentInput, WithdrawError, - WithdrawFut, WithdrawRequest, WithdrawResult}; + TransactionDetails, TransactionFut, TransactionType, ValidateAddressResult, ValidatePaymentInput, + WithdrawError, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bigdecimal::BigDecimal; use bincode::serialize; @@ -117,14 +117,13 @@ async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> Withdr let signers = vec![&coin.platform_coin.key_pair]; let tx = Transaction::new(&signers, msg, hash); let serialized_tx = serialize(&tx).map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let encoded_tx = hex::encode(&serialized_tx); let received_by_me = if req.to == coin.platform_coin.my_address { res.to_send.clone() } else { 0.into() }; Ok(TransactionDetails { - tx_hex: encoded_tx.as_bytes().into(), + tx_hex: serialized_tx.into(), tx_hash: tx.signatures[0].to_string(), from: vec![coin.platform_coin.my_address.clone()], to: vec![req.to], @@ -219,10 +218,16 @@ impl MarketCoinOps for SplToken { fn platform_ticker(&self) -> &str { self.platform_coin.ticker() } + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { self.platform_coin.send_raw_tx(tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + self.platform_coin.send_raw_tx_bytes(tx) + } + fn wait_for_confirmations( &self, _tx: &[u8], diff --git a/mm2src/coins/solana/spl_tests.rs b/mm2src/coins/solana/spl_tests.rs index 0e9c5d7329..9b6ff141dc 100644 --- a/mm2src/coins/solana/spl_tests.rs +++ b/mm2src/coins/solana/spl_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::common::Future01CompatExt; use crate::{solana::solana_common_tests::solana_coin_for_test, solana::solana_common_tests::{spl_coin_for_test, SolanaNet}}; -use std::{str::from_utf8, str::FromStr}; +use std::str::FromStr; mod tests { use super::*; @@ -82,9 +82,16 @@ mod tests { assert_eq!(valid_tx_details.my_balance_change, withdraw_amount.neg()); assert_eq!(valid_tx_details.coin, "USDC".to_string()); assert_ne!(valid_tx_details.timestamp, 0); - let tx_str = from_utf8(&*valid_tx_details.tx_hex.0).unwrap(); - let res = usdc_sol_coin.send_raw_tx(tx_str).compat().await; - assert_eq!(res.is_err(), false); + + let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let res = usdc_sol_coin.send_raw_tx(&tx_str).compat().await.unwrap(); println!("{:?}", res); + + let res2 = usdc_sol_coin + .send_raw_tx_bytes(&*valid_tx_details.tx_hex.0) + .compat() + .await + .unwrap(); + assert_eq!(res, res2); } } diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index ac3e2526df..8a63b2f8e0 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -45,6 +45,8 @@ impl MarketCoinOps for TestCoin { /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send> { unimplemented!() } + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { unimplemented!() } + fn wait_for_confirmations( &self, tx: &[u8], diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 569177c2bd..4ef8b394cc 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -92,11 +92,12 @@ use super::{BalanceError, BalanceFut, BalanceResult, CoinsContext, DerivationMet PrivKeyActivationPolicy, PrivKeyNotAllowed, PrivKeyPolicy, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RpcTransportEventHandler, RpcTransportEventHandlerShared, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, Transaction, TransactionDetails, - TransactionEnum, TransactionFut, UnexpectedDerivationMethod, WithdrawError, WithdrawRequest}; + TransactionEnum, UnexpectedDerivationMethod, WithdrawError, WithdrawRequest}; use crate::coin_balance::{EnableCoinScanPolicy, HDAddressBalanceScanner}; use crate::hd_wallet::{HDAccountOps, HDAccountsMutex, HDAddress, HDWalletCoinOps, HDWalletOps, InvalidBip44ChainError}; use crate::hd_wallet_storage::{HDAccountStorageItem, HDWalletCoinStorage, HDWalletStorageError, HDWalletStorageResult}; use crate::utxo::utxo_block_header_storage::BlockHeaderStorageError; +use crate::TransactionErr; use utxo_block_header_storage::BlockHeaderStorage; #[cfg(not(target_arch = "wasm32"))] pub mod tx_cache; #[cfg(target_arch = "wasm32")] @@ -1469,12 +1470,15 @@ pub fn sat_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu }) } -async fn send_outputs_from_my_address_impl(coin: T, outputs: Vec) -> Result +async fn send_outputs_from_my_address_impl( + coin: T, + outputs: Vec, +) -> Result where T: UtxoCommonOps, { - let my_address = try_s!(coin.as_ref().derivation_method.iguana_or_err()); - let (unspents, recently_sent_txs) = try_s!(coin.list_unspent_ordered(my_address).await); + let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); + let (unspents, recently_sent_txs) = try_tx_s!(coin.list_unspent_ordered(my_address).await); generate_and_send_tx(&coin, unspents, None, FeePolicy::SendExact, recently_sent_txs, outputs).await } @@ -1486,12 +1490,12 @@ async fn generate_and_send_tx( fee_policy: FeePolicy, mut recently_spent: AsyncMutexGuard<'_, RecentlySpentOutPoints>, outputs: Vec, -) -> Result +) -> Result where T: AsRef + UtxoTxGenerationOps + UtxoTxBroadcastOps, { - let my_address = try_s!(coin.as_ref().derivation_method.iguana_or_err()); - let key_pair = try_s!(coin.as_ref().priv_key_policy.key_pair_or_err()); + let my_address = try_tx_s!(coin.as_ref().derivation_method.iguana_or_err()); + let key_pair = try_tx_s!(coin.as_ref().priv_key_policy.key_pair_or_err()); let mut builder = UtxoTxBuilder::new(coin) .add_available_inputs(unspents) @@ -1500,7 +1504,7 @@ where if let Some(required) = required_inputs { builder = builder.add_required_inputs(required); } - let (unsigned, _) = try_s!(builder.build().await); + let (unsigned, _) = try_tx_s!(builder.build().await); let spent_unspents = unsigned .inputs @@ -1518,7 +1522,7 @@ where }; let prev_script = Builder::build_p2pkh(&my_address.hash); - let signed = try_s!(sign_tx( + let signed = try_tx_s!(sign_tx( unsigned, key_pair, prev_script, @@ -1526,7 +1530,7 @@ where coin.as_ref().conf.fork_id )); - try_s!(coin.broadcast_tx(&signed).await); + try_tx_s!(coin.broadcast_tx(&signed).await, signed); recently_spent.add_spent(spent_unspents, signed.hash(), signed.outputs.clone()); diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index ea503887a3..ad34b1afe9 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -6,7 +6,7 @@ use crate::utxo::slp::{parse_slp_script, ParseSlpScriptError, SlpGenesisParams, use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::{BlockHeightAndTime, CanRefundHtlc, CoinBalance, CoinProtocol, NegotiateSwapContractAddrErr, - PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, SwapOps, TradePreimageValue, + PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, SwapOps, TradePreimageValue, TransactionFut, TransactionType, TxFeeDetails, ValidateAddressResult, ValidatePaymentInput, WithdrawFut}; use common::log::warn; use common::mm_metrics::MetricsArc; @@ -1053,10 +1053,16 @@ impl MarketCoinOps for BchCoin { fn platform_ticker(&self) -> &str { self.ticker() } + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo_arc, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 09c9aa177f..ead4e1a786 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -14,7 +14,8 @@ use crate::utxo::utxo_builder::{MergeUtxoArcOps, UtxoCoinBuildError, UtxoCoinBui UtxoFieldsWithHardwareWalletBuilder, UtxoFieldsWithIguanaPrivKeyBuilder}; use crate::{eth, CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, DelegationError, DelegationFut, GetWithdrawSenderAddress, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, StakingInfosFut, SwapOps, - TradePreimageValue, ValidateAddressResult, ValidatePaymentInput, WithdrawFut, WithdrawSenderAddress}; + TradePreimageValue, TransactionFut, ValidateAddressResult, ValidatePaymentInput, WithdrawFut, + WithdrawSenderAddress}; use common::mm_metrics::MetricsArc; use common::mm_number::MmNumber; use crypto::trezor::utxo::TrezorUtxoCoin; @@ -673,10 +674,16 @@ impl MarketCoinOps for QtumCoin { fn platform_ticker(&self) -> &str { self.ticker() } + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo_arc, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index bd206b7bd2..737d025437 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -178,6 +178,14 @@ impl UtxoRpcClientEnum { }; Box::new(fut.boxed().compat()) } + + #[inline] + pub fn is_native(&self) -> bool { + match self { + UtxoRpcClientEnum::Native(_) => true, + UtxoRpcClientEnum::Electrum(_) => false, + } + } } /// Generic unspent info required to build transactions, we need this separate type because native diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index c1af903cd5..f5691af02d 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -13,7 +13,7 @@ use crate::utxo::{generate_and_send_tx, sat_from_big_decimal, ActualTxFee, Addit use crate::{BalanceFut, CoinBalance, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, NumConversError, PrivKeyNotAllowed, RawTransactionFut, RawTransactionRequest, SwapOps, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionEnum, TransactionFut, TxFeeDetails, + TradePreimageValue, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TxFeeDetails, UnexpectedDerivationMethod, ValidateAddressResult, ValidatePaymentInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; @@ -409,8 +409,8 @@ impl SlpToken { Ok((preimage, recently_spent)) } - pub async fn send_slp_outputs(&self, slp_outputs: Vec) -> Result { - let (preimage, recently_spent) = try_s!(self.generate_slp_tx_preimage(slp_outputs).await); + pub async fn send_slp_outputs(&self, slp_outputs: Vec) -> Result { + let (preimage, recently_spent) = try_tx_s!(self.generate_slp_tx_preimage(slp_outputs).await); generate_and_send_tx( self, preimage.available_bch_inputs, @@ -429,11 +429,11 @@ impl SlpToken { time_lock: u32, secret_hash: &[u8], amount: u64, - ) -> Result { + ) -> Result { let payment_script = payment_script(time_lock, secret_hash, my_pub, other_pub); let script_pubkey = ScriptBuilder::build_p2sh(&dhash160(&payment_script).into()).to_bytes(); let slp_out = SlpOutput { amount, script_pubkey }; - let (preimage, recently_spent) = try_s!(self.generate_slp_tx_preimage(vec![slp_out]).await); + let (preimage, recently_spent) = try_tx_s!(self.generate_slp_tx_preimage(vec![slp_out]).await); generate_and_send_tx( self, preimage.available_bch_inputs, @@ -1085,16 +1085,27 @@ impl MarketCoinOps for SlpToken { /// Receives raw transaction bytes in hexadecimal format as input and returns tx hash in hexadecimal format fn send_raw_tx(&self, tx: &str) -> Box + Send> { - let coin = self.clone(); + let selfi = self.clone(); let tx = tx.to_owned(); let fut = async move { let bytes = hex::decode(tx).map_to_mm(|e| e).map_err(|e| format!("{:?}", e))?; - let tx = deserialize(bytes.as_slice()) - .map_to_mm(|e| e) - .map_err(|e| format!("{:?}", e))?; - let hash = coin.broadcast_tx(&tx).await.map_err(|e| format!("{:?}", e))?; + let tx = try_s!(deserialize(bytes.as_slice())); + let hash = selfi.broadcast_tx(&tx).await.map_err(|e| format!("{:?}", e))?; + Ok(format!("{:?}", hash)) + }; + + Box::new(fut.boxed().compat()) + } + + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + let selfi = self.clone(); + let bytes = tx.to_owned(); + let fut = async move { + let tx = try_s!(deserialize(bytes.as_slice())); + let hash = selfi.broadcast_tx(&tx).await.map_err(|e| format!("{:?}", e))?; Ok(format!("{:?}", hash)) }; + Box::new(fut.boxed().compat()) } @@ -1143,13 +1154,13 @@ impl MarketCoinOps for SlpToken { impl SwapOps for SlpToken { fn send_taker_fee(&self, fee_addr: &[u8], amount: BigDecimal, _uuid: &[u8]) -> TransactionFut { let coin = self.clone(); - let fee_pubkey = try_fus!(Public::from_slice(fee_addr)); + let fee_pubkey = try_tx_fus!(Public::from_slice(fee_addr)); let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); - let amount = try_fus!(sat_from_big_decimal(&amount, self.decimals())); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); let fut = async move { let slp_out = SlpOutput { amount, script_pubkey }; - let (preimage, recently_spent) = try_s!(coin.generate_slp_tx_preimage(vec![slp_out]).await); + let (preimage, recently_spent) = try_tx_s!(coin.generate_slp_tx_preimage(vec![slp_out]).await); generate_and_send_tx( &coin, preimage.available_bch_inputs, @@ -1172,14 +1183,14 @@ impl SwapOps for SlpToken { amount: BigDecimal, _swap_contract_address: &Option, ) -> TransactionFut { - let maker_pub = try_fus!(Public::from_slice(maker_pub)); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); - let amount = try_fus!(sat_from_big_decimal(&amount, self.decimals())); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); let secret_hash = secret_hash.to_owned(); let coin = self.clone(); let fut = async move { - let tx = try_s!( + let tx = try_tx_s!( coin.send_htlc(&maker_pub, &taker_pub, time_lock, &secret_hash, amount) .await ); @@ -1197,14 +1208,14 @@ impl SwapOps for SlpToken { amount: BigDecimal, _swap_contract_address: &Option, ) -> TransactionFut { - let taker_pub = try_fus!(Public::from_slice(taker_pub)); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); - let amount = try_fus!(sat_from_big_decimal(&amount, self.decimals())); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, self.decimals())); let secret_hash = secret_hash.to_owned(); let coin = self.clone(); let fut = async move { - let tx = try_s!( + let tx = try_tx_s!( coin.send_htlc(&taker_pub, &maker_pub, time_lock, &secret_hash, amount) .await ); @@ -1223,13 +1234,13 @@ impl SwapOps for SlpToken { _swap_contract_address: &Option, ) -> TransactionFut { let tx = taker_payment_tx.to_owned(); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); let secret = secret.to_owned(); - let htlc_keypair = try_fus!(key_pair_from_secret(htlc_privkey)); + let htlc_keypair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let coin = self.clone(); let fut = async move { - let tx = try_s!( + let tx = try_tx_s!( coin.spend_htlc(&tx, &taker_pub, time_lock, &secret, &htlc_keypair) .await ); @@ -1248,13 +1259,13 @@ impl SwapOps for SlpToken { _swap_contract_address: &Option, ) -> TransactionFut { let tx = maker_payment_tx.to_owned(); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); let secret = secret.to_owned(); - let htlc_keypair = try_fus!(key_pair_from_secret(htlc_privkey)); + let htlc_keypair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let coin = self.clone(); let fut = async move { - let tx = try_s!( + let tx = try_tx_s!( coin.spend_htlc(&tx, &maker_pub, time_lock, &secret, &htlc_keypair) .await ); @@ -1273,9 +1284,9 @@ impl SwapOps for SlpToken { _swap_contract_address: &Option, ) -> TransactionFut { let tx = taker_payment_tx.to_owned(); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); let secret_hash = secret_hash.to_owned(); - let keypair = try_fus!(key_pair_from_secret(htlc_privkey)); + let keypair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let coin = self.clone(); let fut = async move { @@ -1285,7 +1296,7 @@ impl SwapOps for SlpToken { ); Ok(tx.into()) }; - Box::new(fut.boxed().compat()) + Box::new(fut.boxed().compat().map_err(TransactionErr::Plain)) } fn send_maker_refunds_payment( @@ -1298,13 +1309,13 @@ impl SwapOps for SlpToken { _swap_contract_address: &Option, ) -> TransactionFut { let tx = maker_payment_tx.to_owned(); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); let secret_hash = secret_hash.to_owned(); - let keypair = try_fus!(key_pair_from_secret(htlc_privkey)); + let keypair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let coin = self.clone(); let fut = async move { - let tx = try_s!( + let tx = try_tx_s!( coin.refund_htlc(&tx, &taker_pub, time_lock, &secret_hash, &keypair) .await ); @@ -1784,8 +1795,9 @@ pub fn slp_addr_from_pubkey_str(pubkey: &str, prefix: &str) -> Result err, + TransactionErr::Plain(err) => err, + }; + println!("{:?}", err); assert!(err.contains("is not valid with reason outputs greater than inputs")); @@ -2028,18 +2046,32 @@ mod slp_tests { 142, 135, 205, 228, 173, 0, 0, 0, 0, 0, 25, 118, 169, 20, 140, 255, 252, 36, 9, 208, 99, 67, 125, 106, 168, 183, 90, 0, 155, 155, 165, 27, 113, 252, 136, 172, 216, 36, 92, 97, ]; - let tx_bytes_str = hex::encode(tx_bytes); + let tx_bytes_str = hex::encode(tx_bytes); let err = fusd.send_raw_tx(&tx_bytes_str).wait().unwrap_err(); println!("{:?}", err); assert!(err.contains("is not valid with reason outputs greater than inputs")); + let err2 = fusd.send_raw_tx_bytes(tx_bytes).wait().unwrap_err(); + println!("{:?}", err2); + assert!(err2.contains("is not valid with reason outputs greater than inputs")); + assert_eq!(err, err2); + let utxo_tx: UtxoTx = deserialize(tx_bytes).unwrap(); let err = block_on(fusd.broadcast_tx(&utxo_tx)).unwrap_err(); match err.into_inner() { BroadcastTxErr::Other(err) => assert!(err.contains("is not valid with reason outputs greater than inputs")), e @ _ => panic!("Unexpected err {:?}", e), }; + + // The error variant should equal to `TxRecoverable` + assert_eq!( + discriminant(&tx_err), + discriminant(&TransactionErr::TxRecoverable( + TransactionEnum::from(utxo_tx), + String::new() + )) + ); } #[test] diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 6b9d4b1141..5cac15ea76 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -10,8 +10,9 @@ use crate::utxo::rpc_clients::{electrum_script_hash, BlockHashOrHeight, UnspentI UtxoRpcClientOps, UtxoRpcResult}; use crate::utxo::utxo_withdraw::{InitUtxoWithdraw, StandardUtxoWithdraw, UtxoWithdraw}; use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, HDAddressId, - RawTransactionError, RawTransactionRequest, RawTransactionRes, TradePreimageValue, TxFeeDetails, - ValidateAddressResult, ValidatePaymentInput, WithdrawFrom, WithdrawResult, WithdrawSenderAddress}; + RawTransactionError, RawTransactionRequest, RawTransactionRes, TradePreimageValue, TransactionFut, + TxFeeDetails, ValidateAddressResult, ValidatePaymentInput, WithdrawFrom, WithdrawResult, + WithdrawSenderAddress}; use bigdecimal::{BigDecimal, Zero}; pub use bitcrypto::{dhash160, sha256, ChecksumType}; use chain::constants::SEQUENCE_FINAL; @@ -990,8 +991,11 @@ pub async fn p2sh_spending_tx( }) } -pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], amount: BigDecimal) -> TransactionFut { - let address = try_fus!(address_from_raw_pubkey( +pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], amount: BigDecimal) -> TransactionFut +where + T: AsRef + UtxoCommonOps + Send + Sync + 'static, +{ + let address = try_tx_fus!(address_from_raw_pubkey( fee_pub_key, coin.as_ref().conf.pub_addr_prefix, coin.as_ref().conf.pub_t_addr_prefix, @@ -999,7 +1003,7 @@ pub fn send_taker_fee(coin: T, fee_pub_key: &[u8], amount: Big coin.as_ref().conf.bech32_hrp.clone(), coin.addr_format().clone(), )); - let amount = try_fus!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); + let amount = try_tx_fus!(sat_from_big_decimal(&amount, coin.as_ref().decimals)); let output = TransactionOutput { value: amount, script_pubkey: Builder::build_p2pkh(&address.hash).to_bytes(), @@ -1018,7 +1022,7 @@ pub fn send_maker_payment( let SwapPaymentOutputsResult { payment_address, outputs, - } = try_fus!(generate_swap_payment_outputs( + } = try_tx_fus!(generate_swap_payment_outputs( &coin, time_lock, maker_pub, @@ -1029,11 +1033,11 @@ pub fn send_maker_payment( let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), UtxoRpcClientEnum::Native(client) => { - let addr_string = try_fus!(payment_address.display_address()); + let addr_string = try_tx_fus!(payment_address.display_address()); Either::B( client .import_address(&addr_string, &addr_string, false) - .map_err(|e| ERRL!("{}", e)) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))) .and_then(move |_| send_outputs_from_my_address(coin, outputs)), ) }, @@ -1052,7 +1056,7 @@ pub fn send_taker_payment( let SwapPaymentOutputsResult { payment_address, outputs, - } = try_fus!(generate_swap_payment_outputs( + } = try_tx_fus!(generate_swap_payment_outputs( &coin, time_lock, taker_pub, @@ -1060,14 +1064,15 @@ pub fn send_taker_payment( secret_hash, amount )); + let send_fut = match &coin.as_ref().rpc_client { UtxoRpcClientEnum::Electrum(_) => Either::A(send_outputs_from_my_address(coin, outputs)), UtxoRpcClientEnum::Native(client) => { - let addr_string = try_fus!(payment_address.display_address()); + let addr_string = try_tx_fus!(payment_address.display_address()); Either::B( client .import_address(&addr_string, &addr_string, false) - .map_err(|e| ERRL!("{}", e)) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e))) .and_then(move |_| send_outputs_from_my_address(coin, outputs)), ) }, @@ -1083,10 +1088,10 @@ pub fn send_maker_spends_taker_payment( secret: &[u8], htlc_privkey: &[u8], ) -> TransactionFut { - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); - let my_address = try_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); - let mut prev_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_tx: UtxoTx = try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let script_data = Builder::default() .push_data(secret) @@ -1095,17 +1100,18 @@ pub fn send_maker_spends_taker_payment( let redeem_script = payment_script( time_lock, &*dhash160(secret), - &try_fus!(Public::from_slice(taker_pub)), + &try_tx_fus!(Public::from_slice(taker_pub)), key_pair.public(), ); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_tx.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( + + let transaction = try_tx_s!( coin.p2sh_spending_tx( prev_tx, redeem_script.into(), @@ -1117,8 +1123,10 @@ pub fn send_maker_spends_taker_payment( ) .await ); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) @@ -1132,10 +1140,10 @@ pub fn send_taker_spends_maker_payment( secret: &[u8], htlc_privkey: &[u8], ) -> TransactionFut { - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); - let my_address = try_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); - let mut prev_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_tx: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let script_data = Builder::default() .push_data(secret) @@ -1144,17 +1152,18 @@ pub fn send_taker_spends_maker_payment( let redeem_script = payment_script( time_lock, &*dhash160(secret), - &try_fus!(Public::from_slice(maker_pub)), + &try_tx_fus!(Public::from_slice(maker_pub)), key_pair.public(), ); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_tx.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( + + let transaction = try_tx_s!( coin.p2sh_spending_tx( prev_tx, redeem_script.into(), @@ -1166,8 +1175,10 @@ pub fn send_taker_spends_maker_payment( ) .await ); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) @@ -1181,26 +1192,28 @@ pub fn send_taker_refunds_payment( secret_hash: &[u8], htlc_privkey: &[u8], ) -> TransactionFut { - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); - let my_address = try_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); - let mut prev_tx: UtxoTx = try_fus!(deserialize(taker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_tx: UtxoTx = + try_tx_fus!(deserialize(taker_payment_tx).map_err(|e| TransactionErr::Plain(format!("{:?}", e)))); prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( time_lock, secret_hash, key_pair.public(), - &try_fus!(Public::from_slice(maker_pub)), + &try_tx_fus!(Public::from_slice(maker_pub)), ); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_tx.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( + + let transaction = try_tx_s!( coin.p2sh_spending_tx( prev_tx, redeem_script.into(), @@ -1212,8 +1225,10 @@ pub fn send_taker_refunds_payment( ) .await ); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) @@ -1227,26 +1242,27 @@ pub fn send_maker_refunds_payment( secret_hash: &[u8], htlc_privkey: &[u8], ) -> TransactionFut { - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); - let my_address = try_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); + let my_address = try_tx_fus!(coin.as_ref().derivation_method.iguana_or_err()).clone(); - let mut prev_tx: UtxoTx = try_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); + let mut prev_tx: UtxoTx = try_tx_fus!(deserialize(maker_payment_tx).map_err(|e| ERRL!("{:?}", e))); prev_tx.tx_hash_algo = coin.as_ref().tx_hash_algo; let script_data = Builder::default().push_opcode(Opcode::OP_1).into_script(); let redeem_script = payment_script( time_lock, secret_hash, key_pair.public(), - &try_fus!(Public::from_slice(taker_pub)), + &try_tx_fus!(Public::from_slice(taker_pub)), ); let fut = async move { - let fee = try_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); + let fee = try_tx_s!(coin.get_htlc_spend_fee(DEFAULT_SWAP_TX_SPEND_SIZE).await); let script_pubkey = output_script(&my_address, ScriptType::P2PKH).to_bytes(); let output = TransactionOutput { value: prev_tx.outputs[0].value - fee, script_pubkey, }; - let transaction = try_s!( + + let transaction = try_tx_s!( coin.p2sh_spending_tx( prev_tx, redeem_script.into(), @@ -1258,8 +1274,10 @@ pub fn send_maker_refunds_payment( ) .await ); + let tx_fut = coin.as_ref().rpc_client.send_transaction(&transaction).compat(); - try_s!(tx_fut.await); + try_tx_s!(tx_fut.await, transaction); + Ok(transaction.into()) }; Box::new(fut.boxed().compat()) @@ -1638,6 +1656,7 @@ where Box::new(fut.boxed().compat()) } +/// Takes raw transaction as input and returns tx hash in hexadecimal format pub fn send_raw_tx(coin: &UtxoCoinFields, tx: &str) -> Box + Send> { let bytes = try_fus!(hex::decode(tx)); Box::new( @@ -1648,6 +1667,19 @@ pub fn send_raw_tx(coin: &UtxoCoinFields, tx: &str) -> Box Box + Send> { + Box::new( + coin.rpc_client + .send_raw_transaction(tx_bytes.into()) + .map_err(|e| ERRL!("{}", e)) + .map(|hash| format!("{:?}", hash)), + ) +} + pub fn wait_for_confirmations( coin: &UtxoCoinFields, tx: &[u8], @@ -1675,7 +1707,7 @@ pub fn wait_for_output_spend( from_block: u64, wait_until: u64, ) -> TransactionFut { - let mut tx: UtxoTx = try_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); + let mut tx: UtxoTx = try_tx_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.tx_hash_algo; let client = coin.rpc_client.clone(); let tx_hash_algo = coin.tx_hash_algo; @@ -1703,7 +1735,7 @@ pub fn wait_for_output_spend( }; if now_ms() / 1000 > wait_until { - return ERR!( + return TX_PLAIN_ERR!( "Waited too long until {} for transaction {:?} {} to be spent ", wait_until, tx, @@ -3543,7 +3575,7 @@ pub async fn merge_utxo_loop( ticker, tx.hash().reversed() ), - Err(e) => error!("Error {} on UTXO merge attempt for coin {}", e, ticker), + Err(e) => error!("Error {:?} on UTXO merge attempt for coin {}", e, ticker), } } } diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index aafee92a43..238f598c3e 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -12,8 +12,8 @@ use crate::init_create_account::{self, CreateNewAccountParams, InitCreateHDAccou use crate::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandle}; use crate::utxo::utxo_builder::{UtxoArcBuilder, UtxoCoinBuilder}; use crate::{CanRefundHtlc, CoinBalance, CoinWithDerivationMethod, GetWithdrawSenderAddress, - NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SwapOps, TradePreimageValue, ValidateAddressResult, - ValidatePaymentInput, WithdrawFut, WithdrawSenderAddress}; + NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, SwapOps, TradePreimageValue, TransactionFut, + ValidateAddressResult, ValidatePaymentInput, WithdrawFut, WithdrawSenderAddress}; use common::mm_metrics::MetricsArc; use common::mm_number::MmNumber; use crypto::trezor::utxo::TrezorUtxoCoin; @@ -449,10 +449,16 @@ impl MarketCoinOps for UtxoStandardCoin { fn platform_ticker(&self) -> &str { self.ticker() } + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(&self.utxo_arc, tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(&self.utxo_arc, tx) + } + fn wait_for_confirmations( &self, tx: &[u8], diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 0171fe8b23..7900ff8cf0 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -24,6 +24,7 @@ use futures::TryFutureExt; use mocktopus::mocking::*; use rpc::v1::types::H256 as H256Json; use serialization::{deserialize, CoinVariant}; +use std::mem::discriminant; use std::num::NonZeroUsize; const TEST_COIN_NAME: &'static str = "RICK"; @@ -179,6 +180,35 @@ fn test_extract_secret() { assert_eq!(secret, expected_secret); } +#[test] +fn test_send_maker_spends_taker_payment_recoverable_tx() { + let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); + let coin = utxo_coin_for_test(client.into(), None, false); + let tx_hex = hex::decode("0100000001de7aa8d29524906b2b54ee2e0281f3607f75662cbc9080df81d1047b78e21dbc00000000d7473044022079b6c50820040b1fbbe9251ced32ab334d33830f6f8d0bf0a40c7f1336b67d5b0220142ccf723ddabb34e542ed65c395abc1fbf5b6c3e730396f15d25c49b668a1a401209da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365004c6b6304f62b0e5cb175210270e75970bb20029b3879ec76c4acd320a8d0589e003636264d01a7d566504bfbac6782012088a9142fb610d856c19fd57f2d0cffe8dff689074b3d8a882103f368228456c940ac113e53dad5c104cf209f2f102a409207269383b6ab9b03deac68ffffffff01d0dc9800000000001976a9146d9d2b554d768232320587df75c4338ecc8bf37d88ac40280e5c").unwrap(); + let secret = hex::decode("9da937e5609680cb30bff4a7661364ca1d1851c2506fa80c443f00a3d3bf7365").unwrap(); + let keypair = key_pair_from_seed("").unwrap(); + + let tx_err = coin + .send_maker_spends_taker_payment( + &tx_hex, + 777, + &coin.my_public_key().unwrap().to_vec(), + &secret, + &*keypair.private().secret, + &coin.swap_contract_address(), + ) + .wait() + .unwrap_err(); + + let tx: UtxoTx = deserialize(tx_hex.as_slice()).unwrap(); + + // The error variant should equal to `TxRecoverable` + assert_eq!( + discriminant(&tx_err), + discriminant(&TransactionErr::TxRecoverable(TransactionEnum::from(tx), String::new())) + ); +} + #[test] fn test_generate_transaction() { let client = electrum_client_for_test(RICK_ELECTRUM_ADDRS); diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index b0a5247e0a..aebd351805 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -69,6 +69,25 @@ pub use z_coin_errors::*; #[cfg(all(test, feature = "zhtlc-native-tests"))] mod z_coin_tests; +/// `ZP2SHSpendError` compatible `TransactionErr` handling macro. +macro_rules! try_ztx_s { + ($e: expr) => { + match $e { + Ok(ok) => ok, + Err(err) => { + if let Some(tx) = err.get_inner().get_tx() { + return Err(crate::TransactionErr::TxRecoverable( + tx, + format!("{}:{}] {:?}", file!(), line!(), err), + )); + } + + return Err(crate::TransactionErr::Plain(ERRL!("{:?}", err))); + }, + } + }; +} + #[derive(Debug, Clone)] pub struct ARRRConsensusParams {} @@ -750,10 +769,16 @@ impl MarketCoinOps for ZCoin { fn platform_ticker(&self) -> &str { self.ticker() } + #[inline(always)] fn send_raw_tx(&self, tx: &str) -> Box + Send> { utxo_common::send_raw_tx(self.as_ref(), tx) } + #[inline(always)] + fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { + utxo_common::send_raw_tx_bytes(self.as_ref(), tx) + } + fn wait_for_confirmations( &self, tx: &[u8], @@ -809,7 +834,7 @@ impl SwapOps for ZCoin { let selfi = self.clone(); let uuid = uuid.to_owned(); let fut = async move { - let tx = try_s!(z_send_dex_fee(&selfi, amount, &uuid).await); + let tx = try_tx_s!(z_send_dex_fee(&selfi, amount, &uuid).await); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -825,11 +850,11 @@ impl SwapOps for ZCoin { _swap_contract_address: &Option, ) -> TransactionFut { let selfi = self.clone(); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); let secret_hash = secret_hash.to_vec(); let fut = async move { - let utxo_tx = try_s!(z_send_htlc(&selfi, time_lock, &maker_pub, &taker_pub, &secret_hash, amount).await); + let utxo_tx = try_tx_s!(z_send_htlc(&selfi, time_lock, &maker_pub, &taker_pub, &secret_hash, amount).await); Ok(utxo_tx.into()) }; Box::new(fut.boxed().compat()) @@ -845,11 +870,11 @@ impl SwapOps for ZCoin { _swap_contract_address: &Option, ) -> TransactionFut { let selfi = self.clone(); - let taker_pub = try_fus!(Public::from_slice(taker_pub)); - let maker_pub = try_fus!(Public::from_slice(maker_pub)); + let taker_pub = try_tx_fus!(Public::from_slice(taker_pub)); + let maker_pub = try_tx_fus!(Public::from_slice(maker_pub)); let secret_hash = secret_hash.to_vec(); let fut = async move { - let utxo_tx = try_s!(z_send_htlc(&selfi, time_lock, &taker_pub, &maker_pub, &secret_hash, amount).await); + let utxo_tx = try_tx_s!(z_send_htlc(&selfi, time_lock, &taker_pub, &maker_pub, &secret_hash, amount).await); Ok(utxo_tx.into()) }; Box::new(fut.boxed().compat()) @@ -864,12 +889,12 @@ impl SwapOps for ZCoin { htlc_privkey: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - let tx = try_fus!(ZTransaction::read(taker_payment_tx)); - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); + let tx = try_tx_fus!(ZTransaction::read(taker_payment_tx)); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let redeem_script = payment_script( time_lock, &*dhash160(secret), - &try_fus!(Public::from_slice(taker_pub)), + &try_tx_fus!(Public::from_slice(taker_pub)), key_pair.public(), ); let script_data = ScriptBuilder::default() @@ -887,7 +912,7 @@ impl SwapOps for ZCoin { script_data, key_pair.private().secret.as_slice(), ); - let tx = try_s!(tx_fut.await); + let tx = try_ztx_s!(tx_fut.await); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -902,12 +927,12 @@ impl SwapOps for ZCoin { htlc_privkey: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - let tx = try_fus!(ZTransaction::read(maker_payment_tx)); - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); + let tx = try_tx_fus!(ZTransaction::read(maker_payment_tx)); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let redeem_script = payment_script( time_lock, &*dhash160(secret), - &try_fus!(Public::from_slice(maker_pub)), + &try_tx_fus!(Public::from_slice(maker_pub)), key_pair.public(), ); let script_data = ScriptBuilder::default() @@ -925,7 +950,7 @@ impl SwapOps for ZCoin { script_data, key_pair.private().secret.as_slice(), ); - let tx = try_s!(tx_fut.await); + let tx = try_ztx_s!(tx_fut.await); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -940,13 +965,13 @@ impl SwapOps for ZCoin { htlc_privkey: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - let tx = try_fus!(ZTransaction::read(taker_payment_tx)); - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); + let tx = try_tx_fus!(ZTransaction::read(taker_payment_tx)); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let redeem_script = payment_script( time_lock, secret_hash, key_pair.public(), - &try_fus!(Public::from_slice(maker_pub)), + &try_tx_fus!(Public::from_slice(maker_pub)), ); let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); let selfi = self.clone(); @@ -960,7 +985,7 @@ impl SwapOps for ZCoin { script_data, key_pair.private().secret.as_slice(), ); - let tx = try_s!(tx_fut.await); + let tx = try_ztx_s!(tx_fut.await); Ok(tx.into()) }; Box::new(fut.boxed().compat()) @@ -975,13 +1000,13 @@ impl SwapOps for ZCoin { htlc_privkey: &[u8], _swap_contract_address: &Option, ) -> TransactionFut { - let tx = try_fus!(ZTransaction::read(maker_payment_tx)); - let key_pair = try_fus!(key_pair_from_secret(htlc_privkey)); + let tx = try_tx_fus!(ZTransaction::read(maker_payment_tx)); + let key_pair = try_tx_fus!(key_pair_from_secret(htlc_privkey)); let redeem_script = payment_script( time_lock, secret_hash, key_pair.public(), - &try_fus!(Public::from_slice(taker_pub)), + &try_tx_fus!(Public::from_slice(taker_pub)), ); let script_data = ScriptBuilder::default().push_opcode(Opcode::OP_1).into_script(); let selfi = self.clone(); @@ -995,7 +1020,7 @@ impl SwapOps for ZCoin { script_data, key_pair.private().secret.as_slice(), ); - let tx = try_s!(tx_fut.await); + let tx = try_ztx_s!(tx_fut.await); Ok(tx.into()) }; Box::new(fut.boxed().compat()) diff --git a/mm2src/coins/z_coin/z_htlc.rs b/mm2src/coins/z_coin/z_htlc.rs index 5de6a650f9..324d7c9bfb 100644 --- a/mm2src/coins/z_coin/z_htlc.rs +++ b/mm2src/coins/z_coin/z_htlc.rs @@ -10,7 +10,7 @@ use crate::utxo::rpc_clients::{UtxoRpcClientEnum, UtxoRpcError}; use crate::utxo::utxo_common::payment_script; use crate::utxo::{sat_from_big_decimal, UtxoAddressFormat}; use crate::z_coin::{ARRRConsensusParams, SendOutputsErr, ZOutput, DEX_FEE_OVK}; -use crate::{NumConversError, PrivKeyNotAllowed}; +use crate::{NumConversError, PrivKeyNotAllowed, TransactionEnum}; use bigdecimal::BigDecimal; use bitcrypto::dhash160; use chain::Transaction as UtxoTx; @@ -99,6 +99,8 @@ pub enum ZP2SHSpendError { ZTxBuilderError(ZTxBuilderError), PrivKeyNotAllowed(PrivKeyNotAllowed), Rpc(UtxoRpcError), + #[display(fmt = "{:?} {}", _0, _1)] + TxRecoverable(TransactionEnum, String), } impl From for ZP2SHSpendError { @@ -113,6 +115,16 @@ impl From for ZP2SHSpendError { fn from(rpc: UtxoRpcError) -> ZP2SHSpendError { ZP2SHSpendError::Rpc(rpc) } } +impl ZP2SHSpendError { + #[inline] + pub fn get_tx(&self) -> Option { + match self { + ZP2SHSpendError::TxRecoverable(ref tx, _) => Some(tx.clone()), + _ => None, + } + } +} + /// Spends P2SH output 0 to the coin's my_z_addr pub async fn z_p2sh_spend( coin: &ZCoin, @@ -161,10 +173,10 @@ pub async fn z_p2sh_spend( zcash_tx.write(&mut tx_buffer).unwrap(); let refund_tx: UtxoTx = deserialize(tx_buffer.as_slice()).expect("librustzcash should produce a valid tx"); - coin.rpc_client() - .send_raw_transaction(tx_buffer.into()) - .compat() - .await?; + match coin.rpc_client().send_raw_transaction(tx_buffer.into()).compat().await { + Ok(_) => (), + Err(e) => return Err(ZP2SHSpendError::TxRecoverable(refund_tx.into(), e.to_string()).into()), + }; Ok(refund_tx) } diff --git a/mm2src/docker_tests/qrc20_tests.rs b/mm2src/docker_tests/qrc20_tests.rs index 45c9b36a6a..814c2c8f39 100644 --- a/mm2src/docker_tests/qrc20_tests.rs +++ b/mm2src/docker_tests/qrc20_tests.rs @@ -7,7 +7,7 @@ use coins::utxo::rpc_clients::UtxoRpcClientEnum; use coins::utxo::utxo_common::big_decimal_from_sat; use coins::utxo::{UtxoActivationParams, UtxoCommonOps}; use coins::{FeeApproxStage, FoundSwapTxSpend, MarketCoinOps, MmCoin, SwapOps, TradePreimageValue, TransactionEnum, - ValidatePaymentInput}; + TransactionErr, ValidatePaymentInput}; use common::log::debug; use common::mm_ctx::{MmArc, MmCtxBuilder}; use common::mm_number::BigDecimal; @@ -729,7 +729,7 @@ fn test_wait_for_tx_spend() { // first try to check if the wait_for_tx_spend() returns an error correctly let wait_until = (now_ms() / 1000) + 5; - let err = maker_coin + let tx_err = maker_coin .wait_for_tx_spend( &payment_tx_hex, wait_until, @@ -738,6 +738,8 @@ fn test_wait_for_tx_spend() { ) .wait() .expect_err("Expected 'Waited too long' error"); + + let err = tx_err.get_plain_text_format(); log!("error: "[err]); assert!(err.contains("Waited too long")); diff --git a/mm2src/lp_network.rs b/mm2src/lp_network.rs index a08757ef2a..a2412f080d 100644 --- a/mm2src/lp_network.rs +++ b/mm2src/lp_network.rs @@ -16,11 +16,12 @@ // lp_network.rs // marketmaker // +use coins::lp_coinfind; use common::executor::spawn; -use common::log; use common::mm_ctx::{MmArc, MmWeak}; use common::mm_error::prelude::*; use common::mm_metrics::{ClockOps, MetricsOps}; +use common::{log, Future01CompatExt}; use derive_more::Display; use futures::{channel::oneshot, StreamExt}; use keys::KeyPair; @@ -148,6 +149,16 @@ async fn process_p2p_message( lp_swap::process_msg(ctx.clone(), split.next().unwrap_or_default(), &message.data).await; to_propagate = true; }, + Some(lp_swap::TX_HELPER_PREFIX) => { + if let Some(pair) = split.next() { + if let Ok(Some(coin)) = lp_coinfind(&ctx, pair).await { + match coin.send_raw_tx_bytes(&message.data).compat().await { + Ok(id) => log::debug!("Transaction broadcasted successfully: {:?} ", id), + Err(e) => log::error!("Broadcast transaction failed. {}", e), + } + } + } + }, None | Some(_) => (), } } @@ -160,6 +171,7 @@ async fn process_p2p_message( &message.data, i_am_relay, ); + if process_fut.await { to_propagate = true; } diff --git a/mm2src/lp_swap.rs b/mm2src/lp_swap.rs index 45fdac4354..b203e8ff00 100644 --- a/mm2src/lp_swap.rs +++ b/mm2src/lp_swap.rs @@ -116,6 +116,8 @@ pub use trade_preimage::trade_preimage_rpc; pub const SWAP_PREFIX: TopicPrefix = "swap"; +pub const TX_HELPER_PREFIX: TopicPrefix = "txhlp"; + cfg_wasm32! { use common::indexed_db::{ConstructibleDb, DbLocked}; use swap_wasm_db::{InitDbResult, SwapDb}; @@ -190,6 +192,17 @@ pub fn broadcast_swap_message(ctx: &MmArc, topic: String, msg: SwapMsg, p2p_priv broadcast_p2p_msg(ctx, vec![topic], encoded_msg, from); } +/// Broadcast the tx message once +pub fn broadcast_p2p_tx_msg(ctx: &MmArc, topic: String, msg: &TransactionEnum, p2p_privkey: &Option) { + let (p2p_private, from) = match p2p_privkey { + Some(keypair) => (keypair.private_bytes(), Some(keypair.libp2p_peer_id())), + None => (ctx.secp256k1_key_pair().private().secret.take(), None), + }; + + let encoded_msg = encode_and_sign(&msg.tx_hex(), &p2p_private).unwrap(); + broadcast_p2p_msg(ctx, vec![topic], encoded_msg, from); +} + pub async fn process_msg(ctx: MmArc, topic: &str, msg: &[u8]) { let uuid = match Uuid::from_str(topic) { Ok(u) => u, @@ -237,6 +250,16 @@ pub async fn process_msg(ctx: MmArc, topic: &str, msg: &[u8]) { pub fn swap_topic(uuid: &Uuid) -> String { pub_sub_topic(SWAP_PREFIX, &uuid.to_string()) } +/// Formats and returns a topic format for `txhlp`. +/// +/// # Usage +/// ```ignore +/// let topic = tx_helper_topic("BTC"); +/// // Returns topic format `txhlp/BTC` as String type. +/// ``` +#[inline(always)] +pub fn tx_helper_topic(coin: &str) -> String { pub_sub_topic(TX_HELPER_PREFIX, coin) } + async fn recv_swap_msg( ctx: MmArc, mut getter: impl FnMut(&mut SwapMsgStore) -> Option, diff --git a/mm2src/lp_swap/maker_swap.rs b/mm2src/lp_swap/maker_swap.rs index e14d09b92b..8386dffba6 100644 --- a/mm2src/lp_swap/maker_swap.rs +++ b/mm2src/lp_swap/maker_swap.rs @@ -11,6 +11,7 @@ use super::{broadcast_my_swap_status, broadcast_swap_message_every, check_other_ use crate::mm2::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{MakerOrderBuilder, OrderConfirmationsSettings}; +use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, tx_helper_topic}; use crate::mm2::MM_VERSION; use bitcrypto::dhash160; use coins::{CanRefundHtlc, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, TradeFee, TradePreimageValue, @@ -648,6 +649,7 @@ impl MakerSwap { &self.r().data.maker_coin_swap_contract_address, ) .compat(); + let transaction = match transaction_f.await { Ok(res) => match res { Some(tx) => tx, @@ -665,8 +667,10 @@ impl MakerSwap { Ok(t) => t, Err(err) => { return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("{}", err).into()), - ])) + MakerSwapEvent::MakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])); }, } }, @@ -841,17 +845,37 @@ impl MakerSwap { let transaction = match spend_fut.compat().await { Ok(t) => t, Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.taker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + return Ok((Some(MakerSwapCommand::RefundMakerPayment), vec![ MakerSwapEvent::TakerPaymentSpendFailed( - ERRL!("!taker_coin.send_maker_spends_taker_payment: {}", err).into(), + ERRL!( + "!taker_coin.send_maker_spends_taker_payment: {}", + err.get_plain_text_format() + ) + .into(), ), MakerSwapEvent::MakerPaymentWaitRefundStarted { wait_until: self.wait_refund_until(), }, - ])) + ])); }, }; + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.taker_coin.ticker()), + &transaction, + &self.p2p_privkey, + ); + let tx_hash = transaction.tx_hash(); log!({ "Taker payment spend tx {:02x}", tx_hash }); let tx_ident = TransactionIdentifier { @@ -917,13 +941,34 @@ impl MakerSwap { let transaction = match spend_fut.compat().await { Ok(t) => t, Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::MakerPaymentRefundFailed( - ERRL!("!maker_coin.send_maker_refunds_payment: {}", err).into(), + ERRL!( + "!maker_coin.send_maker_refunds_payment: {}", + err.get_plain_text_format() + ) + .into(), ), - ])) + ])); }, }; + + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &transaction, + &self.p2p_privkey, + ); + let tx_hash = transaction.tx_hash(); log!({ "Maker payment refund tx {:02x}", tx_hash }); let tx_ident = TransactionIdentifier { @@ -1074,7 +1119,7 @@ impl MakerSwap { ) .compat() .await - .map_err(|e| ERRL!("{}", e)) + .map_err(|e| ERRL!("{:?}", e)) } if self.finished_at.load(Ordering::Relaxed) == 0 { @@ -1142,6 +1187,7 @@ impl MakerSwap { Ok(Some(FoundSwapTxSpend::Spent(_))) => { log!("Warning: MakerPayment spent, but TakerPayment is not yet. Trying to spend TakerPayment"); let transaction = try_s!(try_spend_taker_payment(self, &secret_hash.0).await); + Ok(RecoveredSwap { action: RecoveredSwapAction::SpentOtherPayment, coin: self.taker_coin.ticker().to_string(), @@ -1163,20 +1209,31 @@ impl MakerSwap { self.r().data.maker_payment_lock + 3700 ); } - let transaction = try_s!( - self.maker_coin - .send_maker_refunds_payment( - &maker_payment, - maker_payment_lock, - other_maker_coin_htlc_pub.as_slice(), - &secret_hash.0, - maker_coin_htlc_keypair.private().secret.as_slice(), - &maker_coin_swap_contract_address, - ) - .compat() - .await + let fut = self.maker_coin.send_maker_refunds_payment( + &maker_payment, + maker_payment_lock, + other_maker_coin_htlc_pub.as_slice(), + &secret_hash.0, + maker_coin_htlc_keypair.private().secret.as_slice(), + &maker_coin_swap_contract_address, ); + let transaction = match fut.compat().await { + Ok(t) => t, + Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + + return ERR!("{}", err.get_plain_text_format()); + }, + }; + Ok(RecoveredSwap { action: RecoveredSwapAction::RefundedMyPayment, coin: self.maker_coin.ticker().to_string(), diff --git a/mm2src/lp_swap/taker_swap.rs b/mm2src/lp_swap/taker_swap.rs index d26069a406..5336c3181e 100644 --- a/mm2src/lp_swap/taker_swap.rs +++ b/mm2src/lp_swap/taker_swap.rs @@ -10,6 +10,7 @@ use super::{broadcast_my_swap_status, broadcast_swap_message_every, check_other_ SwapConfirmationsSettings, SwapError, SwapMsg, SwapsContext, TransactionIdentifier, WAIT_CONFIRM_INTERVAL}; use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{MatchBy, OrderConfirmationsSettings, TakerAction, TakerOrderBuilder}; +use crate::mm2::lp_swap::{broadcast_p2p_tx_msg, tx_helper_topic}; use crate::mm2::MM_VERSION; use coins::{lp_coinfind, CanRefundHtlc, FeeApproxStage, FoundSwapTxSpend, MmCoinEnum, TradeFee, TradePreimageValue, ValidatePaymentInput}; @@ -1022,8 +1023,8 @@ impl TakerSwap { Ok(t) => t, Err(err) => { return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerFeeSendFailed(ERRL!("{}", err).into()), - ])) + TakerSwapEvent::TakerFeeSendFailed(ERRL!("{}", err.get_plain_text_format()).into()), + ])); }, }; @@ -1167,10 +1168,12 @@ impl TakerSwap { match payment_fut.compat().await { Ok(t) => t, - Err(e) => { + Err(err) => { return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("{}", e).into()), - ])) + TakerSwapEvent::TakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])); }, } }, @@ -1231,13 +1234,13 @@ impl TakerSwap { ); let tx = match f.compat().await { Ok(t) => t, - Err(e) => { + Err(err) => { return Ok((Some(TakerSwapCommand::RefundTakerPayment), vec![ - TakerSwapEvent::TakerPaymentWaitForSpendFailed(e.into()), + TakerSwapEvent::TakerPaymentWaitForSpendFailed(err.get_plain_text_format().into()), TakerSwapEvent::TakerPaymentWaitRefundStarted { wait_until: self.wait_refund_until(), }, - ])) + ])); }, }; drop(send_abort_handle); @@ -1279,12 +1282,28 @@ impl TakerSwap { let transaction = match spend_fut.compat().await { Ok(t) => t, Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + }; + return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::MakerPaymentSpendFailed(ERRL!("{}", err).into()), - ])) + TakerSwapEvent::MakerPaymentSpendFailed(ERRL!("{}", err.get_plain_text_format()).into()), + ])); }, }; + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &transaction, + &self.p2p_privkey, + ); + let tx_hash = transaction.tx_hash(); log!({"Maker payment spend tx {:02x}", tx_hash }); let tx_ident = TransactionIdentifier { @@ -1322,12 +1341,28 @@ impl TakerSwap { let transaction = match refund_fut.compat().await { Ok(t) => t, Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.taker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentRefundFailed(ERRL!("{}", err).into()), - ])) + TakerSwapEvent::TakerPaymentRefundFailed(ERRL!("{:?}", err.get_plain_text_format()).into()), + ])); }, }; + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.taker_coin.ticker()), + &transaction, + &self.p2p_privkey, + ); + let tx_hash = transaction.tx_hash(); log!({"Taker refund tx hash {:02x}", tx_hash }); let tx_ident = TransactionIdentifier { @@ -1515,20 +1550,31 @@ impl TakerSwap { let secret = self.r().secret.0; let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); - let transaction = try_s!( - self.maker_coin - .send_taker_spends_maker_payment( - &maker_payment, - self.maker_payment_lock.load(Ordering::Relaxed) as u32, - other_maker_coin_htlc_pub.as_slice(), - &secret, - maker_coin_htlc_keypair.private().secret.as_slice(), - &maker_coin_swap_contract_address, - ) - .compat() - .await + let fut = self.maker_coin.send_taker_spends_maker_payment( + &maker_payment, + self.maker_payment_lock.load(Ordering::Relaxed) as u32, + other_maker_coin_htlc_pub.as_slice(), + &secret, + maker_coin_htlc_keypair.private().secret.as_slice(), + &maker_coin_swap_contract_address, ); + let transaction = match fut.compat().await { + Ok(t) => t, + Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + + return ERR!("{}", err.get_plain_text_format()); + }, + }; + return Ok(RecoveredSwap { action: RecoveredSwapAction::SpentOtherPayment, coin: self.maker_coin.ticker().to_string(), @@ -1554,20 +1600,32 @@ impl TakerSwap { FoundSwapTxSpend::Spent(tx) => { check_maker_payment_is_not_spent!(); let secret = try_s!(self.taker_coin.extract_secret(&self.r().secret_hash.0, &tx.tx_hex())); - let transaction = try_s!( - self.maker_coin - .send_taker_spends_maker_payment( - &maker_payment, - self.maker_payment_lock.load(Ordering::Relaxed) as u32, - other_maker_coin_htlc_pub.as_slice(), - &secret, - maker_coin_htlc_keypair.private().secret.as_slice(), - &maker_coin_swap_contract_address, - ) - .compat() - .await + + let fut = self.maker_coin.send_taker_spends_maker_payment( + &maker_payment, + self.maker_payment_lock.load(Ordering::Relaxed) as u32, + other_maker_coin_htlc_pub.as_slice(), + &secret, + maker_coin_htlc_keypair.private().secret.as_slice(), + &maker_coin_swap_contract_address, ); + let transaction = match fut.compat().await { + Ok(t) => t, + Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.maker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + + return ERR!("{}", err.get_plain_text_format()); + }, + }; + Ok(RecoveredSwap { action: RecoveredSwapAction::SpentOtherPayment, coin: self.maker_coin.ticker().to_string(), @@ -1587,20 +1645,31 @@ impl TakerSwap { ); } - let transaction = try_s!( - self.taker_coin - .send_taker_refunds_payment( - &taker_payment, - taker_payment_lock as u32, - other_taker_coin_htlc_pub.as_slice(), - &secret_hash, - taker_coin_htlc_keypair.private().secret.as_slice(), - &taker_coin_swap_contract_address, - ) - .compat() - .await + let fut = self.taker_coin.send_taker_refunds_payment( + &taker_payment, + taker_payment_lock as u32, + other_taker_coin_htlc_pub.as_slice(), + &secret_hash, + taker_coin_htlc_keypair.private().secret.as_slice(), + &taker_coin_swap_contract_address, ); + let transaction = match fut.compat().await { + Ok(t) => t, + Err(err) => { + if let Some(tx) = err.get_tx() { + broadcast_p2p_tx_msg( + &self.ctx, + tx_helper_topic(self.taker_coin.ticker()), + &tx, + &self.p2p_privkey, + ); + } + + return ERR!("{:?}", err.get_plain_text_format()); + }, + }; + Ok(RecoveredSwap { action: RecoveredSwapAction::RefundedMyPayment, coin: self.taker_coin.ticker().to_string(), diff --git a/mm2src/rpc/lp_commands/lp_commands_legacy.rs b/mm2src/rpc/lp_commands/lp_commands_legacy.rs index 9e3977ef96..4ab1c148e7 100644 --- a/mm2src/rpc/lp_commands/lp_commands_legacy.rs +++ b/mm2src/rpc/lp_commands/lp_commands_legacy.rs @@ -30,8 +30,9 @@ use serde_json::{self as json, Value as Json}; use std::borrow::Cow; use crate::mm2::lp_dispatcher::{dispatch_lp_event, StopCtxEvent}; +use crate::mm2::lp_network::subscribe_to_topic; use crate::mm2::lp_ordermatch::{cancel_orders_by, CancelBy}; -use crate::mm2::lp_swap::active_swaps_using_coin; +use crate::mm2::lp_swap::{active_swaps_using_coin, tx_helper_topic}; use crate::mm2::MmVersionResult; /// Attempts to disable the coin @@ -128,7 +129,13 @@ pub async fn enable(ctx: MmArc, req: Json) -> Result>, String> mature_confirmations: coin.mature_confirmations(), }; let res = try_s!(json::to_vec(&res)); - Ok(try_s!(Response::builder().body(res))) + let res = try_s!(Response::builder().body(res)); + + if coin.is_utxo_in_native_mode() { + subscribe_to_topic(&ctx, tx_helper_topic(coin.ticker())); + } + + Ok(res) } #[cfg(target_arch = "wasm32")]