diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index aead751eb0..6075317bfc 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -1,12 +1,12 @@ -use crate::eth::eth_swap_v2::{PaymentStatusErr, ValidatePaymentV2Err}; -use crate::eth::nft_swap_v2::errors::{Erc721FunctionError, HtlcParamsError, PrepareTxDataError}; +use crate::eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; +use crate::eth::nft_swap_v2::errors::{Erc721FunctionError, HtlcParamsError}; use crate::eth::{EthAssocTypesError, EthNftAssocTypesError, Web3RpcError}; use crate::{utxo::rpc_clients::UtxoRpcError, NumConversError, UnexpectedDerivationMethod}; use enum_derives::EnumFromStringify; use futures01::Future; use mm2_err_handle::prelude::MmError; use spv_validation::helpers_validation::SPVError; -use std::num::TryFromIntError; +use std::{array::TryFromSliceError, num::TryFromIntError}; /// Helper type used as result for swap payment validation function(s) pub type ValidatePaymentFut = Box> + Send>; @@ -24,7 +24,9 @@ pub enum ValidatePaymentError { "NumConversError", "UnexpectedDerivationMethod", "keys::Error", - "PrepareTxDataError" + "PrepareTxDataError", + "ethabi::Error", + "TryFromSliceError" )] InternalError(String), /// Problem with deserializing the transaction, or one of the transaction parts is invalid. @@ -49,8 +51,7 @@ pub enum ValidatePaymentError { WatcherRewardError(String), /// Input payment timelock overflows the type used by specific coin. TimelockOverflow(TryFromIntError), - #[display(fmt = "Nft Protocol is not supported yet!")] - NftProtocolNotSupported, + ProtocolNotSupported(String), InvalidData(String), } @@ -77,7 +78,9 @@ impl From for ValidatePaymentError { | Web3RpcError::Timeout(internal) | Web3RpcError::NumConversError(internal) | Web3RpcError::InvalidGasApiConfig(internal) => ValidatePaymentError::InternalError(internal), - Web3RpcError::NftProtocolNotSupported => ValidatePaymentError::NftProtocolNotSupported, + Web3RpcError::NftProtocolNotSupported => { + ValidatePaymentError::ProtocolNotSupported("Nft protocol is not supported".to_string()) + }, } } } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 6ed8d7031b..5a88cc9ce2 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -158,6 +158,7 @@ use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiC InfuraGasApiCaller}; pub(crate) mod eth_swap_v2; +use eth_swap_v2::{EthPaymentType, PaymentMethod}; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 @@ -245,12 +246,51 @@ pub mod gas_limit { pub const ERC20_RECEIVER_SPEND: u64 = 150_000; /// Gas limit for swap refund tx with coins pub const ETH_SENDER_REFUND: u64 = 100_000; - /// Gas limit for swap refund tx with with ERC20 tokens + /// Gas limit for swap refund tx with ERC20 tokens pub const ERC20_SENDER_REFUND: u64 = 150_000; /// Gas limit for other operations pub const ETH_MAX_TRADE_GAS: u64 = 150_000; } +/// Default gas limits for EthGasLimitV2 +pub mod gas_limit_v2 { + /// Gas limits for maker operations in EtomicSwapMakerV2 contract + pub mod maker { + pub const ETH_PAYMENT: u64 = 65_000; + pub const ERC20_PAYMENT: u64 = 150_000; + pub const ETH_TAKER_SPEND: u64 = 100_000; + pub const ERC20_TAKER_SPEND: u64 = 150_000; + pub const ETH_MAKER_REFUND_TIMELOCK: u64 = 90_000; + pub const ERC20_MAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ETH_MAKER_REFUND_SECRET: u64 = 90_000; + pub const ERC20_MAKER_REFUND_SECRET: u64 = 100_000; + } + + /// Gas limits for taker operations in EtomicSwapTakerV2 contract + pub mod taker { + pub const ETH_PAYMENT: u64 = 65_000; + pub const ERC20_PAYMENT: u64 = 150_000; + pub const ETH_MAKER_SPEND: u64 = 100_000; + pub const ERC20_MAKER_SPEND: u64 = 115_000; + pub const ETH_TAKER_REFUND_TIMELOCK: u64 = 90_000; + pub const ERC20_TAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ETH_TAKER_REFUND_SECRET: u64 = 90_000; + pub const ERC20_TAKER_REFUND_SECRET: u64 = 100_000; + pub const APPROVE_PAYMENT: u64 = 50_000; + } + + pub mod nft_maker { + pub const ERC721_PAYMENT: u64 = 130_000; + pub const ERC1155_PAYMENT: u64 = 130_000; + pub const ERC721_TAKER_SPEND: u64 = 100_000; + pub const ERC1155_TAKER_SPEND: u64 = 100_000; + pub const ERC721_MAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ERC1155_MAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ERC721_MAKER_REFUND_SECRET: u64 = 100_000; + pub const ERC1155_MAKER_REFUND_SECRET: u64 = 100_000; + } +} + /// Coin conf param to override default gas limits #[derive(Deserialize)] #[serde(default)] @@ -269,7 +309,7 @@ pub struct EthGasLimit { pub erc20_receiver_spend: u64, /// Gas limit for swap refund tx with coins pub eth_sender_refund: u64, - /// Gas limit for swap refund tx with with ERC20 tokens + /// Gas limit for swap refund tx with ERC20 tokens pub erc20_sender_refund: u64, /// Gas limit for other operations pub eth_max_trade_gas: u64, @@ -291,6 +331,176 @@ impl Default for EthGasLimit { } } +#[derive(Default, Deserialize)] +#[serde(default)] +pub struct EthGasLimitV2 { + pub maker: MakerGasLimitV2, + pub taker: TakerGasLimitV2, + pub nft_maker: NftMakerGasLimitV2, +} + +#[derive(Deserialize)] +#[serde(default)] +pub struct MakerGasLimitV2 { + pub eth_payment: u64, + pub erc20_payment: u64, + pub eth_taker_spend: u64, + pub erc20_taker_spend: u64, + pub eth_maker_refund_timelock: u64, + pub erc20_maker_refund_timelock: u64, + pub eth_maker_refund_secret: u64, + pub erc20_maker_refund_secret: u64, +} + +#[derive(Deserialize)] +#[serde(default)] +pub struct TakerGasLimitV2 { + pub eth_payment: u64, + pub erc20_payment: u64, + pub eth_maker_spend: u64, + pub erc20_maker_spend: u64, + pub eth_taker_refund_timelock: u64, + pub erc20_taker_refund_timelock: u64, + pub eth_taker_refund_secret: u64, + pub erc20_taker_refund_secret: u64, + pub approve_payment: u64, +} + +#[derive(Deserialize)] +#[serde(default)] +pub struct NftMakerGasLimitV2 { + pub erc721_payment: u64, + pub erc1155_payment: u64, + pub erc721_taker_spend: u64, + pub erc1155_taker_spend: u64, + pub erc721_maker_refund_timelock: u64, + pub erc1155_maker_refund_timelock: u64, + pub erc721_maker_refund_secret: u64, + pub erc1155_maker_refund_secret: u64, +} + +impl EthGasLimitV2 { + fn gas_limit( + &self, + coin_type: &EthCoinType, + payment_type: EthPaymentType, + method: PaymentMethod, + ) -> Result { + match coin_type { + EthCoinType::Eth => { + let gas_limit = match payment_type { + EthPaymentType::MakerPayments => match method { + PaymentMethod::Send => self.maker.eth_payment, + PaymentMethod::Spend => self.maker.eth_taker_spend, + PaymentMethod::RefundTimelock => self.maker.eth_maker_refund_timelock, + PaymentMethod::RefundSecret => self.maker.eth_maker_refund_secret, + }, + EthPaymentType::TakerPayments => match method { + PaymentMethod::Send => self.taker.eth_payment, + PaymentMethod::Spend => self.taker.eth_maker_spend, + PaymentMethod::RefundTimelock => self.taker.eth_taker_refund_timelock, + PaymentMethod::RefundSecret => self.taker.eth_taker_refund_secret, + }, + }; + Ok(gas_limit) + }, + EthCoinType::Erc20 { .. } => { + let gas_limit = match payment_type { + EthPaymentType::MakerPayments => match method { + PaymentMethod::Send => self.maker.erc20_payment, + PaymentMethod::Spend => self.maker.erc20_taker_spend, + PaymentMethod::RefundTimelock => self.maker.erc20_maker_refund_timelock, + PaymentMethod::RefundSecret => self.maker.erc20_maker_refund_secret, + }, + EthPaymentType::TakerPayments => match method { + PaymentMethod::Send => self.taker.erc20_payment, + PaymentMethod::Spend => self.taker.erc20_maker_spend, + PaymentMethod::RefundTimelock => self.taker.erc20_taker_refund_timelock, + PaymentMethod::RefundSecret => self.taker.erc20_taker_refund_secret, + }, + }; + Ok(gas_limit) + }, + EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()), + } + } + + fn nft_gas_limit(&self, contract_type: &ContractType, method: PaymentMethod) -> u64 { + match contract_type { + ContractType::Erc1155 => match method { + PaymentMethod::Send => self.nft_maker.erc1155_payment, + PaymentMethod::Spend => self.nft_maker.erc1155_taker_spend, + PaymentMethod::RefundTimelock => self.nft_maker.erc1155_maker_refund_timelock, + PaymentMethod::RefundSecret => self.nft_maker.erc1155_maker_refund_secret, + }, + ContractType::Erc721 => match method { + PaymentMethod::Send => self.nft_maker.erc721_payment, + PaymentMethod::Spend => self.nft_maker.erc721_taker_spend, + PaymentMethod::RefundTimelock => self.nft_maker.erc721_maker_refund_timelock, + PaymentMethod::RefundSecret => self.nft_maker.erc721_maker_refund_secret, + }, + } + } +} + +impl Default for MakerGasLimitV2 { + fn default() -> Self { + MakerGasLimitV2 { + eth_payment: gas_limit_v2::maker::ETH_PAYMENT, + erc20_payment: gas_limit_v2::maker::ERC20_PAYMENT, + eth_taker_spend: gas_limit_v2::maker::ETH_TAKER_SPEND, + erc20_taker_spend: gas_limit_v2::maker::ERC20_TAKER_SPEND, + eth_maker_refund_timelock: gas_limit_v2::maker::ETH_MAKER_REFUND_TIMELOCK, + erc20_maker_refund_timelock: gas_limit_v2::maker::ERC20_MAKER_REFUND_TIMELOCK, + eth_maker_refund_secret: gas_limit_v2::maker::ETH_MAKER_REFUND_SECRET, + erc20_maker_refund_secret: gas_limit_v2::maker::ERC20_MAKER_REFUND_SECRET, + } + } +} + +impl Default for TakerGasLimitV2 { + fn default() -> Self { + TakerGasLimitV2 { + eth_payment: gas_limit_v2::taker::ETH_PAYMENT, + erc20_payment: gas_limit_v2::taker::ERC20_PAYMENT, + eth_maker_spend: gas_limit_v2::taker::ETH_MAKER_SPEND, + erc20_maker_spend: gas_limit_v2::taker::ERC20_MAKER_SPEND, + eth_taker_refund_timelock: gas_limit_v2::taker::ETH_TAKER_REFUND_TIMELOCK, + erc20_taker_refund_timelock: gas_limit_v2::taker::ERC20_TAKER_REFUND_TIMELOCK, + eth_taker_refund_secret: gas_limit_v2::taker::ETH_TAKER_REFUND_SECRET, + erc20_taker_refund_secret: gas_limit_v2::taker::ERC20_TAKER_REFUND_SECRET, + approve_payment: gas_limit_v2::taker::APPROVE_PAYMENT, + } + } +} + +impl Default for NftMakerGasLimitV2 { + fn default() -> Self { + NftMakerGasLimitV2 { + erc721_payment: gas_limit_v2::nft_maker::ERC721_PAYMENT, + erc1155_payment: gas_limit_v2::nft_maker::ERC1155_PAYMENT, + erc721_taker_spend: gas_limit_v2::nft_maker::ERC721_TAKER_SPEND, + erc1155_taker_spend: gas_limit_v2::nft_maker::ERC1155_TAKER_SPEND, + erc721_maker_refund_timelock: gas_limit_v2::nft_maker::ERC721_MAKER_REFUND_TIMELOCK, + erc1155_maker_refund_timelock: gas_limit_v2::nft_maker::ERC1155_MAKER_REFUND_TIMELOCK, + erc721_maker_refund_secret: gas_limit_v2::nft_maker::ERC721_MAKER_REFUND_SECRET, + erc1155_maker_refund_secret: gas_limit_v2::nft_maker::ERC1155_MAKER_REFUND_SECRET, + } + } +} + +trait ExtractGasLimit: Default + for<'de> Deserialize<'de> { + fn key() -> &'static str; +} + +impl ExtractGasLimit for EthGasLimit { + fn key() -> &'static str { "gas_limit" } +} + +impl ExtractGasLimit for EthGasLimitV2 { + fn key() -> &'static str { "gas_limit_v2" } +} + /// Max transaction type according to EIP-2718 const ETH_MAX_TX_TYPE: u64 = 0x7f; @@ -673,6 +883,8 @@ pub struct EthCoinImpl { pub(crate) platform_fee_estimator_state: Arc, /// Config provided gas limits for swap and send transactions pub(crate) gas_limit: EthGasLimit, + /// Config provided gas limits v2 for swap v2 transactions + pub(crate) gas_limit_v2: EthGasLimitV2, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, @@ -1755,7 +1967,11 @@ impl WatcherOps for EthCoin { ))); } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported by watchers yet".to_string(), + )) + }, } Ok(()) @@ -1996,7 +2212,11 @@ impl WatcherOps for EthCoin { ))); } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported by watchers yet".to_string(), + )) + }, } Ok(()) @@ -4603,7 +4823,7 @@ impl EthCoin { EthCoinType::Erc20 { token_addr, .. } => token_addr, EthCoinType::Nft { .. } => { return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "Nft Protocol is not supported yet!" + "Nft Protocol is not supported by 'approve'!" ))) }, }; @@ -4943,7 +5163,11 @@ impl EthCoin { ))); } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported by legacy swap".to_string(), + )) + }, } Ok(()) @@ -5881,7 +6105,11 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - }, } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported".to_string(), + )) + }, } Ok(()) @@ -6361,7 +6589,8 @@ pub async fn eth_coin_from_conf_and_request( let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(conf)?; + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(conf)?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(conf)?; let coin = EthCoinImpl { priv_key_policy: key_pair, @@ -6388,6 +6617,7 @@ pub async fn eth_coin_from_conf_and_request( nfts_infos: Default::default(), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; @@ -7013,11 +7243,12 @@ pub fn pubkey_from_extended(extended_pubkey: &Secp256k1ExtendedPublicKey) -> Pub pubkey_uncompressed } -fn extract_gas_limit_from_conf(coin_conf: &Json) -> Result { - if coin_conf["gas_limit"].is_null() { +fn extract_gas_limit_from_conf(coin_conf: &Json) -> Result { + let key = T::key(); + if coin_conf[key].is_null() { Ok(Default::default()) } else { - json::from_value(coin_conf["gas_limit"].clone()).map_err(|e| e.to_string()) + json::from_value(coin_conf[key].clone()).map_err(|e| e.to_string()) } } @@ -7141,7 +7372,7 @@ impl TakerCoinSwapOpsV2 for EthCoin { taker_payment: &Self::Tx, _from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { self.wait_for_taker_payment_spend_impl(taker_payment, wait_until).await } } @@ -7204,8 +7435,38 @@ impl EthCoin { nfts_infos: Arc::clone(&self.nfts_infos), platform_fee_estimator_state: Arc::clone(&self.platform_fee_estimator_state), gas_limit: EthGasLimit::default(), + gas_limit_v2: EthGasLimitV2::default(), abortable_system: self.abortable_system.create_subsystem().unwrap(), }; EthCoin(Arc::new(coin)) } } + +#[async_trait] +impl MakerCoinSwapOpsV2 for EthCoin { + async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result { + self.send_maker_payment_v2_impl(args).await + } + + async fn validate_maker_payment_v2(&self, args: ValidateMakerPaymentArgs<'_, Self>) -> ValidatePaymentResult<()> { + self.validate_maker_payment_v2_impl(args).await + } + + async fn refund_maker_payment_v2_timelock( + &self, + args: RefundMakerPaymentTimelockArgs<'_>, + ) -> Result { + self.refund_maker_payment_v2_timelock_impl(args).await + } + + async fn refund_maker_payment_v2_secret( + &self, + args: RefundMakerPaymentSecretArgs<'_, Self>, + ) -> Result { + self.refund_maker_payment_v2_secret_impl(args).await + } + + async fn spend_maker_payment_v2(&self, args: SpendMakerPaymentArgs<'_, Self>) -> Result { + self.spend_maker_payment_v2_impl(args).await + } +} diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs new file mode 100644 index 0000000000..3089604ede --- /dev/null +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -0,0 +1,486 @@ +use super::{validate_amount, validate_from_to_and_status, EthPaymentType, PaymentMethod, PrepareTxDataError, + ZERO_VALUE}; +use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; +use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType, + MakerPaymentStateV2, SignedEthTx, MAKER_SWAP_V2}; +use crate::{ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SendMakerPaymentArgs, + SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TransactionErr, ValidateMakerPaymentArgs}; +use ethabi::{Function, Token}; +use ethcore_transaction::Action; +use ethereum_types::{Address, Public, U256}; +use ethkey::public_to_address; +use futures::compat::Future01CompatExt; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::MapToMmResult; +use std::convert::TryInto; +use web3::types::TransactionId; + +const ETH_MAKER_PAYMENT: &str = "ethMakerPayment"; +const ERC20_MAKER_PAYMENT: &str = "erc20MakerPayment"; + +/// state index for `MakerPayment` structure from `EtomicSwapMakerV2.sol` +/// +/// struct MakerPayment { +/// bytes20 paymentHash; +/// uint32 paymentLockTime; +/// MakerPaymentState state; +/// } +const MAKER_PAYMENT_STATE_INDEX: usize = 2; + +struct MakerPaymentArgs { + taker_address: Address, + taker_secret_hash: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, +} + +struct MakerValidationArgs<'a> { + swap_id: Vec, + amount: U256, + taker: Address, + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], + payment_time_lock: u64, +} + +struct MakerRefundTimelockArgs { + payment_amount: U256, + taker_address: Address, + taker_secret_hash: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, + token_address: Address, +} + +struct MakerRefundSecretArgs { + payment_amount: U256, + taker_address: Address, + taker_secret: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, + token_address: Address, +} + +impl EthCoin { + pub(crate) async fn send_maker_payment_v2_impl( + &self, + args: SendMakerPaymentArgs<'_, Self>, + ) -> Result { + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); + let payment_args = { + let taker_address = public_to_address(args.taker_pub); + MakerPaymentArgs { + taker_address, + taker_secret_hash: try_tx_s!(args.taker_secret_hash.try_into()), + maker_secret_hash: try_tx_s!(args.maker_secret_hash.try_into()), + payment_time_lock: args.time_lock, + } + }; + match &self.coin_type { + EthCoinType::Eth => { + let data = try_tx_s!(self.prepare_maker_eth_payment_data(&payment_args).await); + self.sign_and_send_transaction( + payment_amount, + Action::Call(maker_swap_v2_contract), + data, + U256::from(self.gas_limit_v2.maker.eth_payment), + ) + .compat() + .await + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + let data = try_tx_s!( + self.prepare_maker_erc20_payment_data(&payment_args, payment_amount, *token_addr) + .await + ); + self.handle_allowance(maker_swap_v2_contract, payment_amount, args.time_lock) + .await?; + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(self.gas_limit_v2.maker.erc20_payment), + ) + .compat() + .await + }, + EthCoinType::Nft { .. } => Err(TransactionErr::ProtocolNotSupported(ERRL!( + "NFT protocol is not supported for ETH and ERC20 Swaps" + ))), + } + } + + pub(crate) async fn validate_maker_payment_v2_impl( + &self, + args: ValidateMakerPaymentArgs<'_, Self>, + ) -> ValidatePaymentResult<()> { + if let EthCoinType::Nft { .. } = self.coin_type { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )); + } + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + ValidatePaymentError::InternalError("Expected swap_v2_contracts to be Some, but found None".to_string()) + })? + .maker_swap_v2_contract; + let taker_secret_hash = args.taker_secret_hash.try_into()?; + let maker_secret_hash = args.maker_secret_hash.try_into()?; + validate_amount(&args.amount).map_to_mm(ValidatePaymentError::InternalError)?; + let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); + let maker_status = self + .payment_status_v2( + maker_swap_v2_contract, + Token::FixedBytes(swap_id.clone()), + &MAKER_SWAP_V2, + EthPaymentType::MakerPayments, + MAKER_PAYMENT_STATE_INDEX, + ) + .await?; + + let tx_from_rpc = self + .transaction(TransactionId::Hash(args.maker_payment_tx.tx_hash())) + .await?; + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!( + "Didn't find provided tx {:?} on ETH node", + args.maker_payment_tx.tx_hash() + )) + })?; + let maker_address = public_to_address(args.maker_pub); + validate_from_to_and_status( + tx_from_rpc, + maker_address, + maker_swap_v2_contract, + maker_status, + MakerPaymentStateV2::PaymentSent as u8, + )?; + + let validation_args = { + let amount = wei_from_big_decimal(&args.amount, self.decimals)?; + MakerValidationArgs { + swap_id, + amount, + taker: self.my_addr().await, + taker_secret_hash, + maker_secret_hash, + payment_time_lock: args.time_lock, + } + }; + match self.coin_type { + EthCoinType::Eth => { + let function = MAKER_SWAP_V2.function(ETH_MAKER_PAYMENT)?; + let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; + validate_eth_maker_payment_data(&decoded, &validation_args, function, tx_from_rpc.value)?; + }, + EthCoinType::Erc20 { token_addr, .. } => { + let function = MAKER_SWAP_V2.function(ERC20_MAKER_PAYMENT)?; + let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; + validate_erc20_maker_payment_data(&decoded, &validation_args, function, token_addr)?; + }, + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )); + }, + } + Ok(()) + } + + pub(crate) async fn refund_maker_payment_v2_timelock_impl( + &self, + args: RefundMakerPaymentTimelockArgs<'_>, + ) -> Result { + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::MakerPayments, + PaymentMethod::RefundTimelock, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + let (maker_secret_hash, taker_secret_hash) = match args.tx_type_with_secret_hash { + SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash, + taker_secret_hash, + } => (maker_secret_hash, taker_secret_hash), + _ => { + return Err(TransactionErr::Plain(ERRL!( + "Unsupported swap tx type for timelock refund" + ))) + }, + }; + let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); + + let args = { + let taker_address = public_to_address(&Public::from_slice(args.taker_pub)); + MakerRefundTimelockArgs { + payment_amount, + taker_address, + taker_secret_hash: try_tx_s!(taker_secret_hash.try_into()), + maker_secret_hash: try_tx_s!(maker_secret_hash.try_into()), + payment_time_lock: args.time_lock, + token_address, + } + }; + let data = try_tx_s!(self.prepare_refund_maker_payment_timelock_data(args).await); + + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(gas_limit), + ) + .compat() + .await + } + + pub(crate) async fn refund_maker_payment_v2_secret_impl( + &self, + args: RefundMakerPaymentSecretArgs<'_, Self>, + ) -> Result { + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::MakerPayments, + PaymentMethod::RefundSecret, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + let taker_secret = try_tx_s!(args.taker_secret.try_into()); + let maker_secret_hash = try_tx_s!(args.maker_secret_hash.try_into()); + let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); + let args = { + let taker_address = public_to_address(args.taker_pub); + MakerRefundSecretArgs { + payment_amount, + taker_address, + taker_secret, + maker_secret_hash, + payment_time_lock: args.time_lock, + token_address, + } + }; + let data = try_tx_s!(self.prepare_refund_maker_payment_secret_data(args).await); + + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(gas_limit), + ) + .compat() + .await + } + + pub(crate) async fn spend_maker_payment_v2_impl( + &self, + args: SpendMakerPaymentArgs<'_, Self>, + ) -> Result { + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Spend) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + let data = try_tx_s!(self.prepare_spend_maker_payment_data(args, token_address).await); + + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(gas_limit), + ) + .compat() + .await + } + + /// Prepares data for EtomicSwapMakerV2 contract [ethMakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L30) method + async fn prepare_maker_eth_payment_data(&self, args: &MakerPaymentArgs) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function(ETH_MAKER_PAYMENT)?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Uint(args.payment_time_lock.into()), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [erc20MakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L64) method + async fn prepare_maker_erc20_payment_data( + &self, + args: &MakerPaymentArgs, + payment_amount: U256, + token_address: Address, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function(ERC20_MAKER_PAYMENT)?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(payment_amount), + Token::Address(token_address), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Uint(args.payment_time_lock.into()), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [refundMakerPaymentTimelock](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L144) method + async fn prepare_refund_maker_payment_timelock_data( + &self, + args: MakerRefundTimelockArgs, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function("refundMakerPaymentTimelock")?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(args.payment_amount), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Address(args.token_address), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [refundMakerPaymentSecret](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L190) method + async fn prepare_refund_maker_payment_secret_data( + &self, + args: MakerRefundSecretArgs, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function("refundMakerPaymentSecret")?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(args.payment_amount), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Address(args.token_address), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [spendMakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L104) method + async fn prepare_spend_maker_payment_data( + &self, + args: SpendMakerPaymentArgs<'_, Self>, + token_address: Address, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function("spendMakerPayment")?; + let id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); + let maker_address = public_to_address(args.maker_pub); + let payment_amount = wei_from_big_decimal(&args.amount, self.decimals) + .map_err(|e| PrepareTxDataError::Internal(e.to_string()))?; + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(payment_amount), + Token::Address(maker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret.to_vec()), + Token::Address(token_address), + ])?; + Ok(data) + } +} + +/// Validation function for ETH maker payment data +fn validate_eth_maker_payment_data( + decoded: &[Token], + args: &MakerValidationArgs, + func: &Function, + tx_value: U256, +) -> Result<(), MmError> { + let checks = vec![ + (0, Token::FixedBytes(args.swap_id.clone()), "id"), + (1, Token::Address(args.taker), "taker"), + (2, Token::FixedBytes(args.taker_secret_hash.to_vec()), "takerSecretHash"), + (3, Token::FixedBytes(args.maker_secret_hash.to_vec()), "makerSecretHash"), + (4, Token::Uint(U256::from(args.payment_time_lock)), "paymentLockTime"), + ]; + + for (index, expected_token, field_name) in checks { + let token = get_function_input_data(decoded, func, index).map_to_mm(ValidatePaymentError::InternalError)?; + if token != expected_token { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ETH Maker Payment `{}` {:?} is invalid, expected {:?}", + field_name, + decoded.get(index), + expected_token + ))); + } + } + if args.amount != tx_value { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ETH Maker Payment amount, is invalid, expected {:?}, got {:?}", + args.amount, tx_value + ))); + } + Ok(()) +} + +/// Validation function for ERC20 maker payment data +fn validate_erc20_maker_payment_data( + decoded: &[Token], + args: &MakerValidationArgs, + func: &Function, + token_addr: Address, +) -> Result<(), MmError> { + let checks = vec![ + (0, Token::FixedBytes(args.swap_id.clone()), "id"), + (1, Token::Uint(args.amount), "amount"), + (2, Token::Address(token_addr), "tokenAddress"), + (3, Token::Address(args.taker), "taker"), + (4, Token::FixedBytes(args.taker_secret_hash.to_vec()), "takerSecretHash"), + (5, Token::FixedBytes(args.maker_secret_hash.to_vec()), "makerSecretHash"), + (6, Token::Uint(U256::from(args.payment_time_lock)), "paymentLockTime"), + ]; + + for (index, expected_token, field_name) in checks { + let token = get_function_input_data(decoded, func, index).map_to_mm(ValidatePaymentError::InternalError)?; + if token != expected_token { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ERC20 Maker Payment `{}` {:?} is invalid, expected {:?}", + field_name, + decoded.get(index), + expected_token + ))); + } + } + Ok(()) +} diff --git a/mm2src/coins/eth/eth_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs similarity index 71% rename from mm2src/coins/eth/eth_swap_v2.rs rename to mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs index 0d61ac6004..fea91b0408 100644 --- a/mm2src/coins/eth/eth_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs @@ -1,31 +1,36 @@ -use super::eth::{wei_from_big_decimal, EthCoin, EthCoinType, SignedEthTx, TAKER_SWAP_V2}; -use super::{decode_contract_call, get_function_input_data, ParseCoinAssocTypes, RefundFundingSecretArgs, - RefundTakerPaymentArgs, SendTakerFundingArgs, SwapTxTypeWithSecretHash, TakerPaymentStateV2, Transaction, - TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs}; +use super::{check_decoded_length, validate_amount, validate_from_to_and_status, validate_payment_state, + EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; +use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType, + ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, + SignedEthTx, SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, TAKER_SWAP_V2}; use crate::{FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, SearchForFundingSpendErr, - WaitForTakerPaymentSpendError}; + WaitForPaymentSpendError}; use common::executor::Timer; use common::now_sec; -use enum_derives::EnumFromStringify; -use ethabi::{Contract, Function, Token}; +use ethabi::{Function, Token}; use ethcore_transaction::Action; use ethereum_types::{Address, Public, U256}; use ethkey::public_to_address; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; -use mm2_number::BigDecimal; use std::convert::TryInto; -use web3::types::{Transaction as Web3Tx, TransactionId}; +use web3::types::TransactionId; -/// ZERO_VALUE is used to represent a 0 amount in transactions where the value is encoded in the transaction input data. -/// This is typically used in function calls where the value is not directly transferred with the transaction, such as in -/// `spendTakerPayment` where the [amount](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L166) -/// is provided as part of the input data rather than as an Ether value -pub(crate) const ZERO_VALUE: u32 = 0; const ETH_TAKER_PAYMENT: &str = "ethTakerPayment"; const ERC20_TAKER_PAYMENT: &str = "erc20TakerPayment"; const TAKER_PAYMENT_APPROVE: &str = "takerPaymentApprove"; +/// state index for `TakerPayment` structure from `EtomicSwapTakerV2.sol` +/// +/// struct TakerPayment { +/// bytes20 paymentHash; +/// uint32 preApproveLockTime; +/// uint32 paymentLockTime; +/// TakerPaymentState state; +/// } +const TAKER_PAYMENT_STATE_INDEX: usize = 3; + struct TakerFundingArgs { dex_fee: U256, payment_amount: U256, @@ -36,17 +41,37 @@ struct TakerFundingArgs { payment_time_lock: u64, } -struct TakerRefundArgs { +struct TakerRefundTimelockArgs { dex_fee: U256, payment_amount: U256, maker_address: Address, - taker_secret: [u8; 32], taker_secret_hash: [u8; 32], maker_secret_hash: [u8; 32], payment_time_lock: u64, token_address: Address, } +struct TakerRefundSecretArgs { + dex_fee: U256, + payment_amount: U256, + maker_address: Address, + taker_secret: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, + token_address: Address, +} + +struct TakerValidationArgs<'a> { + swap_id: Vec, + amount: U256, + dex_fee: U256, + receiver: Address, + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], + funding_time_lock: u64, + payment_time_lock: u64, +} + impl EthCoin { /// Calls `"ethTakerPayment"` or `"erc20TakerPayment"` swap contract methods. /// Returns taker sent payment transaction. @@ -56,9 +81,8 @@ impl EthCoin { ) -> Result { let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) - .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))?; + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .taker_swap_v2_contract; // TODO add burnFee support let dex_fee = try_tx_s!(wei_from_big_decimal(&args.dex_fee.fee_amount().into(), self.decimals)); @@ -88,8 +112,7 @@ impl EthCoin { eth_total_payment, Action::Call(taker_swap_v2_contract), data, - // TODO need new consts and params for v2 calls. now it uses v1 - U256::from(self.gas_limit.eth_payment), + U256::from(self.gas_limit_v2.taker.eth_payment), ) .compat() .await @@ -98,31 +121,14 @@ impl EthCoin { platform: _, token_addr, } => { - let allowed = self - .allowance(taker_swap_v2_contract) - .compat() - .await - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let data = try_tx_s!(self.prepare_taker_erc20_funding_data(&funding_args, *token_addr).await); - if allowed < payment_amount { - let approved_tx = self.approve(taker_swap_v2_contract, U256::max_value()).compat().await?; - self.wait_for_required_allowance(taker_swap_v2_contract, payment_amount, args.funding_time_lock) - .compat() - .await - .map_err(|e| { - TransactionErr::Plain(ERRL!( - "Allowed value was not updated in time after sending approve transaction {:02x}: {}", - approved_tx.tx_hash_as_bytes(), - e - )) - })?; - } + self.handle_allowance(taker_swap_v2_contract, payment_amount, args.funding_time_lock) + .await?; self.sign_and_send_transaction( U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - // TODO need new consts and params for v2 calls. now it uses v1 - U256::from(self.gas_limit.erc20_payment), + U256::from(self.gas_limit_v2.taker.erc20_payment), ) .compat() .await @@ -144,14 +150,13 @@ impl EthCoin { } let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) .ok_or_else(|| { ValidateSwapV2TxError::Internal("Expected swap_v2_contracts to be Some, but found None".to_string()) - })?; - validate_payment_args(args.taker_secret_hash, args.maker_secret_hash, &args.trading_amount) - .map_err(ValidateSwapV2TxError::Internal)?; - let taker_address = public_to_address(args.taker_pub); + })? + .taker_swap_v2_contract; + let taker_secret_hash = args.taker_secret_hash.try_into()?; + let maker_secret_hash = args.maker_secret_hash.try_into()?; + validate_amount(&args.trading_amount).map_err(ValidateSwapV2TxError::Internal)?; let swap_id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let taker_status = self .payment_status_v2( @@ -159,7 +164,7 @@ impl EthCoin { Token::FixedBytes(swap_id.clone()), &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await?; @@ -170,6 +175,7 @@ impl EthCoin { args.funding_tx.tx_hash() )) })?; + let taker_address = public_to_address(args.taker_pub); validate_from_to_and_status( tx_from_rpc, taker_address, @@ -186,8 +192,8 @@ impl EthCoin { amount: payment_amount, dex_fee, receiver: self.my_addr().await, - taker_secret_hash: args.taker_secret_hash, - maker_secret_hash: args.maker_secret_hash, + taker_secret_hash, + maker_secret_hash, funding_time_lock: args.funding_time_lock, payment_time_lock: args.payment_time_lock, } @@ -203,7 +209,11 @@ impl EthCoin { let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; validate_erc20_taker_payment_data(&decoded, &validation_args, function, token_addr)?; }, - EthCoinType::Nft { .. } => unreachable!(), + EthCoinType::Nft { .. } => { + return MmError::err(ValidateSwapV2TxError::ProtocolNotSupported( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )); + }, } Ok(()) } @@ -214,9 +224,8 @@ impl EthCoin { &self, args: &GenTakerFundingSpendArgs<'_, Self>, ) -> Result { - // TODO need new consts and params for v2 calls, here should be common `gas_limit.taker_approve` param for Eth and Erc20 let gas_limit = match self.coin_type { - EthCoinType::Eth | EthCoinType::Erc20 { .. } => U256::from(self.gas_limit.eth_payment), + EthCoinType::Eth | EthCoinType::Erc20 { .. } => U256::from(self.gas_limit_v2.taker.approve_payment), EthCoinType::Nft { .. } => { return Err(TransactionErr::ProtocolNotSupported(ERRL!( "NFT protocol is not supported for ETH and ERC20 Swaps" @@ -233,7 +242,7 @@ impl EthCoin { decoded[0].clone(), &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await ); @@ -259,34 +268,22 @@ impl EthCoin { &self, args: RefundTakerPaymentArgs<'_>, ) -> Result { - let (token_address, gas_limit) = match &self.coin_type { - // TODO need new consts and params for v2 calls - EthCoinType::Eth => (Address::default(), self.gas_limit.eth_sender_refund), - EthCoinType::Erc20 { - platform: _, - token_addr, - } => (*token_addr, self.gas_limit.erc20_sender_refund), - EthCoinType::Nft { .. } => { - return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "NFT protocol is not supported for ETH and ERC20 Swaps" - ))) - }, - }; - + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) - .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))?; - let dex_fee = try_tx_s!(wei_from_big_decimal( - &args.dex_fee.fee_amount().to_decimal(), - self.decimals - )); - let payment_amount = try_tx_s!(wei_from_big_decimal( - &(args.trading_amount + args.premium_amount), - self.decimals - )); - let maker_address = public_to_address(&Public::from_slice(args.maker_pub)); + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .taker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::TakerPayments, + PaymentMethod::RefundTimelock, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let (maker_secret_hash, taker_secret_hash) = match args.tx_type_with_secret_hash { SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash, @@ -298,16 +295,26 @@ impl EthCoin { ))) }, }; + let dex_fee = try_tx_s!(wei_from_big_decimal( + &args.dex_fee.fee_amount().to_decimal(), + self.decimals + )); + let payment_amount = try_tx_s!(wei_from_big_decimal( + &(args.trading_amount + args.premium_amount), + self.decimals + )); - let args = TakerRefundArgs { - dex_fee, - payment_amount, - maker_address, - taker_secret: [0u8; 32], - taker_secret_hash: try_tx_s!(taker_secret_hash.try_into()), - maker_secret_hash: try_tx_s!(maker_secret_hash.try_into()), - payment_time_lock: args.time_lock, - token_address, + let args = { + let maker_address = public_to_address(&Public::from_slice(args.maker_pub)); + TakerRefundTimelockArgs { + dex_fee, + payment_amount, + maker_address, + taker_secret_hash: try_tx_s!(taker_secret_hash.try_into()), + maker_secret_hash: try_tx_s!(maker_secret_hash.try_into()), + payment_time_lock: args.time_lock, + token_address, + } }; let data = try_tx_s!(self.prepare_taker_refund_payment_timelock_data(args).await); @@ -325,25 +332,22 @@ impl EthCoin { &self, args: RefundFundingSecretArgs<'_, Self>, ) -> Result { - let (token_address, gas_limit) = match &self.coin_type { - // TODO need new consts and params for v2 calls - EthCoinType::Eth => (Address::default(), self.gas_limit.eth_sender_refund), - EthCoinType::Erc20 { - platform: _, - token_addr, - } => (*token_addr, self.gas_limit.erc20_sender_refund), - EthCoinType::Nft { .. } => { - return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "NFT protocol is not supported for ETH and ERC20 Swaps" - ))) - }, - }; - + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) - .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))?; + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .taker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::TakerPayments, + PaymentMethod::RefundSecret, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let taker_secret = try_tx_s!(args.taker_secret.try_into()); let maker_secret_hash = try_tx_s!(args.maker_secret_hash.try_into()); let dex_fee = try_tx_s!(wei_from_big_decimal( @@ -354,17 +358,18 @@ impl EthCoin { &(args.trading_amount + args.premium_amount), self.decimals )); - let maker_address = public_to_address(args.maker_pubkey); - - let refund_args = TakerRefundArgs { - dex_fee, - payment_amount, - maker_address, - taker_secret, - taker_secret_hash: [0u8; 32], - maker_secret_hash, - payment_time_lock: args.payment_time_lock, - token_address, + + let refund_args = { + let maker_address = public_to_address(args.maker_pubkey); + TakerRefundSecretArgs { + dex_fee, + payment_amount, + maker_address, + taker_secret, + maker_secret_hash, + payment_time_lock: args.payment_time_lock, + token_address, + } }; let data = try_tx_s!(self.prepare_taker_refund_payment_secret_data(&refund_args).await); @@ -394,7 +399,7 @@ impl EthCoin { decoded[0].clone(), // id from takerPaymentApprove &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await .map_err(|e| SearchForFundingSpendErr::Internal(ERRL!("{}", e)))?; @@ -411,16 +416,11 @@ impl EthCoin { gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], ) -> Result { - // TODO need new consts and params for v2 calls - let gas_limit = match self.coin_type { - EthCoinType::Eth => U256::from(self.gas_limit.eth_receiver_spend), - EthCoinType::Erc20 { .. } => U256::from(self.gas_limit.erc20_receiver_spend), - EthCoinType::Nft { .. } => { - return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "NFT protocol is not supported for ETH and ERC20 Swaps" - ))) - }, - }; + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::TakerPayments, PaymentMethod::Spend) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let (taker_swap_v2_contract, approve_func, token_address) = self .taker_swap_v2_details(TAKER_PAYMENT_APPROVE, TAKER_PAYMENT_APPROVE) .await?; @@ -431,7 +431,7 @@ impl EthCoin { decoded[0].clone(), &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await ); @@ -450,7 +450,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - gas_limit, + U256::from(gas_limit), ) .compat() .await?; @@ -463,7 +463,7 @@ impl EthCoin { &self, taker_payment: &SignedEthTx, wait_until: u64, - ) -> MmResult { + ) -> MmResult { let (decoded, taker_swap_v2_contract) = self .get_decoded_and_swap_contract(taker_payment, "spendTakerPayment") .await?; @@ -474,7 +474,7 @@ impl EthCoin { decoded[0].clone(), // id from spendTakerPayment &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await?; if taker_status == U256::from(TakerPaymentStateV2::MakerSpent as u8) { @@ -482,7 +482,7 @@ impl EthCoin { } let now = now_sec(); if now > wait_until { - return MmError::err(WaitForTakerPaymentSpendError::Timeout { wait_until, now }); + return MmError::err(WaitForPaymentSpendError::Timeout { wait_until, now }); } Timer::sleep(10.).await; } @@ -529,7 +529,7 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [refundTakerPaymentTimelock](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L208) method async fn prepare_taker_refund_payment_timelock_data( &self, - args: TakerRefundArgs, + args: TakerRefundTimelockArgs, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function("refundTakerPaymentTimelock")?; let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); @@ -548,7 +548,7 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [refundTakerPaymentSecret](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L267) method async fn prepare_taker_refund_payment_secret_data( &self, - args: &TakerRefundArgs, + args: &TakerRefundSecretArgs, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function("refundTakerPaymentSecret")?; let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); @@ -640,38 +640,6 @@ impl EthCoin { Ok(data) } - /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. - pub(crate) async fn payment_status_v2( - &self, - swap_address: Address, - swap_id: Token, - contract_abi: &Contract, - payment_type: EthPaymentType, - state_index: usize, - ) -> Result { - let function_name = payment_type.as_str(); - let function = contract_abi.function(function_name)?; - let data = function.encode_input(&[swap_id])?; - let bytes = self - .call_request(self.my_addr().await, swap_address, None, Some(data.into())) - .await?; - let decoded_tokens = function.decode_output(&bytes.0)?; - - let state = decoded_tokens.get(state_index).ok_or_else(|| { - PaymentStatusErr::Internal(format!( - "Payment status must contain 'state' as the {} token", - state_index - )) - })?; - match state { - Token::Uint(state) => Ok(*state), - _ => Err(PaymentStatusErr::InvalidData(format!( - "Payment status must be Uint, got {:?}", - state - ))), - } - } - /// Retrieves the taker smart contract address, the corresponding function, and the token address. /// /// Depending on the coin type (ETH or ERC20), it fetches the appropriate function name and token address. @@ -726,77 +694,6 @@ impl EthCoin { } } -#[derive(Debug, Display, EnumFromStringify)] -pub(crate) enum PrepareTxDataError { - #[from_stringify("ethabi::Error")] - #[display(fmt = "ABI error: {}", _0)] - ABIError(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -// TODO validate premium when add its support in swap_v2 -fn validate_payment_args<'a>( - taker_secret_hash: &'a [u8], - maker_secret_hash: &'a [u8], - trading_amount: &BigDecimal, -) -> Result<(), String> { - if !is_positive(trading_amount) { - return Err("trading_amount must be a positive value".to_string()); - } - if taker_secret_hash.len() != 32 { - return Err("taker_secret_hash must be 32 bytes".to_string()); - } - if maker_secret_hash.len() != 32 { - return Err("maker_secret_hash must be 32 bytes".to_string()); - } - Ok(()) -} - -/// function to check if BigDecimal is a positive value -#[inline(always)] -fn is_positive(amount: &BigDecimal) -> bool { amount > &BigDecimal::from(0) } - -pub(crate) fn validate_from_to_and_status( - tx_from_rpc: &Web3Tx, - expected_from: Address, - expected_to: Address, - status: U256, - expected_status: u8, -) -> Result<(), MmError> { - if status != U256::from(expected_status) { - return MmError::err(ValidatePaymentV2Err::UnexpectedPaymentState(format!( - "Payment state is not `PaymentSent`, got {}", - status - ))); - } - if tx_from_rpc.from != Some(expected_from) { - return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {:?} was sent from wrong address, expected {:?}", - tx_from_rpc, expected_from - ))); - } - // (in NFT case) as NFT owner calls "safeTransferFrom" directly, then in Transaction 'to' field we expect token_address - if tx_from_rpc.to != Some(expected_to) { - return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {:?} was sent to wrong address, expected {:?}", - tx_from_rpc, expected_to, - ))); - } - Ok(()) -} - -struct TakerValidationArgs<'a> { - swap_id: Vec, - amount: U256, - dex_fee: U256, - receiver: Address, - taker_secret_hash: &'a [u8], - maker_secret_hash: &'a [u8], - funding_time_lock: u64, - payment_time_lock: u64, -} - /// Validation function for ETH taker payment data fn validate_eth_taker_payment_data( decoded: &[Token], @@ -869,62 +766,3 @@ fn validate_erc20_taker_payment_data( } Ok(()) } - -pub(crate) fn validate_payment_state( - tx: &SignedEthTx, - state: U256, - expected_state: u8, -) -> Result<(), PrepareTxDataError> { - if state != U256::from(expected_state) { - return Err(PrepareTxDataError::Internal(format!( - "Payment {:?} state is not `{}`, got `{}`", - tx, expected_state, state - ))); - } - Ok(()) -} - -#[derive(Debug, Display)] -pub(crate) enum ValidatePaymentV2Err { - UnexpectedPaymentState(String), - WrongPaymentTx(String), -} - -pub(crate) enum EthPaymentType { - MakerPayments, - TakerPayments, -} - -impl EthPaymentType { - pub(crate) fn as_str(&self) -> &'static str { - match self { - EthPaymentType::MakerPayments => "makerPayments", - EthPaymentType::TakerPayments => "takerPayments", - } - } -} - -#[derive(Debug, Display, EnumFromStringify)] -pub(crate) enum PaymentStatusErr { - #[from_stringify("ethabi::Error")] - #[display(fmt = "ABI error: {}", _0)] - ABIError(String), - #[from_stringify("web3::Error")] - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), - #[display(fmt = "Invalid data error: {}", _0)] - InvalidData(String), -} - -fn check_decoded_length(decoded: &Vec, expected_len: usize) -> Result<(), PrepareTxDataError> { - if decoded.len() != expected_len { - return Err(PrepareTxDataError::Internal(format!( - "Invalid number of tokens in decoded. Expected {}, found {}", - expected_len, - decoded.len() - ))); - } - Ok(()) -} diff --git a/mm2src/coins/eth/eth_swap_v2/mod.rs b/mm2src/coins/eth/eth_swap_v2/mod.rs new file mode 100644 index 0000000000..798a232d56 --- /dev/null +++ b/mm2src/coins/eth/eth_swap_v2/mod.rs @@ -0,0 +1,203 @@ +use crate::eth::{EthCoin, EthCoinType, ParseCoinAssocTypes, Transaction, TransactionErr}; +use enum_derives::EnumFromStringify; +use ethabi::{Contract, Token}; +use ethcore_transaction::SignedTransaction as SignedEthTx; +use ethereum_types::{Address, U256}; +use futures::compat::Future01CompatExt; +use mm2_err_handle::mm_error::MmError; +use mm2_number::BigDecimal; +use num_traits::Signed; +use web3::types::Transaction as Web3Tx; + +pub(crate) mod eth_maker_swap_v2; +pub(crate) mod eth_taker_swap_v2; + +/// ZERO_VALUE is used to represent a 0 amount in transactions where the value is encoded in the transaction input data. +/// This is typically used in function calls where the value is not directly transferred with the transaction, such as in +/// `spendTakerPayment` where the [amount](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L166) +/// is provided as part of the input data rather than as an Ether value +pub(crate) const ZERO_VALUE: u32 = 0; + +pub enum EthPaymentType { + MakerPayments, + TakerPayments, +} + +impl EthPaymentType { + pub(crate) fn as_str(&self) -> &'static str { + match self { + EthPaymentType::MakerPayments => "makerPayments", + EthPaymentType::TakerPayments => "takerPayments", + } + } +} + +pub enum PaymentMethod { + Send, + Spend, + RefundTimelock, + RefundSecret, +} + +#[derive(Debug, Display)] +pub(crate) enum ValidatePaymentV2Err { + UnexpectedPaymentState(String), + WrongPaymentTx(String), +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum PaymentStatusErr { + #[from_stringify("ethabi::Error")] + #[display(fmt = "ABI error: {}", _0)] + ABIError(String), + #[from_stringify("web3::Error")] + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Invalid data error: {}", _0)] + InvalidData(String), +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum PrepareTxDataError { + #[from_stringify("ethabi::Error")] + #[display(fmt = "ABI error: {}", _0)] + ABIError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl EthCoin { + /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. + pub(crate) async fn payment_status_v2( + &self, + swap_address: Address, + swap_id: Token, + contract_abi: &Contract, + payment_type: EthPaymentType, + state_index: usize, + ) -> Result { + let function_name = payment_type.as_str(); + let function = contract_abi.function(function_name)?; + let data = function.encode_input(&[swap_id])?; + let bytes = self + .call_request(self.my_addr().await, swap_address, None, Some(data.into())) + .await?; + let decoded_tokens = function.decode_output(&bytes.0)?; + + let state = decoded_tokens.get(state_index).ok_or_else(|| { + PaymentStatusErr::Internal(format!( + "Payment status must contain 'state' as the {} token", + state_index + )) + })?; + match state { + Token::Uint(state) => Ok(*state), + _ => Err(PaymentStatusErr::InvalidData(format!( + "Payment status must be Uint, got {:?}", + state + ))), + } + } + + pub(super) fn get_token_address(&self) -> Result { + match &self.coin_type { + EthCoinType::Eth => Ok(Address::default()), + EthCoinType::Erc20 { token_addr, .. } => Ok(*token_addr), + EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()), + } + } +} + +pub(crate) fn validate_payment_state( + tx: &SignedEthTx, + state: U256, + expected_state: u8, +) -> Result<(), PrepareTxDataError> { + if state != U256::from(expected_state) { + return Err(PrepareTxDataError::Internal(format!( + "Payment {:?} state is not `{}`, got `{}`", + tx, expected_state, state + ))); + } + Ok(()) +} + +pub(crate) fn validate_from_to_and_status( + tx_from_rpc: &Web3Tx, + expected_from: Address, + expected_to: Address, + status: U256, + expected_status: u8, +) -> Result<(), MmError> { + if status != U256::from(expected_status) { + return MmError::err(ValidatePaymentV2Err::UnexpectedPaymentState(format!( + "Payment state is not `PaymentSent`, got {}", + status + ))); + } + if tx_from_rpc.from != Some(expected_from) { + return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( + "Payment tx {:?} was sent from wrong address, expected {:?}", + tx_from_rpc, expected_from + ))); + } + // (in NFT case) as NFT owner calls "safeTransferFrom" directly, then in Transaction 'to' field we expect token_address + if tx_from_rpc.to != Some(expected_to) { + return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( + "Payment tx {:?} was sent to wrong address, expected {:?}", + tx_from_rpc, expected_to, + ))); + } + Ok(()) +} + +// TODO validate premium when add its support in swap_v2 +fn validate_amount(trading_amount: &BigDecimal) -> Result<(), String> { + if !trading_amount.is_positive() { + return Err("trading_amount must be a positive value".to_string()); + } + Ok(()) +} + +fn check_decoded_length(decoded: &Vec, expected_len: usize) -> Result<(), PrepareTxDataError> { + if decoded.len() != expected_len { + return Err(PrepareTxDataError::Internal(format!( + "Invalid number of tokens in decoded. Expected {}, found {}", + expected_len, + decoded.len() + ))); + } + Ok(()) +} + +impl EthCoin { + async fn handle_allowance( + &self, + swap_contract: Address, + payment_amount: U256, + time_lock: u64, + ) -> Result<(), TransactionErr> { + let allowed = self + .allowance(swap_contract) + .compat() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + if allowed < payment_amount { + let approved_tx = self.approve(swap_contract, U256::max_value()).compat().await?; + self.wait_for_required_allowance(swap_contract, payment_amount, time_lock) + .compat() + .await + .map_err(|e| { + TransactionErr::Plain(ERRL!( + "Allowed value was not updated in time after sending approve transaction {:02x}: {}", + approved_tx.tx_hash_as_bytes(), + e + )) + })?; + } + Ok(()) + } +} diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index d3b8ece3ac..cc6d5cd375 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -50,7 +50,8 @@ pub(crate) fn eth_coin_from_keypair( }; let my_address = key_pair.address(); let coin_conf = coin_conf(&ctx, &ticker); - let gas_limit = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); let eth_coin = EthCoin(Arc::new(EthCoinImpl { coin_type, @@ -77,6 +78,7 @@ pub(crate) fn eth_coin_from_keypair( nfts_infos: Arc::new(Default::default()), platform_fee_estimator_state: Arc::new(FeeEstimatorState::CoinNotSupported), gas_limit, + gas_limit_v2, abortable_system: AbortableQueue::default(), })); (ctx, eth_coin) diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs index d5de6dcc86..f4c909bd32 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -1,4 +1,3 @@ -use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use ethabi::{Contract, Token}; use ethcore_transaction::Action; use ethereum_types::{Address, U256}; @@ -6,21 +5,23 @@ use ethkey::public_to_address; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; use mm2_number::BigDecimal; +use num_traits::Signed; use web3::types::TransactionId; -pub(crate) mod errors; -use errors::{Erc721FunctionError, HtlcParamsError, PrepareTxDataError}; -mod structs; -use structs::{ExpectedHtlcParams, ValidationParams}; - use super::ContractType; -use crate::eth::eth_swap_v2::{validate_from_to_and_status, validate_payment_state, EthPaymentType, PaymentStatusErr, - ZERO_VALUE}; +use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; +use crate::eth::eth_swap_v2::{validate_from_to_and_status, validate_payment_state, EthPaymentType, PaymentMethod, + PaymentStatusErr, PrepareTxDataError, ZERO_VALUE}; use crate::eth::{decode_contract_call, EthCoin, EthCoinType, MakerPaymentStateV2, SignedEthTx, ERC1155_CONTRACT, ERC721_CONTRACT, NFT_MAKER_SWAP_V2}; use crate::{ParseCoinAssocTypes, RefundNftMakerPaymentArgs, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, TransactionErr, ValidateNftMakerPaymentArgs}; +pub(crate) mod errors; +use errors::{Erc721FunctionError, HtlcParamsError}; +mod structs; +use structs::{ExpectedHtlcParams, ValidationParams}; + impl EthCoin { pub(crate) async fn send_nft_maker_payment_v2_impl( &self, @@ -37,11 +38,14 @@ impl EthCoin { let htlc_data = try_tx_s!(self.prepare_htlc_data(&args)); let data = try_tx_s!(self.prepare_nft_maker_payment_v2_data(&args, htlc_data).await); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.nft_swap_info.contract_type, PaymentMethod::Send); self.sign_and_send_transaction( ZERO_VALUE.into(), Action::Call(*args.nft_swap_info.token_address), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -58,6 +62,14 @@ impl EthCoin { ) -> ValidatePaymentResult<()> { match self.coin_type { EthCoinType::Nft { .. } => { + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + ValidatePaymentError::InternalError( + "Expected swap_v2_contracts to be Some, but found None".to_string(), + ) + })? + .nft_maker_swap_v2_contract; let contract_type = args.nft_swap_info.contract_type; validate_payment_args( args.taker_secret_hash, @@ -66,14 +78,12 @@ impl EthCoin { contract_type, ) .map_err(ValidatePaymentError::InternalError)?; - // TODO use swap contract address from self - let etomic_swap_contract = args.nft_swap_info.swap_contract_address; let token_address = args.nft_swap_info.token_address; let maker_address = public_to_address(args.maker_pub); let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); let maker_status = self .payment_status_v2( - *etomic_swap_contract, + nft_maker_swap_v2_contract, Token::FixedBytes(swap_id.clone()), &NFT_MAKER_SWAP_V2, EthPaymentType::MakerPayments, @@ -107,7 +117,7 @@ impl EthCoin { let validation_params = ValidationParams { maker_address, - etomic_swap_contract: *etomic_swap_contract, + nft_maker_swap_v2_contract, token_id: args.nft_swap_info.token_id, amount, }; @@ -139,7 +149,12 @@ impl EthCoin { ) -> Result { match self.coin_type { EthCoinType::Nft { .. } => { - let etomic_swap_contract = args.swap_contract_address; + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")) + })? + .nft_maker_swap_v2_contract; if args.maker_secret.len() != 32 { return Err(TransactionErr::Plain(ERRL!("maker_secret must be 32 bytes"))); } @@ -150,7 +165,7 @@ impl EthCoin { let (state, htlc_params) = try_tx_s!( self.status_and_htlc_params_from_tx_data( - *etomic_swap_contract, + nft_maker_swap_v2_contract, &NFT_MAKER_SWAP_V2, &decoded, bytes_index, @@ -160,11 +175,14 @@ impl EthCoin { .await ); let data = try_tx_s!(self.prepare_spend_nft_maker_v2_data(&args, decoded, htlc_params, state)); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.contract_type, PaymentMethod::Spend); self.sign_and_send_transaction( ZERO_VALUE.into(), - Action::Call(*etomic_swap_contract), + Action::Call(nft_maker_swap_v2_contract), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -181,7 +199,12 @@ impl EthCoin { ) -> Result { match self.coin_type { EthCoinType::Nft { .. } => { - let etomic_swap_contract = args.swap_contract_address; + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")) + })? + .nft_maker_swap_v2_contract; let (decoded, bytes_index) = try_tx_s!(get_decoded_tx_data_and_bytes_index( args.contract_type, args.maker_payment_tx.unsigned().data() @@ -189,7 +212,7 @@ impl EthCoin { let (state, htlc_params) = try_tx_s!( self.status_and_htlc_params_from_tx_data( - *etomic_swap_contract, + nft_maker_swap_v2_contract, &NFT_MAKER_SWAP_V2, &decoded, bytes_index, @@ -200,11 +223,14 @@ impl EthCoin { ); let data = try_tx_s!(self.prepare_refund_nft_maker_payment_v2_timelock(&args, decoded, htlc_params, state)); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.contract_type, PaymentMethod::RefundTimelock); self.sign_and_send_transaction( ZERO_VALUE.into(), - Action::Call(*etomic_swap_contract), + Action::Call(nft_maker_swap_v2_contract), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -221,7 +247,12 @@ impl EthCoin { ) -> Result { match self.coin_type { EthCoinType::Nft { .. } => { - let etomic_swap_contract = args.swap_contract_address; + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")) + })? + .nft_maker_swap_v2_contract; let (decoded, bytes_index) = try_tx_s!(get_decoded_tx_data_and_bytes_index( args.contract_type, args.maker_payment_tx.unsigned().data() @@ -229,7 +260,7 @@ impl EthCoin { let (state, htlc_params) = try_tx_s!( self.status_and_htlc_params_from_tx_data( - *etomic_swap_contract, + nft_maker_swap_v2_contract, &NFT_MAKER_SWAP_V2, &decoded, bytes_index, @@ -241,11 +272,14 @@ impl EthCoin { let data = try_tx_s!(self.prepare_refund_nft_maker_payment_v2_secret(&args, decoded, htlc_params, state)); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.contract_type, PaymentMethod::RefundSecret); self.sign_and_send_transaction( ZERO_VALUE.into(), - Action::Call(*etomic_swap_contract), + Action::Call(nft_maker_swap_v2_contract), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -261,6 +295,12 @@ impl EthCoin { args: &SendNftMakerPaymentArgs<'_, Self>, htlc_data: Vec, ) -> Result, PrepareTxDataError> { + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + PrepareTxDataError::Internal("Expected swap_v2_contracts to be Some, but found None".to_string()) + })? + .nft_maker_swap_v2_contract; match args.nft_swap_info.contract_type { ContractType::Erc1155 => { let function = ERC1155_CONTRACT.function("safeTransferFrom")?; @@ -268,7 +308,7 @@ impl EthCoin { .map_err(|e| PrepareTxDataError::Internal(e.to_string()))?; let data = function.encode_input(&[ Token::Address(self.my_addr().await), - Token::Address(*args.nft_swap_info.swap_contract_address), + Token::Address(nft_maker_swap_v2_contract), Token::Uint(U256::from(args.nft_swap_info.token_id)), Token::Uint(amount_u256), Token::Bytes(htlc_data), @@ -279,7 +319,7 @@ impl EthCoin { let function = erc721_transfer_with_data()?; let data = function.encode_input(&[ Token::Address(self.my_addr().await), - Token::Address(*args.nft_swap_info.swap_contract_address), + Token::Address(nft_maker_swap_v2_contract), Token::Uint(U256::from(args.nft_swap_info.token_id)), Token::Bytes(htlc_data), ])?; @@ -438,7 +478,11 @@ impl EthCoin { fn validate_decoded_data(decoded: &[Token], params: &ValidationParams) -> Result<(), MmError> { let checks = vec![ (0, Token::Address(params.maker_address), "maker_address"), - (1, Token::Address(params.etomic_swap_contract), "etomic_swap_contract"), + ( + 1, + Token::Address(params.nft_maker_swap_v2_contract), + "nft_maker_swap_v2_contract", + ), (2, Token::Uint(U256::from(params.token_id)), "token_id"), ]; @@ -528,7 +572,7 @@ fn htlc_params() -> &'static [ethabi::ParamType] { /// function to check if BigDecimal is a positive integer #[inline(always)] -fn is_positive_integer(amount: &BigDecimal) -> bool { amount == &amount.with_scale(0) && amount > &BigDecimal::from(0) } +fn is_positive_integer(amount: &BigDecimal) -> bool { amount == &amount.with_scale(0) && amount.is_positive() } fn validate_payment_args<'a>( taker_secret_hash: &'a [u8], diff --git a/mm2src/coins/eth/nft_swap_v2/structs.rs b/mm2src/coins/eth/nft_swap_v2/structs.rs index 2866d81a01..7bd4130d9f 100644 --- a/mm2src/coins/eth/nft_swap_v2/structs.rs +++ b/mm2src/coins/eth/nft_swap_v2/structs.rs @@ -11,7 +11,7 @@ pub(crate) struct ExpectedHtlcParams { pub(crate) struct ValidationParams<'a> { pub(crate) maker_address: Address, - pub(crate) etomic_swap_contract: Address, + pub(crate) nft_maker_swap_v2_contract: Address, pub(crate) token_id: &'a [u8], // Optional, as it's not needed for ERC721 pub(crate) amount: Option, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 576920b030..d96d11a95c 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -417,7 +417,9 @@ impl EthCoin { }; let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(&conf) + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&conf) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; let token = EthCoinImpl { @@ -448,6 +450,7 @@ impl EthCoin { nfts_infos: Default::default(), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; @@ -506,7 +509,9 @@ impl EthCoin { }; let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(&conf) + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&conf) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; let global_nft = EthCoinImpl { @@ -534,6 +539,7 @@ impl EthCoin { nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; Ok(EthCoin(Arc::new(global_nft))) @@ -639,7 +645,9 @@ pub async fn eth_coin_from_conf_and_request_v2( let coin_type = EthCoinType::Eth; let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(conf) + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(conf) + .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(conf) .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; let coin = EthCoinImpl { @@ -667,6 +675,7 @@ pub async fn eth_coin_from_conf_and_request_v2( nfts_infos: Default::default(), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 69ad142b0c..0e35a8c2ac 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -72,6 +72,7 @@ use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as BytesJson, H256 as H256Json}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::{self as json, Value as Json}; +use std::array::TryFromSliceError; use std::cmp::Ordering; use std::collections::hash_map::{HashMap, RawEntryMut}; use std::collections::HashSet; @@ -956,9 +957,9 @@ pub struct RefundMakerPaymentTimelockArgs<'a> { pub time_lock: u64, pub taker_pub: &'a [u8], pub tx_type_with_secret_hash: SwapTxTypeWithSecretHash<'a>, - pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], pub watcher_reward: bool, + pub amount: BigDecimal, } #[derive(Debug)] @@ -1532,7 +1533,7 @@ pub enum ValidateSwapV2TxError { /// Indicates that overflow occurred, either while calculating a total payment or converting the timelock. Overflow(String), /// Internal error - #[from_stringify("ethabi::Error")] + #[from_stringify("ethabi::Error", "TryFromSliceError")] Internal(String), /// Payment transaction is in unexpected state. E.g., `Uninitialized` instead of `PaymentSent` for ETH payment. UnexpectedPaymentState(String), @@ -1698,8 +1699,6 @@ pub struct NftSwapInfo<'a, Coin: ParseNftAssocTypes + ?Sized> { pub token_id: &'a [u8], /// The type of smart contract that governs this NFT pub contract_type: &'a Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: &'a Coin::ContractAddress, } pub struct SendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { @@ -1772,6 +1771,7 @@ pub struct RefundMakerPaymentSecretArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> pub taker_pub: &'a Coin::Pubkey, /// Unique data of specific swap pub swap_unique_data: &'a [u8], + pub amount: BigDecimal, } /// Common refund NFT Maker Payment structure for [MakerNftSwapOpsV2::refund_nft_maker_payment_v2_timelock] and @@ -1789,8 +1789,6 @@ pub struct RefundNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAss pub swap_unique_data: &'a [u8], /// The type of smart contract that governs this NFT pub contract_type: &'a Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: &'a Coin::ContractAddress, } pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { @@ -1808,6 +1806,7 @@ pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub maker_pub: &'a Coin::Pubkey, /// Unique data of specific swap pub swap_unique_data: &'a [u8], + pub amount: BigDecimal, } pub struct SpendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { @@ -1825,8 +1824,6 @@ pub struct SpendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAsso pub swap_unique_data: &'a [u8], /// The type of smart contract that governs this NFT pub contract_type: &'a Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: &'a Coin::ContractAddress, } /// Operations specific to maker coin in [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) @@ -1888,7 +1885,7 @@ pub trait MakerNftSwapOpsV2: ParseCoinAssocTypes + ParseNftAssocTypes + Send + S /// Enum representing errors that can occur while waiting for taker payment spend. #[derive(Display, Debug, EnumFromStringify)] -pub enum WaitForTakerPaymentSpendError { +pub enum WaitForPaymentSpendError { /// Timeout error variant, indicating that the wait for taker payment spend has timed out. #[display( fmt = "Timed out waiting for taker payment spend, wait_until {}, now {}", @@ -1911,20 +1908,18 @@ pub enum WaitForTakerPaymentSpendError { Transport(String), } -impl From for WaitForTakerPaymentSpendError { +impl From for WaitForPaymentSpendError { fn from(err: WaitForOutputSpendErr) -> Self { match err { - WaitForOutputSpendErr::Timeout { wait_until, now } => { - WaitForTakerPaymentSpendError::Timeout { wait_until, now } - }, + WaitForOutputSpendErr::Timeout { wait_until, now } => WaitForPaymentSpendError::Timeout { wait_until, now }, WaitForOutputSpendErr::NoOutputWithIndex(index) => { - WaitForTakerPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) + WaitForPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) }, } } } -impl From for WaitForTakerPaymentSpendError { +impl From for WaitForPaymentSpendError { fn from(e: PaymentStatusErr) -> Self { match e { PaymentStatusErr::ABIError(e) => Self::ABIError(e), @@ -1935,7 +1930,7 @@ impl From for WaitForTakerPaymentSpendError { } } -impl From for WaitForTakerPaymentSpendError { +impl From for WaitForPaymentSpendError { fn from(e: PrepareTxDataError) -> Self { match e { PrepareTxDataError::ABIError(e) => Self::ABIError(e), @@ -2076,7 +2071,7 @@ pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Syn taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult; + ) -> MmResult; } #[async_trait] diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index b558d20789..43765ab0ba 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -2,7 +2,7 @@ use super::{CoinBalance, CommonSwapOpsV2, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, - TransactionEnum, TransactionFut, WaitForTakerPaymentSpendError}; + TransactionEnum, TransactionFut, WaitForPaymentSpendError}; use crate::coin_errors::ValidatePaymentResult; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, @@ -562,7 +562,7 @@ impl TakerCoinSwapOpsV2 for TestCoin { taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { unimplemented!() } } diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 02c559caf1..f5a02f5095 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -37,7 +37,7 @@ use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinBalanceMap ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForTakerPaymentSpendError, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForPaymentSpendError, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; @@ -871,7 +871,7 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { let res = utxo_common::wait_for_output_spend_impl( self.as_ref(), taker_payment, diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 2459fcd159..dc7f818de6 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -22,6 +22,8 @@ default = [] trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] +sepolia-maker-swap-v2-tests = [] +sepolia-taker-swap-v2-tests = [] [dependencies] async-std = { version = "1.5", features = ["unstable"] } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index 2a5fd662ac..d0e667a752 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1459,6 +1459,7 @@ impl tx, diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index 100a8914de..3050f22826 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -40,10 +40,14 @@ use secp256k1::Secp256k1; pub use secp256k1::{PublicKey, SecretKey}; use serde_json::{self as json, Value as Json}; use std::process::{Command, Stdio}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use std::str::FromStr; pub use std::{env, thread}; -use std::{path::PathBuf, str::FromStr, sync::Mutex, time::Duration}; +use std::{path::PathBuf, sync::Mutex, time::Duration}; use testcontainers::{clients::Cli, core::WaitFor, Container, GenericImage, RunnableImage}; -use web3::types::{Address as EthAddress, BlockId, BlockNumber, TransactionRequest}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use web3::types::Address as EthAddress; +use web3::types::{BlockId, BlockNumber, TransactionRequest}; use web3::{transports::Http, Web3}; lazy_static! { @@ -64,10 +68,15 @@ lazy_static! { /// This approach addresses the `replacement transaction` issue, which occurs when different transactions share the same nonce. pub static ref MM_CTX1: MmArc = MmCtxBuilder::new().with_conf(json!({"use_trading_proto_v2": true})).into_mm_arc(); pub static ref GETH_WEB3: Web3 = Web3::new(Http::new(GETH_RPC_URL).unwrap()); - pub static ref SEPOLIA_WEB3: Web3 = Web3::new(Http::new(SEPOLIA_RPC_URL).unwrap()); // Mutex used to prevent nonce re-usage during funding addresses used in tests pub static ref GETH_NONCE_LOCK: Mutex<()> = Mutex::new(()); +} + +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +lazy_static! { + pub static ref SEPOLIA_WEB3: Web3 = Web3::new(Http::new(SEPOLIA_RPC_URL).unwrap()); pub static ref SEPOLIA_NONCE_LOCK: Mutex<()> = Mutex::new(()); + pub static ref SEPOLIA_TESTS_LOCK: Mutex<()> = Mutex::new(()); } pub static mut QICK_TOKEN_ADDRESS: Option = None; @@ -78,6 +87,7 @@ pub static mut QTUM_CONF_PATH: Option = None; pub static mut GETH_ACCOUNT: H160Eth = H160Eth::zero(); /// ERC20 token address on Geth dev node pub static mut GETH_ERC20_CONTRACT: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static mut SEPOLIA_ERC20_CONTRACT: H160Eth = H160Eth::zero(); /// Swap contract address on Geth dev node pub static mut GETH_SWAP_CONTRACT: H160Eth = H160Eth::zero(); @@ -85,7 +95,9 @@ pub static mut GETH_SWAP_CONTRACT: H160Eth = H160Eth::zero(); pub static mut GETH_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); /// Taker Swap V2 contract address on Geth dev node pub static mut GETH_TAKER_SWAP_V2: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static mut SEPOLIA_TAKER_SWAP_V2: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static mut SEPOLIA_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); /// Swap contract (with watchers support) address on Geth dev node pub static mut GETH_WATCHERS_SWAP_CONTRACT: H160Eth = H160Eth::zero(); @@ -95,9 +107,11 @@ pub static mut GETH_ERC721_CONTRACT: H160Eth = H160Eth::zero(); pub static mut GETH_ERC1155_CONTRACT: H160Eth = H160Eth::zero(); /// NFT Maker Swap V2 contract address on Geth dev node pub static mut GETH_NFT_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// NFT Maker Swap V2 contract address on Sepolia testnet pub static mut SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2: H160Eth = H160Eth::zero(); pub static GETH_RPC_URL: &str = "http://127.0.0.1:8545"; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static SEPOLIA_RPC_URL: &str = "https://ethereum-sepolia-rpc.publicnode.com"; pub const UTXO_ASSET_DOCKER_IMAGE: &str = "docker.io/artempikulin/testblockchain"; @@ -1566,12 +1580,15 @@ pub fn init_geth_node() { thread::sleep(Duration::from_millis(100)); } - SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 = EthAddress::from_str("0x9eb88cd58605d8fb9b14652d6152727f7e95fb4d").unwrap(); - SEPOLIA_ERC20_CONTRACT = EthAddress::from_str("0xF7b5F8E8555EF7A743f24D3E974E23A3C6cB6638").unwrap(); - SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); - // TODO update this - SEPOLIA_MAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); - + #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] + { + SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 = + EthAddress::from_str("0x9eb88cd58605d8fb9b14652d6152727f7e95fb4d").unwrap(); + SEPOLIA_ERC20_CONTRACT = EthAddress::from_str("0xF7b5F8E8555EF7A743f24D3E974E23A3C6cB6638").unwrap(); + SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); + // deploy tx https://sepolia.etherscan.io/tx/0x6f743d79ecb806f5899a6a801083e33eba9e6f10726af0873af9f39883db7f11 + SEPOLIA_MAKER_SWAP_V2 = EthAddress::from_str("0xf9000589c66Df3573645B59c10aa87594Edc318F").unwrap(); + } let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); let alice_keypair = key_pair_from_seed(&alice_passphrase).unwrap(); let alice_eth_addr = addr_from_raw_pubkey(alice_keypair.public()).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 5fc3b7ea81..22675745fc 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -1,9 +1,11 @@ use super::docker_tests_common::{random_secp256k1_secret, ERC1155_TEST_ABI, ERC721_TEST_ABI, GETH_ACCOUNT, GETH_ERC1155_CONTRACT, GETH_ERC20_CONTRACT, GETH_ERC721_CONTRACT, GETH_MAKER_SWAP_V2, GETH_NFT_MAKER_SWAP_V2, GETH_NONCE_LOCK, GETH_RPC_URL, GETH_SWAP_CONTRACT, - GETH_TAKER_SWAP_V2, GETH_WATCHERS_SWAP_CONTRACT, GETH_WEB3, MM_CTX, MM_CTX1, - SEPOLIA_ERC20_CONTRACT, SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2, SEPOLIA_MAKER_SWAP_V2, - SEPOLIA_NONCE_LOCK, SEPOLIA_RPC_URL, SEPOLIA_TAKER_SWAP_V2, SEPOLIA_WEB3}; + GETH_TAKER_SWAP_V2, GETH_WATCHERS_SWAP_CONTRACT, GETH_WEB3, MM_CTX, MM_CTX1}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use super::docker_tests_common::{SEPOLIA_ERC20_CONTRACT, SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2, SEPOLIA_MAKER_SWAP_V2, + SEPOLIA_NONCE_LOCK, SEPOLIA_RPC_URL, SEPOLIA_TAKER_SWAP_V2, SEPOLIA_TESTS_LOCK, + SEPOLIA_WEB3}; use crate::common::Future01CompatExt; use bitcrypto::{dhash160, sha256}; use coins::eth::gas_limit::ETH_MAX_TRADE_GAS; @@ -11,29 +13,41 @@ use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivation use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -use coins::{lp_coinfind, CoinProtocol, CoinWithDerivationMethod, CoinsContext, CommonSwapOpsV2, ConfirmPaymentInput, - DerivationMethod, DexFee, Eip1559Ops, FoundSwapTxSpend, FundingTxSpend, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerNftSwapOpsV2, MarketCoinOps, MmCoinEnum, MmCoinStruct, NftSwapInfo, - ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundFundingSecretArgs, - RefundNftMakerPaymentArgs, RefundPaymentArgs, RefundTakerPaymentArgs, SearchForSwapTxSpendInput, - SendNftMakerPaymentArgs, SendPaymentArgs, SendTakerFundingArgs, SpendNftMakerPaymentArgs, - SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, - Transaction, TxPreimageWithSig, ValidateNftMakerPaymentArgs, ValidateTakerFundingArgs}; +use coins::{lp_coinfind, CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, + DerivationMethod, Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, + ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, + ValidateNftMakerPaymentArgs}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use coins::{CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + MakerCoinSwapOpsV2, MmCoinEnum, MmCoinStruct, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, + RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, + SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, ValidateMakerPaymentArgs, + ValidateTakerFundingArgs}; use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_core::mm_ctx::MmArc; use mm2_number::{BigDecimal, BigUint}; -use mm2_test_helpers::for_tests::{erc20_dev_conf, eth_dev_conf, eth_sepolia_conf, nft_dev_conf, sepolia_erc20_dev_conf}; +use mm2_test_helpers::for_tests::{erc20_dev_conf, eth_dev_conf, nft_dev_conf}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; use serde_json::Value as Json; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use std::str::FromStr; use std::thread; use std::time::Duration; use web3::contract::{Contract, Options}; use web3::ethabi::Token; -use web3::types::{Address, BlockNumber, TransactionRequest, H256}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use web3::types::BlockNumber; +use web3::types::{Address, TransactionRequest, H256}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61a6eb73326de0487928"; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; @@ -55,7 +69,9 @@ pub fn maker_swap_v2() -> Address { unsafe { GETH_MAKER_SWAP_V2 } } /// /// GETH_TAKER_SWAP_V2 is set once during initialization before tests start pub fn taker_swap_v2() -> Address { unsafe { GETH_TAKER_SWAP_V2 } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_taker_swap_v2() -> Address { unsafe { SEPOLIA_TAKER_SWAP_V2 } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_maker_swap_v2() -> Address { unsafe { SEPOLIA_MAKER_SWAP_V2 } } /// # Safety /// @@ -69,9 +85,11 @@ pub fn watchers_swap_contract() -> Address { unsafe { GETH_WATCHERS_SWAP_CONTRAC /// /// GETH_ERC20_CONTRACT is set once during initialization before tests start pub fn erc20_contract() -> Address { unsafe { GETH_ERC20_CONTRACT } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_erc20_contract() -> Address { unsafe { SEPOLIA_ERC20_CONTRACT } } /// Return ERC20 dev token contract address in checksum format pub fn erc20_contract_checksum() -> String { checksum_address(&format!("{:02x}", erc20_contract())) } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_erc20_contract_checksum() -> String { checksum_address(&format!("{:02x}", sepolia_erc20_contract())) } /// # Safety /// @@ -81,6 +99,7 @@ pub fn geth_erc721_contract() -> Address { unsafe { GETH_ERC721_CONTRACT } } /// /// GETH_ERC1155_CONTRACT is set once during initialization before tests start pub fn geth_erc1155_contract() -> Address { unsafe { GETH_ERC1155_CONTRACT } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// # Safety /// /// SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 address is set once during initialization before tests start @@ -403,6 +422,7 @@ fn global_nft_with_random_privkey( global_nft } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// Can be used to generate coin from Sepolia Maker/Taker priv keys. fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { let swap_addr = SwapAddresses { @@ -463,6 +483,7 @@ fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, co coin } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] fn get_or_create_sepolia_coin(ctx: &MmArc, priv_key: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { match block_on(lp_coinfind(ctx, ticker)).unwrap() { None => sepolia_coin_from_privkey(ctx, priv_key, ticker, conf, erc20), @@ -863,6 +884,7 @@ fn send_and_spend_erc20_maker_payment_priority_fee() { send_and_spend_erc20_maker_payment_impl(SwapTxFeePolicy::Medium); } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// Wait for all pending transactions for the given address to be confirmed fn wait_pending_transactions(wallet_address: Address) { let _guard = SEPOLIA_NONCE_LOCK.lock().unwrap(); @@ -1200,9 +1222,8 @@ pub struct TestNftSwapInfo { pub token_id: Vec, /// The type of smart contract that governs this NFT pub contract_type: Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: Coin::ContractAddress, } + struct NftActivationV2Args { swap_contract_address: Address, fallback_swap_contract_address: Address, @@ -1270,7 +1291,6 @@ fn setup_test( token_address: token_contract, token_id, contract_type, - swap_contract_address: activation.swap_v2_contracts.nft_maker_swap_v2_contract, }; NftTestSetup { @@ -1290,7 +1310,6 @@ fn send_nft_maker_payment(setup: &NftTestSetup, amount: BigDecimal) -> SignedEth token_address: &setup.nft_swap_info.token_address, token_id: &setup.nft_swap_info.token_id, contract_type: &setup.nft_swap_info.contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; let send_payment_args = SendNftMakerPaymentArgs:: { time_lock: setup.time_lock, @@ -1320,7 +1339,6 @@ fn validate_nft_maker_payment(setup: &NftTestSetup, maker_payment: &SignedEthTx, token_address: &setup.nft_swap_info.token_address, token_id: &setup.nft_swap_info.token_id, contract_type: &setup.nft_swap_info.contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; let validate_args = ValidateNftMakerPaymentArgs { maker_payment_tx: maker_payment, @@ -1349,7 +1367,6 @@ fn spend_nft_maker_payment( maker_pub: &setup.maker_global_nft.derive_htlc_pubkey_v2(&[]), swap_unique_data: &[], contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; block_on(setup.taker_global_nft.spend_nft_maker_payment_v2(spend_payment_args)).unwrap() } @@ -1367,7 +1384,6 @@ fn refund_nft_maker_payment( taker_secret: &setup.taker_secret, swap_unique_data: &[], contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; match refund_type { RefundType::Timelock => { @@ -1454,11 +1470,11 @@ fn eth_coin_v2_activation_with_random_privkey( coin } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_by_secret_eth() { - // sepolia test - thread::sleep(Duration::from_secs(5)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1516,10 +1532,11 @@ fn send_and_refund_taker_funding_by_secret_eth() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_by_secret_erc20() { - thread::sleep(Duration::from_secs(130)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -1578,11 +1595,11 @@ fn send_and_refund_taker_funding_by_secret_erc20() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 200); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { - thread::sleep(Duration::from_secs(12)); - // sepolia test + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1643,10 +1660,11 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn taker_send_approve_and_spend_eth() { - // sepolia test + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1752,11 +1770,11 @@ fn taker_send_approve_and_spend_eth() { block_on(taker_coin.wait_for_taker_payment_spend(&spend_tx, 0u64, payment_time_lock)).unwrap(); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn taker_send_approve_and_spend_erc20() { - // sepolia test - thread::sleep(Duration::from_secs(9)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -1862,11 +1880,11 @@ fn taker_send_approve_and_spend_erc20() { block_on(taker_coin.wait_for_taker_payment_spend(&spend_tx, 0u64, payment_time_lock)).unwrap(); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { - // sepolia test - thread::sleep(Duration::from_secs(25)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1946,11 +1964,11 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { - // sepolia test - thread::sleep(Duration::from_secs(28)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -2032,11 +2050,11 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { - // sepolia test - thread::sleep(Duration::from_secs(200)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -2090,7 +2108,6 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { trading_amount, }; wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); - thread::sleep(Duration::from_secs(3)); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_timelock(refund_args)).unwrap(); log!( "Taker refunded ERC20 funding after pre-approval lock time was exceeded, tx hash: {:02x}", @@ -2098,3 +2115,340 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { ); wait_for_confirmations(&taker_coin, &funding_tx_refund, 150); } + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_timelock_eth() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() - 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash: &maker_secret_hash, + taker_secret_hash: &taker_secret_hash, + }; + let refund_args = RefundMakerPaymentTimelockArgs { + payment_tx: &payment_tx.to_bytes(), + time_lock: payment_time_lock, + taker_pub: &taker_pub.to_bytes(), + tx_type_with_secret_hash, + swap_unique_data: &[], + watcher_reward: false, + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); + log!( + "Maker refunded ETH payment after timelock, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_timelock_erc20() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() - 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash: &maker_secret_hash, + taker_secret_hash: &taker_secret_hash, + }; + let refund_args = RefundMakerPaymentTimelockArgs { + payment_tx: &payment_tx.to_bytes(), + time_lock: payment_time_lock, + taker_pub: &taker_pub.to_bytes(), + tx_type_with_secret_hash, + swap_unique_data: &[], + watcher_reward: false, + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); + log!( + "Maker refunded ERC20 payment after timelock, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_secret_eth() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let refund_args = RefundMakerPaymentSecretArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + taker_secret: &taker_secret, + taker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); + log!( + "Maker refunded ETH payment using taker secret, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_secret_erc20() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let refund_args = RefundMakerPaymentSecretArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + taker_secret: &taker_secret, + taker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); + log!( + "Maker refunded ERC20 payment using taker secret, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_and_spend_maker_payment_eth() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_address = block_on(taker_coin.my_addr()); + let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let validation_args = ValidateMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + maker_pub, + swap_unique_data: &[], + }; + block_on(taker_coin.validate_maker_payment_v2(validation_args)).unwrap(); + log!("Taker validated maker ETH payment"); + + let spend_args = SpendMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + maker_secret: &maker_secret, + maker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); + let spend_tx = block_on(taker_coin.spend_maker_payment_v2(spend_args)).unwrap(); + log!("Taker spent maker ETH payment, tx hash: {:02x}", spend_tx.tx_hash()); + wait_for_confirmations(&taker_coin, &spend_tx, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_and_spend_maker_payment_erc20() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_address = block_on(taker_coin.my_addr()); + let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let validation_args = ValidateMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + maker_pub, + swap_unique_data: &[], + }; + block_on(taker_coin.validate_maker_payment_v2(validation_args)).unwrap(); + log!("Taker validated maker ERC20 payment"); + + let spend_args = SpendMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + maker_secret: &maker_secret, + maker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); + let spend_tx = block_on(taker_coin.spend_maker_payment_v2(spend_args)).unwrap(); + log!("Taker spent maker ERC20 payment, tx hash: {:02x}", spend_tx.tx_hash()); + wait_for_confirmations(&taker_coin, &spend_tx, 100); +} diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 46d950242d..304f6f4819 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -548,8 +548,8 @@ fn send_and_refund_maker_payment_timelock() { maker_secret_hash, }, swap_unique_data: &[], - swap_contract_address: &None, watcher_reward: false, + amount: Default::default(), }; let refund_tx = block_on(coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); @@ -611,6 +611,7 @@ fn send_and_refund_maker_payment_taker_secret() { swap_unique_data: &[], taker_secret, taker_pub, + amount: Default::default(), }; let refund_tx = block_on(coin.refund_maker_payment_v2_secret(refund_args)).unwrap();