diff --git a/Cargo.lock b/Cargo.lock index d71fc532c8..2adeb0a727 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2260,7 +2260,7 @@ dependencies = [ [[package]] name = "ethcore-transaction" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" dependencies = [ "ethereum-types", "ethkey", @@ -2287,7 +2287,7 @@ dependencies = [ [[package]] name = "ethkey" version = "0.3.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" dependencies = [ "byteorder", "edit-distance", @@ -4214,7 +4214,7 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "mem" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" [[package]] name = "memchr" @@ -4382,6 +4382,7 @@ name = "mm2_core" version = "0.1.0" dependencies = [ "arrayref", + "async-std", "async-trait", "cfg-if 1.0.0", "common", @@ -4390,17 +4391,23 @@ dependencies = [ "futures 0.3.28", "gstuff", "hex", + "instant", "lazy_static", + "mm2_err_handle", "mm2_event_stream", "mm2_metrics", "mm2_rpc", "primitives", "rand 0.7.3", "rustls 0.21.10", + "ser_error", + "ser_error_derive", "serde", "serde_json", "shared_ref_counter", + "tokio", "uuid 1.2.2", + "wasm-bindgen-test", ] [[package]] @@ -4550,6 +4557,7 @@ dependencies = [ "enum-primitive-derive", "enum_derives", "ethabi", + "ethcore-transaction", "ethereum-types", "futures 0.1.29", "futures 0.3.28", @@ -4591,9 +4599,11 @@ dependencies = [ "rand 0.7.3", "rcgen", "regex", + "rlp", "rmp-serde", "rpc", "rpc_task", + "rustc-hex", "rustls 0.21.10", "rustls-pemfile 1.0.2", "script", @@ -8787,7 +8797,7 @@ dependencies = [ [[package]] name = "unexpected" version = "0.1.0" -source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git#3326a6c3c12c1655f9dec57ad28b0983d8c08997" +source = "git+https://github.com/KomodoPlatform/mm2-parity-ethereum.git?rev=mm2-v2.1.1#d5524212230c4773d01b2527e9b3c422a251e0dc" [[package]] name = "unicode-bidi" @@ -9155,7 +9165,7 @@ dependencies = [ [[package]] name = "web3" version = "0.19.0" -source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.19.0#ec5e72a5c95e3935ea0c9ab77b501e3926686fa9" +source = "git+https://github.com/KomodoPlatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" dependencies = [ "arrayvec 0.7.1", "base64 0.13.0", diff --git a/mm2src/adex_cli/src/rpc_data.rs b/mm2src/adex_cli/src/rpc_data.rs index f8e1329453..a3146cbe47 100644 --- a/mm2src/adex_cli/src/rpc_data.rs +++ b/mm2src/adex_cli/src/rpc_data.rs @@ -3,7 +3,7 @@ //! *Note: it's expected that the following data types will be moved to mm2_rpc::data when mm2 is refactored to be able to handle them* //! -use mm2_rpc::data::legacy::{ElectrumProtocol, GasStationPricePolicy, UtxoMergeParams}; +use mm2_rpc::data::legacy::{ElectrumProtocol, UtxoMergeParams}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] @@ -23,12 +23,6 @@ pub(crate) struct EnableRequest { #[serde(skip_serializing_if = "Option::is_none")] fallback_swap_contract: Option, #[serde(skip_serializing_if = "Option::is_none")] - gas_station_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - gas_station_decimals: Option, - #[serde(skip_serializing_if = "Option::is_none")] - gas_station_policy: Option, - #[serde(skip_serializing_if = "Option::is_none")] mm2: Option, #[serde(default)] tx_history: bool, diff --git a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs index 36a864b5f8..7bc46126ba 100644 --- a/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs +++ b/mm2src/adex_cli/src/scenarios/mm2_proc_mng.rs @@ -347,10 +347,11 @@ pub(crate) fn get_status() { .filter(|line| line.contains("PID")) .last() { - let pid = found - .trim() + let chars = found.trim(); + + let pid = chars .matches(char::is_numeric) - .fold(String::default(), |mut pid, ch| { + .fold(String::with_capacity(chars.len()), |mut pid, ch| { pid.push_str(ch); pid }); diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 55dc1903e5..fcc83c7697 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -50,9 +50,9 @@ derive_more = "0.99" ed25519-dalek = "1.0.1" enum_derives = { path = "../derives/enum_derives" } ethabi = { version = "17.0.0" } -ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +ethcore-transaction = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } -ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git" } +ethkey = { git = "https://github.com/KomodoPlatform/mm2-parity-ethereum.git", rev = "mm2-v2.1.1" } # Waiting for https://github.com/rust-lang/rust/issues/54725 to use on Stable. #enum_dispatch = "0.1" futures01 = { version = "0.1", package = "futures" } @@ -112,7 +112,7 @@ url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } # One of web3 dependencies is the old `tokio-uds 0.1.7` which fails cross-compiling to ARM. # We don't need the default web3 features at all since we added our own web3 transport using shared HYPER instance. -web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.19.0", default-features = false } +web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false } zbase32 = "0.1.2" zcash_client_backend = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } zcash_extras = { git = "https://github.com/KomodoPlatform/librustzcash.git", tag = "k-1.4.1" } diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index 5c86537a46..34b0c46486 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -71,9 +71,10 @@ impl From for ValidatePaymentError { match e { Web3RpcError::Transport(tr) => ValidatePaymentError::Transport(tr), Web3RpcError::InvalidResponse(resp) => ValidatePaymentError::InvalidRpcResponse(resp), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - ValidatePaymentError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => ValidatePaymentError::InternalError(internal), Web3RpcError::NftProtocolNotSupported => ValidatePaymentError::NftProtocolNotSupported, } } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 62514973e7..e0e0db45d5 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -47,15 +47,15 @@ use crate::rpc_command::init_withdraw::{InitWithdrawCoin, WithdrawTaskHandleShar use crate::rpc_command::{account_balance, get_new_address, init_account_balance, init_create_account, init_scan_for_new_addresses}; use crate::{coin_balance, scan_for_new_addresses_impl, BalanceResult, CoinWithDerivationMethod, DerivationMethod, - DexFee, MakerNftSwapOpsV2, ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyPolicy, RefundMakerPaymentArgs, - RpcCommonOps, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, ToBytes, ValidateNftMakerPaymentArgs, - ValidateWatcherSpendInput, WatcherSpendType}; + DexFee, Eip1559Ops, MakerNftSwapOpsV2, ParseCoinAssocTypes, ParseNftAssocTypes, PayForGasParams, + PrivKeyPolicy, RefundMakerPaymentArgs, RpcCommonOps, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, + ToBytes, ValidateNftMakerPaymentArgs, ValidateWatcherSpendInput, WatcherSpendType}; use async_trait::async_trait; use bitcrypto::{dhash160, keccak256, ripemd160, sha256}; use common::custom_futures::repeatable::{Ready, Retry, RetryOnError}; use common::custom_futures::timeout::FutureTimerExt; -use common::executor::{abortable_queue::AbortableQueue, AbortSettings, AbortableSystem, AbortedError, SpawnAbortable, - Timer}; +use common::executor::{abortable_queue::AbortableQueue, AbortOnDropHandle, AbortSettings, AbortableSystem, + AbortedError, SpawnAbortable, Timer}; use common::log::{debug, error, info, warn}; use common::number_type_casting::SafeTypeCastingNumbers; use common::{get_utc_timestamp, now_sec, small_rng, DEX_FEE_ADDR_RAW_PUBKEY}; @@ -64,22 +64,24 @@ use crypto::{Bip44Chain, CryptoCtx, CryptoCtxError, GlobalHDAccountArc, KeyPairP use derive_more::Display; use enum_derives::EnumFromStringify; use ethabi::{Contract, Function, Token}; -pub use ethcore_transaction::SignedTransaction as SignedEthTx; -use ethcore_transaction::{Action, Transaction as UnSignedEthTx, UnverifiedTransaction}; +use ethcore_transaction::tx_builders::TxBuilderError; +use ethcore_transaction::{Action, TransactionWrapper, TransactionWrapperBuilder as UnSignedEthTxBuilder, + UnverifiedEip1559Transaction, UnverifiedEip2930Transaction, UnverifiedLegacyTransaction, + UnverifiedTransactionWrapper}; +pub use ethcore_transaction::{SignedTransaction as SignedEthTx, TxType}; use ethereum_types::{Address, H160, H256, U256}; use ethkey::{public_to_address, sign, verify_address, KeyPair, Public, Signature}; use futures::compat::Future01CompatExt; -use futures::future::{join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; +use futures::future::{join, join_all, select_ok, try_join_all, Either, FutureExt, TryFutureExt}; use futures01::Future; -use http::{StatusCode, Uri}; +use http::Uri; use instant::Instant; use keys::Public as HtlcPubKey; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_event_stream::behaviour::{EventBehaviour, EventInitStatus}; -use mm2_net::transport::{slurp_url, GuiAuthValidation, GuiAuthValidationGenerator, SlurpError}; +use mm2_net::transport::{GuiAuthValidation, GuiAuthValidationGenerator}; use mm2_number::bigdecimal_custom::CheckedDivision; use mm2_number::{BigDecimal, BigUint, MmNumber}; -use mm2_rpc::data::legacy::GasStationPricePolicy; #[cfg(test)] use mocktopus::macros::*; use rand::seq::SliceRandom; use rlp::{DecoderError, Encodable, RlpStream}; @@ -108,6 +110,26 @@ cfg_wasm32! { use web3::types::TransactionRequest; } +use super::{coin_conf, lp_coinfind_or_err, AsyncMutex, BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, + CoinBalance, CoinFutSpawner, CoinProtocol, CoinTransportMetrics, CoinsContext, ConfirmPaymentInput, + EthValidateFeeArgs, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, + MarketCoinOps, MmCoin, MmCoinEnum, MyAddressError, MyWalletAddress, NegotiateSwapContractAddrErr, + NumConversError, NumConversResult, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, + PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, + RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, + RefundResult, RewardTarget, RpcClientType, RpcTransportEventHandler, RpcTransportEventHandlerShared, + SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignEthTransactionParams, + SignRawTransactionEnum, SignRawTransactionRequest, SignatureError, SignatureResult, SpendPaymentArgs, + SwapOps, SwapTxFeePolicy, TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, + TradePreimageResult, TradePreimageValue, Transaction, TransactionDetails, TransactionEnum, TransactionErr, + TransactionFut, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, + ValidatePaymentFut, ValidatePaymentInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, + WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, + WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, + WithdrawRequest, WithdrawResult, EARLY_CONFIRMATION_ERR_LOG, INVALID_CONTRACT_ADDRESS_ERR_LOG, + INVALID_PAYMENT_STATE_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_SENDER_ERR_LOG, INVALID_SWAP_ID_ERR_LOG}; +pub use rlp; cfg_native! { use std::path::PathBuf; } @@ -133,6 +155,11 @@ use eth_withdraw::{EthWithdraw, InitEthWithdraw, StandardEthWithdraw}; mod nonce; use nonce::ParityNonce; +mod eip1559_gas_fee; +pub(crate) use eip1559_gas_fee::FeePerGasEstimated; +use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider, + InfuraGasApiCaller}; + /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 /// Ropsten: https://ropsten.etherscan.io/address/0x7bc1bbdd6a0a722fc9bffc49c921b685ecb84b94 @@ -171,16 +198,15 @@ pub(crate) enum TakerPaymentStateV2 { TakerRefunded, } -// Ethgasstation API returns response in 10^8 wei units. So 10 from their API mean 1 gwei -const ETH_GAS_STATION_DECIMALS: u8 = 8; -const GAS_PRICE_PERCENT: u64 = 10; /// It can change 12.5% max each block according to https://www.blocknative.com/blog/eip-1559-fees const BASE_BLOCK_FEE_DIFF_PCT: u64 = 13; const DEFAULT_LOGS_BLOCK_RANGE: u64 = 1000; const DEFAULT_REQUIRED_CONFIRMATIONS: u8 = 1; -const ETH_DECIMALS: u8 = 18; +pub(crate) const ETH_DECIMALS: u8 = 18; + +pub(crate) const ETH_GWEI_DECIMALS: u8 = 9; /// Take into account that the dynamic fee may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP: u64 = 3; @@ -196,11 +222,79 @@ const GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE: u64 = 5; /// - it may increase by 3% during the swap. const GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE: u64 = 7; -pub const ETH_GAS: u64 = 150_000; +/// Heuristic default gas limits for withdraw and swap operations (including extra margin value for possible changes in opcodes cost) +pub mod gas_limit { + /// Gas limit for sending coins + pub const ETH_SEND_COINS: u64 = 21_000; + /// Gas limit for transfer ERC20 tokens + /// TODO: maybe this is too much and 150K is okay + pub const ETH_SEND_ERC20: u64 = 210_000; + /// Gas limit for swap payment tx with coins + /// real values are approx 48,6K by etherscan + pub const ETH_PAYMENT: u64 = 65_000; + /// Gas limit for swap payment tx with ERC20 tokens + /// real values are 98,9K for ERC20 and 135K for ERC-1967 proxied ERC20 contracts (use 'gas_limit' override in coins to tune) + pub const ERC20_PAYMENT: u64 = 150_000; + /// Gas limit for swap receiver spend tx with coins + /// real values are 40,7K + pub const ETH_RECEIVER_SPEND: u64 = 65_000; + /// Gas limit for swap receiver spend tx with ERC20 tokens + /// real values are 72,8K + 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 + pub const ERC20_SENDER_REFUND: u64 = 150_000; + /// Gas limit for other operations + pub const ETH_MAX_TRADE_GAS: u64 = 150_000; +} + +/// Coin conf param to override default gas limits +#[derive(Deserialize)] +#[serde(default)] +pub struct EthGasLimit { + /// Gas limit for sending coins + pub eth_send_coins: u64, + /// Gas limit for sending ERC20 tokens + pub eth_send_erc20: u64, + /// Gas limit for swap payment tx with coins + pub eth_payment: u64, + /// Gas limit for swap payment tx with ERC20 tokens + pub erc20_payment: u64, + /// Gas limit for swap receiver spend tx with coins + pub eth_receiver_spend: u64, + /// Gas limit for swap receiver spend tx with ERC20 tokens + 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 + pub erc20_sender_refund: u64, + /// Gas limit for other operations + pub eth_max_trade_gas: u64, +} + +impl Default for EthGasLimit { + fn default() -> Self { + EthGasLimit { + eth_send_coins: gas_limit::ETH_SEND_COINS, + eth_send_erc20: gas_limit::ETH_SEND_ERC20, + eth_payment: gas_limit::ETH_PAYMENT, + erc20_payment: gas_limit::ERC20_PAYMENT, + eth_receiver_spend: gas_limit::ETH_RECEIVER_SPEND, + erc20_receiver_spend: gas_limit::ERC20_RECEIVER_SPEND, + eth_sender_refund: gas_limit::ETH_SENDER_REFUND, + erc20_sender_refund: gas_limit::ERC20_SENDER_REFUND, + eth_max_trade_gas: gas_limit::ETH_MAX_TRADE_GAS, + } + } +} /// Lifetime of generated signed message for gui-auth requests const GUI_AUTH_SIGNED_MESSAGE_LIFETIME_SEC: i64 = 90; +/// Max transaction type according to EIP-2718 +const ETH_MAX_TX_TYPE: u64 = 0x7f; + lazy_static! { pub static ref SWAP_CONTRACT: Contract = Contract::load(SWAP_CONTRACT_ABI.as_bytes()).unwrap(); pub static ref ERC20_CONTRACT: Contract = Contract::load(ERC20_ABI.as_bytes()).unwrap(); @@ -209,39 +303,81 @@ lazy_static! { pub static ref NFT_SWAP_CONTRACT: Contract = Contract::load(NFT_SWAP_CONTRACT_ABI.as_bytes()).unwrap(); } -pub type GasStationResult = Result>; pub type EthDerivationMethod = DerivationMethod; pub type Web3RpcFut = Box> + Send>; pub type Web3RpcResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; -type GasDetails = (U256, U256); -#[derive(Debug, Display, EnumFromStringify)] -pub enum GasStationReqErr { - #[display(fmt = "Transport '{}' error: {}", uri, error)] - Transport { - uri: String, - error: String, - }, - #[from_stringify("serde_json::Error")] - #[display(fmt = "Invalid response: {}", _0)] - InvalidResponse(String), - Internal(String), +#[macro_export] +macro_rules! wei_from_gwei_decimal { + ($big_decimal: expr) => { + $crate::eth::wei_from_big_decimal($big_decimal, $crate::eth::ETH_GWEI_DECIMALS) + }; } -impl From for GasStationReqErr { - fn from(e: SlurpError) -> Self { - let error = e.to_string(); - match e { - SlurpError::ErrorDeserializing { .. } => GasStationReqErr::InvalidResponse(error), - SlurpError::Transport { uri, .. } | SlurpError::Timeout { uri, .. } => { - GasStationReqErr::Transport { uri, error } - }, - SlurpError::Internal(_) | SlurpError::InvalidRequest(_) => GasStationReqErr::Internal(error), +#[macro_export] +macro_rules! wei_to_gwei_decimal { + ($gwei: expr) => { + $crate::eth::u256_to_big_decimal($gwei, $crate::eth::ETH_GWEI_DECIMALS) + }; +} + +#[derive(Clone, Debug)] +pub(crate) struct LegacyGasPrice { + pub(crate) gas_price: U256, +} + +#[derive(Clone, Debug)] +pub(crate) struct Eip1559FeePerGas { + pub(crate) max_fee_per_gas: U256, + pub(crate) max_priority_fee_per_gas: U256, +} + +/// Internal structure describing how transaction pays for gas unit: +/// either legacy gas price or EIP-1559 fee per gas +#[derive(Clone, Debug)] +pub(crate) enum PayForGasOption { + Legacy(LegacyGasPrice), + Eip1559(Eip1559FeePerGas), +} + +impl PayForGasOption { + fn get_gas_price(&self) -> Option { + match self { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => Some(*gas_price), + PayForGasOption::Eip1559(..) => None, + } + } + + fn get_fee_per_gas(&self) -> (Option, Option) { + match self { + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) => (Some(*max_fee_per_gas), Some(*max_priority_fee_per_gas)), + PayForGasOption::Legacy(..) => (None, None), + } + } +} + +impl TryFrom for PayForGasOption { + type Error = MmError; + + fn try_from(param: PayForGasParams) -> Result { + match param { + PayForGasParams::Legacy(legacy) => Ok(Self::Legacy(LegacyGasPrice { + gas_price: wei_from_gwei_decimal!(&legacy.gas_price)?, + })), + PayForGasParams::Eip1559(eip1559) => Ok(Self::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_priority_fee_per_gas)?, + })), } } } +type GasDetails = (U256, PayForGasOption); + #[derive(Debug, Display, EnumFromStringify)] pub enum Web3RpcError { #[display(fmt = "Transport: {}", _0)] @@ -253,18 +389,12 @@ pub enum Web3RpcError { Timeout(String), #[display(fmt = "Internal: {}", _0)] Internal(String), + #[display(fmt = "Invalid gas api provider config: {}", _0)] + InvalidGasApiConfig(String), #[display(fmt = "Nft Protocol is not supported yet!")] NftProtocolNotSupported, -} - -impl From for Web3RpcError { - fn from(err: GasStationReqErr) -> Self { - match err { - GasStationReqErr::Transport { .. } => Web3RpcError::Transport(err.to_string()), - GasStationReqErr::InvalidResponse(err) => Web3RpcError::InvalidResponse(err), - GasStationReqErr::Internal(err) => Web3RpcError::Internal(err), - } - } + #[display(fmt = "Number conversion: {}", _0)] + NumConversError(String), } impl From for Web3RpcError { @@ -286,9 +416,10 @@ impl From for RawTransactionError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => RawTransactionError::Transport(tr), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - RawTransactionError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => RawTransactionError::InternalError(internal), Web3RpcError::NftProtocolNotSupported => { RawTransactionError::InternalError("Nft Protocol is not supported yet!".to_string()) }, @@ -318,6 +449,10 @@ impl From for Web3RpcError { } } +impl From for Web3RpcError { + fn from(e: NumConversError) -> Self { Web3RpcError::NumConversError(e.to_string()) } +} + impl From for WithdrawError { fn from(e: ethabi::Error) -> Self { // Currently, we use the `ethabi` crate to work with a smart contract ABI known at compile time. @@ -334,14 +469,19 @@ impl From for WithdrawError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => WithdrawError::Transport(err), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - WithdrawError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => WithdrawError::InternalError(internal), Web3RpcError::NftProtocolNotSupported => WithdrawError::NftProtocolNotSupported, } } } +impl From for WithdrawError { + fn from(e: ethcore_transaction::Error) -> Self { WithdrawError::SigningError(e.to_string()) } +} + impl From for TradePreimageError { fn from(e: web3::Error) -> Self { TradePreimageError::Transport(e.to_string()) } } @@ -350,9 +490,10 @@ impl From for TradePreimageError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(err) | Web3RpcError::InvalidResponse(err) => TradePreimageError::Transport(err), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => { - TradePreimageError::InternalError(internal) - }, + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => TradePreimageError::InternalError(internal), Web3RpcError::NftProtocolNotSupported => TradePreimageError::NftProtocolNotSupported, } } @@ -382,7 +523,10 @@ impl From for BalanceError { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => BalanceError::Transport(tr), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => BalanceError::Internal(internal), + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => BalanceError::Internal(internal), Web3RpcError::NftProtocolNotSupported => { BalanceError::Internal("Nft Protocol is not supported yet!".to_string()) }, @@ -390,6 +534,14 @@ impl From for BalanceError { } } +impl From for TransactionErr { + fn from(e: TxBuilderError) -> Self { TransactionErr::Plain(e.to_string()) } +} + +impl From for TransactionErr { + fn from(e: ethcore_transaction::Error) -> Self { TransactionErr::Plain(e.to_string()) } +} + #[derive(Debug, Deserialize, Serialize)] struct SavedTraces { /// ETH traces for my_address @@ -460,6 +612,29 @@ impl From for EthPrivKeyBuildPolicy { } } +/// Gas fee estimator loop context, runs a loop to estimate max fee and max priority fee per gas according to EIP-1559 for the next block +/// +/// This FeeEstimatorContext handles rpc requests which start and stop gas fee estimation loop and handles the loop itself. +/// FeeEstimatorContext keeps the latest estimated gas fees to return them on rpc request +pub(crate) struct FeeEstimatorContext { + /// Latest estimated gas fee values + pub(crate) estimated_fees: Arc>, + /// Handler for estimator loop graceful shutdown + pub(crate) abort_handler: AsyncMutex>, +} + +/// Gas fee estimator creation state +pub(crate) enum FeeEstimatorState { + /// Gas fee estimation not supported for this coin + CoinNotSupported, + /// Platform coin required to be enabled for gas fee estimation for this coin + PlatformCoinRequired, + /// Fee estimator created, use simple internal estimator + Simple(AsyncMutex), + /// Fee estimator created, use provider or simple internal estimator (if provider fails) + Provider(AsyncMutex), +} + /// pImpl idiom. pub struct EthCoinImpl { ticker: String, @@ -476,11 +651,10 @@ pub struct EthCoinImpl { contract_supports_watchers: bool, web3_instances: AsyncMutex>, decimals: u8, - gas_station_url: Option, - gas_station_decimals: u8, - gas_station_policy: GasStationPricePolicy, history_sync_state: Mutex, required_confirmations: AtomicU64, + swap_txfee_policy: Mutex, + max_eth_tx_type: Option, /// Coin needs access to the context in order to reuse the logging and shutdown facilities. /// Using a weak reference by default in order to avoid circular references and leaks. pub ctx: MmWeak, @@ -500,6 +674,10 @@ pub struct EthCoinImpl { /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. pub nfts_infos: Arc>>, + /// Context for eth fee per gas estimator loop. Created if coin supports fee per gas estimation + pub(crate) platform_fee_estimator_state: Arc, + /// Config provided gas limits for swap and send transactions + pub(crate) gas_limit: EthGasLimit, /// 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, @@ -533,18 +711,18 @@ pub enum EthAddressFormat { MixedCase, } -#[cfg_attr(test, mockable)] -async fn make_gas_station_request(url: &str) -> GasStationResult { - let resp = slurp_url(url).await?; - if resp.0 != StatusCode::OK { - let error = format!("Gas price request failed with status code {}", resp.0); - return MmError::err(GasStationReqErr::Transport { - uri: url.to_owned(), - error, - }); - } - let result: GasStationData = json::from_slice(&resp.2)?; - Ok(result) +/// get tx type from pay_for_gas_option +/// currently only type2 and legacy supported +/// if for Eth Classic we also want support for type 1 then use a fn +#[macro_export] +macro_rules! tx_type_from_pay_for_gas_option { + ($pay_for_gas_option: expr) => { + if matches!($pay_for_gas_option, PayForGasOption::Eip1559(..)) { + ethcore_transaction::TxType::Type2 + } else { + ethcore_transaction::TxType::Legacy + } + }; } impl EthCoinImpl { @@ -638,8 +816,10 @@ impl EthCoinImpl { /// The id used to differentiate payments on Etomic swap smart contract pub(crate) fn etomic_swap_id(&self, time_lock: u32, secret_hash: &[u8]) -> Vec { - let mut input = vec![]; - input.extend_from_slice(&time_lock.to_le_bytes()); + let timelock_bytes = time_lock.to_le_bytes(); + + let mut input = Vec::with_capacity(timelock_bytes.len() + secret_hash.len()); + input.extend_from_slice(&timelock_bytes); input.extend_from_slice(secret_hash); sha256(&input).to_vec() } @@ -754,7 +934,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit }, EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; - let (gas, gas_price) = get_eth_gas_details( + let (gas, pay_for_gas_option) = get_eth_gas_details_from_withdraw_fee( ð_coin, withdraw_type.fee, eth_value, @@ -774,23 +954,23 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit .await? .map_to_mm(WithdrawError::Transport)?; - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Call(call_addr), - data, - gas, - gas_price, - }; - + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !eth_coin.is_tx_type_supported(&tx_type) { + return MmError::err(WithdrawError::TxTypeNotSupported); + } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, Action::Call(call_addr), eth_value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(ð_coin, tx_builder, &pay_for_gas_option)?; + let tx = tx_builder + .build() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, Some(eth_coin.chain_id)); + let signed = tx.sign(secret, Some(eth_coin.chain_id))?; let signed_bytes = rlp::encode(&signed); - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash()), + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, @@ -844,7 +1024,7 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd // TODO: start to use NFT GLOBAL TOKEN for withdraw EthCoinType::Nft { .. } => return MmError::err(WithdrawError::NftProtocolNotSupported), }; - let (gas, gas_price) = get_eth_gas_details( + let (gas, pay_for_gas_option) = get_eth_gas_details_from_withdraw_fee( ð_coin, withdraw_type.fee, eth_value, @@ -865,23 +1045,23 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd .await? .map_to_mm(WithdrawError::Transport)?; - let tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Call(call_addr), - data, - gas, - gas_price, - }; - + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !eth_coin.is_tx_type_supported(&tx_type) { + return MmError::err(WithdrawError::TxTypeNotSupported); + } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, Action::Call(call_addr), eth_value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(ð_coin, tx_builder, &pay_for_gas_option)?; + let tx = tx_builder + .build() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let secret = eth_coin.priv_key_policy.activated_key_or_err()?.secret(); - let signed = tx.sign(secret, Some(eth_coin.chain_id)); + let signed = tx.sign(secret, Some(eth_coin.chain_id))?; let signed_bytes = rlp::encode(&signed); - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash()), + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, @@ -906,7 +1086,7 @@ impl Deref for EthCoin { #[async_trait] impl SwapOps for EthCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let address = try_tx_fus!(addr_from_raw_pubkey(fee_addr)); Box::new( @@ -968,7 +1148,7 @@ impl SwapOps for EthCoin { _ => panic!(), }; validate_fee_impl(self.clone(), EthValidateFeeArgs { - fee_tx_hash: &tx.hash, + fee_tx_hash: &tx.tx_hash(), expected_sender: validate_fee_args.expected_sender, fee_addr: validate_fee_args.fee_addr, amount: &validate_fee_args.dex_fee.fee_amount().into(), @@ -1093,14 +1273,14 @@ impl SwapOps for EthCoin { spend_tx: &[u8], watcher_reward: bool, ) -> Result, String> { - let unverified: UnverifiedTransaction = try_s!(rlp::decode(spend_tx)); + let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(spend_tx)); let function_name = get_function_name("receiverSpend", watcher_reward); let function = try_s!(SWAP_CONTRACT.function(&function_name)); // Validate contract call; expected to be receiverSpend. // https://www.4byte.directory/signatures/?bytes4_signature=02ed292b. let expected_signature = function.short_signature(); - let actual_signature = &unverified.data[0..4]; + let actual_signature = &unverified.unsigned().data()[0..4]; if actual_signature != expected_signature { return ERR!( "Expected 'receiverSpend' contract call signature: {:?}, found {:?}", @@ -1109,7 +1289,7 @@ impl SwapOps for EthCoin { ); }; - let tokens = try_s!(decode_contract_call(function, &unverified.data)); + let tokens = try_s!(decode_contract_call(function, unverified.unsigned().data())); if tokens.len() < 3 { return ERR!("Invalid arguments in 'receiverSpend' call: {:?}", tokens); } @@ -1280,7 +1460,7 @@ impl WatcherOps for EthCoin { _secret_hash: &[u8], _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(maker_payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(maker_payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); let fut = async move { Ok(TransactionEnum::from(signed)) }; @@ -1296,7 +1476,7 @@ impl WatcherOps for EthCoin { _swap_contract_address: &Option, _swap_unique_data: &[u8], ) -> TransactionFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(taker_payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(taker_payment_tx)); let signed = try_tx_fus!(SignedEthTx::new(tx)); let fut = async move { Ok(TransactionEnum::from(signed)) }; @@ -1336,7 +1516,7 @@ impl WatcherOps for EthCoin { .try_to_address() .map_to_mm(ValidatePaymentError::InvalidParameter)); - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); @@ -1358,9 +1538,9 @@ impl WatcherOps for EthCoin { let trade_amount = try_f!(wei_from_big_decimal(&(input.amount), decimals)); let fut = async move { - match tx.action { + match tx.unsigned().action() { Call(contract_address) => { - if contract_address != expected_swap_contract_address { + if *contract_address != expected_swap_contract_address { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Transaction {:?} was sent to wrong address, expected {:?}", contract_address, expected_swap_contract_address, @@ -1398,7 +1578,7 @@ impl WatcherOps for EthCoin { .function(&function_name) .map_to_mm(|err| ValidatePaymentError::InternalError(err.to_string()))?; - let decoded = decode_contract_call(function, &tx.data) + let decoded = decode_contract_call(function, tx.unsigned().data()) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))?; let swap_id_input = get_function_input_data(&decoded, function, 0) @@ -1495,10 +1675,10 @@ impl WatcherOps for EthCoin { ))); } - if tx.value != U256::zero() { + if tx.unsigned().value() != U256::zero() { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "Transaction value arg {:?} is invalid, expected 0", - tx.value + tx.unsigned().value() ))); } @@ -1569,7 +1749,7 @@ impl WatcherOps for EthCoin { } fn watcher_validate_taker_payment(&self, input: WatcherValidatePaymentInput) -> ValidatePaymentFut<()> { - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); @@ -1591,7 +1771,7 @@ impl WatcherOps for EthCoin { let fallback_swap_contract = self.fallback_swap_contract; let fut = async move { - let tx_from_rpc = selfi.transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi.transaction(TransactionId::Hash(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", tx)) @@ -1813,10 +1993,10 @@ impl WatcherOps for EthCoin { &self, input: WatcherSearchForSwapTxSpendInput<'_>, ) -> Result, String> { - let unverified: UnverifiedTransaction = try_s!(rlp::decode(input.tx)); + let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(input.tx)); let tx = try_s!(SignedEthTx::new(unverified)); - let swap_contract_address = match tx.action { - Call(address) => address, + let swap_contract_address = match tx.unsigned().action() { + Call(address) => *address, Create => return Err(ERRL!("Invalid payment action: the payment action cannot be create")), }; @@ -2076,9 +2256,9 @@ impl MarketCoinOps for EthCoin { status.status(&[&self.ticker], "Waiting for confirmations…"); status.deadline(input.wait_until * 1000); - let unsigned: UnverifiedTransaction = try_fus!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_fus!(rlp::decode(&input.payment_tx)); let tx = try_fus!(SignedEthTx::new(unsigned)); - let tx_hash = tx.hash(); + let tx_hash = tx.tx_hash(); let required_confirms = U64::from(input.confirmations); let check_every = input.check_every as f64; @@ -2149,13 +2329,13 @@ impl MarketCoinOps for EthCoin { } fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - let unverified: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.tx_bytes)); + let unverified: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(args.tx_bytes)); let tx = try_tx_fus!(SignedEthTx::new(unverified)); let swap_contract_address = match args.swap_contract_address { Some(addr) => try_tx_fus!(addr.try_to_address()), - None => match tx.action { - Call(address) => address, + None => match tx.unsigned().action() { + Call(address) => *address, Create => { return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" @@ -2175,7 +2355,7 @@ impl MarketCoinOps for EthCoin { }; let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &tx.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, tx.unsigned().data())); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), invalid_token => { @@ -2294,7 +2474,7 @@ impl MarketCoinOps for EthCoin { } pub fn signed_eth_tx_from_bytes(bytes: &[u8]) -> Result { - let tx: UnverifiedTransaction = try_s!(rlp::decode(bytes)); + let tx: UnverifiedTransactionWrapper = try_s!(rlp::decode(bytes)); let signed = try_s!(SignedEthTx::new(tx)); Ok(signed) } @@ -2315,35 +2495,36 @@ type EthTxFut = Box + Sen /// This method polls for the latest nonce from the RPC nodes and uses it for the transaction to be signed. /// A `nonce_lock` is returned so that the caller doesn't release it until the transaction is sent and the /// address nonce is updated on RPC nodes. -async fn sign_transaction_with_keypair( - coin: &EthCoin, +#[allow(clippy::too_many_arguments)] +async fn sign_transaction_with_keypair<'a>( + coin: &'a EthCoin, key_pair: &KeyPair, value: U256, action: Action, data: Vec, gas: U256, + pay_for_gas_option: &PayForGasOption, from_address: Address, ) -> Result<(SignedEthTx, Vec), TransactionErr> { info!(target: "sign", "get_addr_nonce…"); let (nonce, web3_instances_with_latest_nonce) = try_tx_s!(coin.clone().get_addr_nonce(from_address).compat().await); - info!(target: "sign", "get_gas_price…"); - let gas_price = try_tx_s!(coin.get_gas_price().compat().await); - - let tx = UnSignedEthTx { - nonce, - gas_price, - gas, - action, - value, - data, - }; + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !coin.is_tx_type_supported(&tx_type) { + return Err(TransactionErr::Plain("Eth transaction type not supported".into())); + } + let tx_builder = UnSignedEthTxBuilder::new(tx_type, nonce, gas, action, value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, pay_for_gas_option) + .map_err(|e| TransactionErr::Plain(e.get_inner().to_string()))?; + let tx = tx_builder.build()?; Ok(( - tx.sign(key_pair.secret(), Some(coin.chain_id)), + tx.sign(key_pair.secret(), Some(coin.chain_id))?, web3_instances_with_latest_nonce, )) } +/// Sign and send eth transaction with provided keypair, +/// This fn is primarily for swap transactions so it uses swap tx fee policy async fn sign_and_send_transaction_with_keypair( coin: &EthCoin, key_pair: &KeyPair, @@ -2353,11 +2534,15 @@ async fn sign_and_send_transaction_with_keypair( data: Vec, gas: U256, ) -> Result { - let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); - let address_lock = coin.get_address_lock(my_address.to_string()).await; + info!(target: "sign-and-send", "get_gas_price…"); + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + ); + let address_lock = coin.get_address_lock(address.to_string()).await; let _nonce_lock = address_lock.lock().await; let (signed, web3_instances_with_latest_nonce) = - sign_transaction_with_keypair(coin, key_pair, value, action, data, gas, my_address).await?; + sign_transaction_with_keypair(coin, key_pair, value, action, data, gas, &pay_for_gas_option, address).await?; let bytes = Bytes(rlp::encode(&signed).to_vec()); info!(target: "sign-and-send", "send_raw_transaction…"); @@ -2367,11 +2552,13 @@ async fn sign_and_send_transaction_with_keypair( try_tx_s!(select_ok(futures).await.map_err(|e| ERRL!("{}", e)), signed); info!(target: "sign-and-send", "wait_for_tx_appears_on_rpc…"); - coin.wait_for_addr_nonce_increase(address, signed.transaction.unsigned.nonce) + coin.wait_for_addr_nonce_increase(address, signed.unsigned().nonce()) .await; Ok(signed) } +/// Sign and send eth transaction with metamask API, +/// This fn is primarily for swap transactions so it uses swap tx fee policy #[cfg(target_arch = "wasm32")] async fn sign_and_send_transaction_with_metamask( coin: EthCoin, @@ -2385,14 +2572,20 @@ async fn sign_and_send_transaction_with_metamask( Action::Call(to) => Some(to), }; + let pay_for_gas_option = try_tx_s!( + coin.get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + ); let my_address = try_tx_s!(coin.derivation_method.single_addr_or_err().await); - let gas_price = try_tx_s!(coin.get_gas_price().compat().await); - + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); let tx_to_send = TransactionRequest { from: my_address, to, gas: Some(gas), - gas_price: Some(gas_price), + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, value: Some(value), data: Some(data.clone().into()), nonce: None, @@ -2444,12 +2637,29 @@ async fn sign_raw_eth_tx(coin: &EthCoin, args: &SignEthTransactionParams) -> Raw .mm_err(|e| RawTransactionError::InternalError(e.to_string()))?; let address_lock = coin.get_address_lock(my_address.to_string()).await; let _nonce_lock = address_lock.lock().await; - return sign_transaction_with_keypair(coin, key_pair, value, action, data, args.gas_limit, my_address) - .await - .map(|(signed_tx, _)| RawTransactionRes { - tx_hex: signed_tx.tx_hex().into(), - }) - .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())); + let pay_for_gas_option = if let Some(ref pay_for_gas) = args.pay_for_gas { + pay_for_gas.clone().try_into()? + } else { + // use legacy gas_price() if not set + info!(target: "sign-and-send", "get_gas_price…"); + let gas_price = coin.get_gas_price().await?; + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) + }; + return sign_transaction_with_keypair( + coin, + key_pair, + value, + action, + data, + args.gas_limit, + &pay_for_gas_option, + my_address, + ) + .await + .map(|(signed_tx, _)| RawTransactionRes { + tx_hex: signed_tx.tx_hex().into(), + }) + .map_to_mm(|err| RawTransactionError::TransactionError(err.get_plain_text_format())); }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(RawTransactionError::InvalidParam( @@ -2873,10 +3083,17 @@ impl EthCoin { Some(r) => { let gas_used = r.gas_used.unwrap_or_default(); let gas_price = web3_tx.gas_price.unwrap_or_default(); - // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail - // due to `u256_to_big_decimal` only. + // TODO: create and use EthTxFeeDetails::from(web3_tx) + // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail due to `u256_to_big_decimal` only. // Also TX history is not used by any GUI and has significant disadvantages. - Some(EthTxFeeDetails::new(gas_used, gas_price, fee_coin).unwrap()) + Some( + EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .unwrap(), + ) }, None => None, }; @@ -2928,8 +3145,10 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: trace.block_number, - tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), - tx_hex: BytesJson(rlp::encode(&raw).to_vec()), + tx: TransactionData::new_signed( + BytesJson(rlp::encode(&raw).to_vec()), + format!("{:02x}", BytesJson(raw.tx_hash_as_bytes().to_vec())), + ), internal_id, timestamp: block.timestamp.into_or_max(), kmd_rewards: None, @@ -3263,7 +3482,14 @@ impl EthCoin { // It's relatively safe to unwrap `EthTxFeeDetails::new` as it may fail // due to `u256_to_big_decimal` only. // Also TX history is not used by any GUI and has significant disadvantages. - Some(EthTxFeeDetails::new(gas_used, gas_price, fee_coin).unwrap()) + Some( + EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .unwrap(), + ) }, None => None, }; @@ -3299,8 +3525,10 @@ impl EthCoin { coin: self.ticker.clone(), fee_details: fee_details.map(|d| d.into()), block_height: block_number.as_u64(), - tx_hash: format!("{:02x}", BytesJson(raw.hash.as_bytes().to_vec())), - tx_hex: BytesJson(rlp::encode(&raw).to_vec()), + tx: TransactionData::new_signed( + BytesJson(rlp::encode(&raw).to_vec()), + format!("{:02x}", BytesJson(raw.tx_hash_as_bytes().to_vec())), + ), internal_id: BytesJson(internal_id.to_vec()), timestamp: block.timestamp.into_or_max(), kmd_rewards: None, @@ -3337,6 +3565,18 @@ impl EthCoin { } } + /// Returns tx type as number if this type supported by this coin + fn is_tx_type_supported(&self, tx_type: &TxType) -> bool { + let tx_type_as_num = match tx_type { + TxType::Legacy => 0_u64, + TxType::Type1 => 1_u64, + TxType::Type2 => 2_u64, + TxType::Invalid => return false, + }; + let max_tx_type = self.max_eth_tx_type.unwrap_or(0_u64); + tx_type_as_num <= max_tx_type + } + /// Retrieves the lock associated with a given address. /// /// This function is used to ensure that only one transaction is sent at a time per address. @@ -3354,6 +3594,8 @@ impl EthCoin { #[cfg_attr(test, mockable)] impl EthCoin { + /// Sign and send eth transaction. + /// This function is primarily for swap transactions so internally it relies on the swap tx fee policy pub(crate) fn sign_and_send_transaction(&self, value: U256, action: Action, data: Vec, gas: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { @@ -3382,7 +3624,12 @@ impl EthCoin { pub fn send_to_address(&self, address: Address, value: U256) -> EthTxFut { match &self.coin_type { - EthCoinType::Eth => self.sign_and_send_transaction(value, Call(address), vec![], U256::from(21000)), + EthCoinType::Eth => self.sign_and_send_transaction( + value, + Action::Call(address), + vec![], + U256::from(self.gas_limit.eth_send_coins), + ), EthCoinType::Erc20 { platform: _, token_addr, @@ -3390,7 +3637,12 @@ impl EthCoin { let abi = try_tx_fus!(Contract::load(ERC20_ABI.as_bytes())); let function = try_tx_fus!(abi.function("transfer")); let data = try_tx_fus!(function.encode_input(&[Token::Address(address), Token::Uint(value)])); - self.sign_and_send_transaction(0.into(), Call(*token_addr), data, U256::from(210_000)) + self.sign_and_send_transaction( + 0.into(), + Action::Call(*token_addr), + data, + U256::from(self.gas_limit.eth_send_erc20), + ) }, EthCoinType::Nft { .. } => { return Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( @@ -3407,7 +3659,6 @@ impl EthCoin { let trade_amount = try_tx_fus!(wei_from_big_decimal(&args.amount, self.decimals)); let time_lock = U256::from(args.time_lock); - let gas = U256::from(ETH_GAS); let secret_hash = if args.secret_hash.len() == 32 { ripemd160(args.secret_hash).to_vec() @@ -3445,8 +3696,8 @@ impl EthCoin { Token::Uint(time_lock), ])), }; - - self.sign_and_send_transaction(value, Call(swap_contract_address), data, gas) + let gas = U256::from(self.gas_limit.eth_payment); + self.sign_and_send_transaction(value, Action::Call(swap_contract_address), data, gas) }, EthCoinType::Erc20 { platform: _, @@ -3516,6 +3767,7 @@ impl EthCoin { }; let wait_for_required_allowance_until = args.wait_for_confirmation_until; + let gas = U256::from(self.gas_limit.erc20_payment); let arc = self.clone(); Box::new(allowance_fut.and_then(move |allowed| -> EthTxFut { @@ -3533,7 +3785,7 @@ impl EthCoin { .map_err(move |e| { TransactionErr::Plain(ERRL!( "Allowed value was not updated in time after sending approve transaction {:02x}: {}", - approved.tx_hash(), + approved.tx_hash_as_bytes(), e )) }) @@ -3566,7 +3818,7 @@ impl EthCoin { } fn watcher_spends_hash_time_locked_payment(&self, input: SendMakerPaymentSpendPreimageInput) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(input.preimage)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(input.preimage)); let payment = try_tx_fus!(SignedEthTx::new(tx)); let function_name = get_function_name("receiverSpend", input.watcher_reward); @@ -3574,8 +3826,8 @@ impl EthCoin { let clone = self.clone(); let secret_vec = input.secret.to_vec(); let taker_addr = addr_from_raw_pubkey(input.taker_pub).unwrap(); - let swap_contract_address = match payment.action { - Call(address) => address, + let swap_contract_address = match payment.unsigned().action() { + Call(address) => *address, Create => { return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" @@ -3588,7 +3840,7 @@ impl EthCoin { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let state_f = self.payment_status(swap_contract_address, swap_id_input.clone()); @@ -3604,7 +3856,7 @@ impl EthCoin { )))); } - let value = payment.value; + let value = payment.unsigned().value(); let reward_target = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); let sends_contract_reward = try_tx_fus!(get_function_input_data(&decoded, payment_func, 5)); let watcher_reward_amount = try_tx_fus!(get_function_input_data(&decoded, payment_func, 6)); @@ -3625,7 +3877,7 @@ impl EthCoin { 0.into(), Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.eth_receiver_spend), ) }), ) @@ -3637,7 +3889,7 @@ impl EthCoin { let function_name = get_function_name("erc20Payment", watcher_reward); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); @@ -3673,7 +3925,7 @@ impl EthCoin { 0.into(), Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.erc20_receiver_spend), ) }), ) @@ -3687,7 +3939,7 @@ impl EthCoin { } fn watcher_refunds_hash_time_locked_payment(&self, args: RefundPaymentArgs) -> EthTxFut { - let tx: UnverifiedTransaction = try_tx_fus!(rlp::decode(args.payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(args.payment_tx)); let payment = try_tx_fus!(SignedEthTx::new(tx)); let function_name = get_function_name("senderRefund", true); @@ -3695,8 +3947,8 @@ impl EthCoin { let clone = self.clone(); let taker_addr = addr_from_raw_pubkey(args.other_pubkey).unwrap(); - let swap_contract_address = match payment.action { - Call(address) => address, + let swap_contract_address = match payment.unsigned().action() { + Call(address) => *address, Create => { return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" @@ -3708,7 +3960,7 @@ impl EthCoin { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", true); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let receiver_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); let hash_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 2)); @@ -3726,7 +3978,7 @@ impl EthCoin { )))); } - let value = payment.value; + let value = payment.unsigned().value(); let reward_target = try_tx_fus!(get_function_input_data(&decoded, payment_func, 4)); let sends_contract_reward = try_tx_fus!(get_function_input_data(&decoded, payment_func, 5)); let reward_amount = try_tx_fus!(get_function_input_data(&decoded, payment_func, 6)); @@ -3747,7 +3999,7 @@ impl EthCoin { 0.into(), Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.eth_sender_refund), ) }), ) @@ -3759,7 +4011,7 @@ impl EthCoin { let function_name = get_function_name("erc20Payment", true); let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_fus!(decode_contract_call(payment_func, payment.unsigned().data())); let swap_id_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 0)); let amount_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 1)); let receiver_input = try_tx_fus!(get_function_input_data(&decoded, payment_func, 3)); @@ -3798,7 +4050,7 @@ impl EthCoin { 0.into(), Call(swap_contract_address), data, - U256::from(ETH_GAS), + U256::from(clone.gas_limit.erc20_sender_refund), ) }), ) @@ -3815,7 +4067,7 @@ impl EthCoin { &self, args: SpendPaymentArgs<'a>, ) -> Result { - let tx: UnverifiedTransaction = try_tx_s!(rlp::decode(args.other_payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.other_payment_tx)); let payment = try_tx_s!(SignedEthTx::new(tx)); let my_address = try_tx_s!(self.derivation_method.single_addr_or_err().await); let swap_contract_address = try_tx_s!(args.swap_contract_address.try_to_address()); @@ -3830,7 +4082,7 @@ impl EthCoin { EthCoinType::Eth => { let function_name = get_function_name("ethPayment", watcher_reward); let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); let state = try_tx_s!( self.payment_status(swap_contract_address, decoded[0].clone()) @@ -3848,7 +4100,7 @@ impl EthCoin { let data = if watcher_reward { try_tx_s!(spend_func.encode_input(&[ decoded[0].clone(), - Token::Uint(payment.value), + Token::Uint(payment.unsigned().value()), Token::FixedBytes(secret_vec), Token::Address(Address::default()), Token::Address(payment.sender()), @@ -3860,16 +4112,21 @@ impl EthCoin { } else { try_tx_s!(spend_func.encode_input(&[ decoded[0].clone(), - Token::Uint(payment.value), + Token::Uint(payment.unsigned().value()), Token::FixedBytes(secret_vec), Token::Address(Address::default()), Token::Address(payment.sender()), ])) }; - self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) - .compat() - .await + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.eth_receiver_spend), + ) + .compat() + .await }, EthCoinType::Erc20 { platform: _, @@ -3878,7 +4135,7 @@ impl EthCoin { let function_name = get_function_name("erc20Payment", watcher_reward); let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); let state = try_tx_s!( self.payment_status(swap_contract_address, decoded[0].clone()) .compat() @@ -3914,9 +4171,14 @@ impl EthCoin { ])) }; - self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) - .compat() - .await + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.erc20_receiver_spend), + ) + .compat() + .await }, EthCoinType::Nft { .. } => { return Err(TransactionErr::ProtocolNotSupported(ERRL!( @@ -3930,7 +4192,7 @@ impl EthCoin { &self, args: RefundPaymentArgs<'a>, ) -> Result { - let tx: UnverifiedTransaction = try_tx_s!(rlp::decode(args.payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.payment_tx)); let payment = try_tx_s!(SignedEthTx::new(tx)); let my_address = try_tx_s!(self.derivation_method.single_addr_or_err().await); let swap_contract_address = try_tx_s!(args.swap_contract_address.try_to_address()); @@ -3944,7 +4206,7 @@ impl EthCoin { let function_name = get_function_name("ethPayment", watcher_reward); let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); let state = try_tx_s!( self.payment_status(swap_contract_address, decoded[0].clone()) @@ -3959,7 +4221,7 @@ impl EthCoin { ))); } - let value = payment.value; + let value = payment.unsigned().value(); let data = if watcher_reward { try_tx_s!(refund_func.encode_input(&[ decoded[0].clone(), @@ -3982,9 +4244,14 @@ impl EthCoin { ])) }; - self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) - .compat() - .await + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.eth_sender_refund), + ) + .compat() + .await }, EthCoinType::Erc20 { platform: _, @@ -3993,7 +4260,7 @@ impl EthCoin { let function_name = get_function_name("erc20Payment", watcher_reward); let payment_func = try_tx_s!(SWAP_CONTRACT.function(&function_name)); - let decoded = try_tx_s!(decode_contract_call(payment_func, &payment.data)); + let decoded = try_tx_s!(decode_contract_call(payment_func, payment.unsigned().data())); let state = try_tx_s!( self.payment_status(swap_contract_address, decoded[0].clone()) .compat() @@ -4029,9 +4296,14 @@ impl EthCoin { ])) }; - self.sign_and_send_transaction(0.into(), Call(swap_contract_address), data, U256::from(ETH_GAS)) - .compat() - .await + self.sign_and_send_transaction( + 0.into(), + Call(swap_contract_address), + data, + U256::from(self.gas_limit.erc20_sender_refund), + ) + .compat() + .await }, EthCoinType::Nft { .. } => { return Err(TransactionErr::ProtocolNotSupported(ERRL!( @@ -4082,8 +4354,11 @@ impl EthCoin { address: Address, ) -> Result> { let coin = || self; - let mut requests = Vec::new(); - for (token_ticker, info) in self.get_erc_tokens_infos() { + + let tokens = self.get_erc_tokens_infos(); + let mut requests = Vec::with_capacity(tokens.len()); + + for (token_ticker, info) in tokens { let fut = async move { let balance_as_u256 = coin() .get_token_balance_for_address(address, info.token_address) @@ -4207,29 +4482,26 @@ impl EthCoin { /// /// Also, note that the contract call has to be initiated by my wallet address, /// because [`CallRequest::from`] is set to [`EthCoinImpl::my_address`]. - fn estimate_gas_for_contract_call(&self, contract_addr: Address, call_data: Bytes) -> Web3RpcFut { + async fn estimate_gas_for_contract_call(&self, contract_addr: Address, call_data: Bytes) -> Web3RpcResult { let coin = self.clone(); - let fut = async move { - let my_address = coin.derivation_method.single_addr_or_err().await?; - let gas_price = coin.get_gas_price().compat().await?; - let eth_value = U256::zero(); - let estimate_gas_req = CallRequest { - value: Some(eth_value), - data: Some(call_data), - from: Some(my_address), - to: Some(contract_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - coin.estimate_gas_wrapper(estimate_gas_req) - .compat() - .await - .map_to_mm(Web3RpcError::from) + let my_address = coin.derivation_method.single_addr_or_err().await?; + let fee_policy_for_estimate = get_swap_fee_policy_for_estimate(self.get_swap_transaction_fee_policy()); + let pay_for_gas_option = coin.get_swap_pay_for_gas_option(fee_policy_for_estimate).await?; + let eth_value = U256::zero(); + let estimate_gas_req = CallRequest { + value: Some(eth_value), + data: Some(call_data), + from: Some(my_address), + to: Some(contract_addr), + ..CallRequest::default() }; - Box::new(fut.boxed().compat()) + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option); + coin.estimate_gas_wrapper(estimate_gas_req) + .compat() + .await + .map_to_mm(Web3RpcError::from) } fn eth_balance(&self) -> BalanceFut { @@ -4345,7 +4617,6 @@ impl EthCoin { let gas_limit = try_tx_s!( coin.estimate_gas_for_contract_call(token_addr, Bytes::from(data.clone())) - .compat() .await ); @@ -4404,7 +4675,7 @@ impl EthCoin { .try_to_address() .map_to_mm(ValidatePaymentError::InvalidParameter)); - let unsigned: UnverifiedTransaction = try_f!(rlp::decode(&input.payment_tx)); + let unsigned: UnverifiedTransactionWrapper = try_f!(rlp::decode(&input.payment_tx)); let tx = try_f!(SignedEthTx::new(unsigned) .map_to_mm(|err| ValidatePaymentError::TxDeserializationError(err.to_string()))); @@ -4436,9 +4707,9 @@ impl EthCoin { ))); } - let tx_from_rpc = selfi.transaction(TransactionId::Hash(tx.hash)).await?; + let tx_from_rpc = selfi.transaction(TransactionId::Hash(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", tx.hash)) + ValidatePaymentError::TxDoesNotExist(format!("Didn't find provided tx {:?} on ETH node", tx.tx_hash())) })?; if tx_from_rpc.from != Some(sender) { @@ -4726,7 +4997,7 @@ impl EthCoin { search_from_block: u64, watcher_reward: bool, ) -> Result, String> { - let unverified: UnverifiedTransaction = try_s!(rlp::decode(tx)); + let unverified: UnverifiedTransactionWrapper = try_s!(rlp::decode(tx)); let tx = try_s!(SignedEthTx::new(unverified)); let func_name = match self.coin_type { @@ -4736,7 +5007,7 @@ impl EthCoin { }; let payment_func = try_s!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_s!(decode_contract_call(payment_func, &tx.data)); + let decoded = try_s!(decode_contract_call(payment_func, tx.unsigned().data())); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), invalid_token => return ERR!("Expected Token::FixedBytes, got {:?}", invalid_token), @@ -4812,48 +5083,39 @@ impl EthCoin { } pub async fn get_watcher_reward_amount(&self, wait_until: u64) -> Result> { - let gas_price = repeatable!(async { self.get_gas_price().compat().await.retry_on_err() }) - .until_s(wait_until) - .repeat_every_secs(10.) - .await - .map_err(|_| WatcherRewardError::RPCError("Error getting the gas price".to_string()))?; + let pay_for_gas_option = repeatable!(async { + self.get_swap_pay_for_gas_option(self.get_swap_transaction_fee_policy()) + .await + .retry_on_err() + }) + .until_s(wait_until) + .repeat_every_secs(10.) + .await + .map_err(|_| WatcherRewardError::RPCError("Error getting the gas price".to_string()))?; - let gas_cost_wei = U256::from(REWARD_GAS_AMOUNT) * gas_price; + let gas_cost_wei = calc_total_fee(U256::from(REWARD_GAS_AMOUNT), &pay_for_gas_option) + .map_err(|e| WatcherRewardError::InternalError(e.to_string()))?; let gas_cost_eth = u256_to_big_decimal(gas_cost_wei, ETH_DECIMALS) .map_err(|e| WatcherRewardError::InternalError(e.to_string()))?; Ok(gas_cost_eth) } /// Get gas price - pub fn get_gas_price(&self) -> Web3RpcFut { + pub async fn get_gas_price(&self) -> Web3RpcResult { let coin = self.clone(); - let fut = async move { - // TODO refactor to error_log_passthrough once simple maker bot is merged - let gas_station_price = match &coin.gas_station_url { - Some(url) => { - match GasStationData::get_gas_price(url, coin.gas_station_decimals, coin.gas_station_policy.clone()) - .compat() - .await - { - Ok(from_station) => Some(increase_by_percent_one_gwei(from_station, GAS_PRICE_PERCENT)), - Err(e) => { - error!("Error {} on request to gas station url {}", e, url); - None - }, - } - }, - None => None, - }; - - let eth_gas_price = match coin.gas_price().await { + let eth_gas_price_fut = async { + match coin.gas_price().await { Ok(eth_gas) => Some(eth_gas), Err(e) => { error!("Error {} on eth_gasPrice request", e); None }, - }; + } + } + .boxed(); - let eth_fee_history_price = match coin.eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]).await { + let eth_fee_history_price_fut = async { + match coin.eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[]).await { Ok(res) => res .base_fee_per_gas .first() @@ -4862,16 +5124,83 @@ impl EthCoin { debug!("Error {} on eth_feeHistory request", e); None }, - }; + } + } + .boxed(); + + let (eth_gas_price, eth_fee_history_price) = join(eth_gas_price_fut, eth_fee_history_price_fut).await; + // on editions < 2021 the compiler will resolve array.into_iter() as (&array).into_iter() + // https://doc.rust-lang.org/edition-guide/rust-2021/IntoIterator-for-arrays.html#details + IntoIterator::into_iter([eth_gas_price, eth_fee_history_price]) + .flatten() + .max() + .or_mm_err(|| Web3RpcError::Internal("All requests failed".into())) + } - // on editions < 2021 the compiler will resolve array.into_iter() as (&array).into_iter() - // https://doc.rust-lang.org/edition-guide/rust-2021/IntoIterator-for-arrays.html#details - IntoIterator::into_iter([gas_station_price, eth_gas_price, eth_fee_history_price]) - .flatten() - .max() - .or_mm_err(|| Web3RpcError::Internal("All requests failed".into())) + /// Get gas base fee and suggest priority tip fees for the next block (see EIP-1559) + pub async fn get_eip1559_gas_fee(&self) -> Web3RpcResult { + let coin = self.clone(); + let history_estimator_fut = FeePerGasSimpleEstimator::estimate_fee_by_history(&coin); + let ctx = + MmArc::from_weak(&coin.ctx).ok_or_else(|| MmError::new(Web3RpcError::Internal("ctx is null".into())))?; + let gas_api_conf = ctx.conf["gas_api"].clone(); + if gas_api_conf.is_null() { + debug!("No eth gas api provider config, using only history estimator"); + return history_estimator_fut + .await + .map_err(|e| MmError::new(Web3RpcError::Internal(e.to_string()))); + } + let gas_api_conf: GasApiConfig = json::from_value(gas_api_conf) + .map_err(|e| MmError::new(Web3RpcError::InvalidGasApiConfig(e.to_string())))?; + let provider_estimator_fut = match gas_api_conf.provider { + GasApiProvider::Infura => InfuraGasApiCaller::fetch_infura_fee_estimation(&gas_api_conf.url).boxed(), + GasApiProvider::Blocknative => { + BlocknativeGasApiCaller::fetch_blocknative_fee_estimation(&gas_api_conf.url).boxed() + }, }; - Box::new(fut.boxed().compat()) + provider_estimator_fut + .or_else(|provider_estimator_err| { + debug!( + "Call to eth gas api provider failed {}, using internal fee estimator", + provider_estimator_err + ); + history_estimator_fut.map_err(move |history_estimator_err| { + MmError::new(Web3RpcError::Internal(format!( + "All gas api requests failed, provider estimator error: {}, history estimator error: {}", + provider_estimator_err, history_estimator_err + ))) + }) + }) + .await + } + + async fn get_swap_pay_for_gas_option(&self, swap_fee_policy: SwapTxFeePolicy) -> Web3RpcResult { + let coin = self.clone(); + match swap_fee_policy { + SwapTxFeePolicy::Internal => { + let gas_price = coin.get_gas_price().await?; + Ok(PayForGasOption::Legacy(LegacyGasPrice { gas_price })) + }, + SwapTxFeePolicy::Low | SwapTxFeePolicy::Medium | SwapTxFeePolicy::High => { + let fee_per_gas = coin.get_eip1559_gas_fee().await?; + let pay_result = match swap_fee_policy { + SwapTxFeePolicy::Low => PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: fee_per_gas.low.max_fee_per_gas, + max_priority_fee_per_gas: fee_per_gas.low.max_priority_fee_per_gas, + }), + SwapTxFeePolicy::Medium => PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: fee_per_gas.medium.max_fee_per_gas, + max_priority_fee_per_gas: fee_per_gas.medium.max_priority_fee_per_gas, + }), + _ => PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas: fee_per_gas.high.max_fee_per_gas, + max_priority_fee_per_gas: fee_per_gas.high.max_priority_fee_per_gas, + }), + }; + Ok(pay_result) + }, + SwapTxFeePolicy::Unsupported => Err(MmError::new(Web3RpcError::Internal("swap fee policy not set".into()))), + } } /// Checks every second till at least one ETH node recognizes that nonce is increased. @@ -5096,24 +5425,46 @@ impl EthCoin { pub struct EthTxFeeDetails { pub coin: String, pub gas: u64, - /// WEI units per 1 gas + /// Gas price in ETH per gas unit + /// if 'max_fee_per_gas' and 'max_priority_fee_per_gas' are used we set 'gas_price' as 'max_fee_per_gas' for compatibility with GUI pub gas_price: BigDecimal, + /// Max fee per gas in ETH per gas unit + pub max_fee_per_gas: Option, + /// Max priority fee per gas in ETH per gas unit + pub max_priority_fee_per_gas: Option, pub total_fee: BigDecimal, } impl EthTxFeeDetails { - pub(crate) fn new(gas: U256, gas_price: U256, coin: &str) -> NumConversResult { - let total_fee = gas * gas_price; + pub(crate) fn new(gas: U256, pay_for_gas_option: PayForGasOption, coin: &str) -> NumConversResult { + let total_fee = calc_total_fee(gas, &pay_for_gas_option)?; // Fees are always paid in ETH, can use 18 decimals by default let total_fee = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; + let (gas_price, max_fee_per_gas, max_priority_fee_per_gas) = match pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => (gas_price, None, None), + // Using max_fee_per_gas as estimated gas_price value for compatibility in caller not expecting eip1559 fee per gas values. + // Normally the caller should pay attention to presence of max_fee_per_gas and max_priority_fee_per_gas in the result: + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) => (max_fee_per_gas, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)), + }; let gas_price = u256_to_big_decimal(gas_price, ETH_DECIMALS)?; - + let (max_fee_per_gas, max_priority_fee_per_gas) = match (max_fee_per_gas, max_priority_fee_per_gas) { + (Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => ( + Some(u256_to_big_decimal(max_fee_per_gas, ETH_DECIMALS)?), + Some(u256_to_big_decimal(max_priority_fee_per_gas, ETH_DECIMALS)?), + ), + (_, _) => (None, None), + }; let gas_u64 = u64::try_from(gas).map_to_mm(|e| NumConversError::new(e.to_string()))?; Ok(EthTxFeeDetails { coin: coin.to_owned(), gas: gas_u64, gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, total_fee, }) } @@ -5199,21 +5550,27 @@ impl MmCoin for EthCoin { fn get_trade_fee(&self) -> Box + Send> { let coin = self.clone(); Box::new( - self.get_gas_price() - .map_err(|e| e.to_string()) - .and_then(move |gas_price| { - let fee = gas_price * U256::from(ETH_GAS); - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, - EthCoinType::Erc20 { platform, .. } => platform, - EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), - }; - Ok(TradeFee { - coin: fee_coin.into(), - amount: try_s!(u256_to_big_decimal(fee, ETH_DECIMALS)).into(), - paid_from_trading_vol: false, - }) - }), + async move { + let pay_for_gas_option = coin + .get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await + .map_err(|e| e.to_string())?; + + let fee = calc_total_fee(U256::from(coin.gas_limit.eth_max_trade_gas), &pay_for_gas_option) + .map_err(|e| e.to_string())?; + let fee_coin = match &coin.coin_type { + EthCoinType::Eth => &coin.ticker, + EthCoinType::Erc20 { platform, .. } => platform, + EthCoinType::Nft { .. } => return ERR!("Nft Protocol is not supported yet!"), + }; + Ok(TradeFee { + coin: fee_coin.into(), + amount: try_s!(u256_to_big_decimal(fee, ETH_DECIMALS)).into(), + paid_from_trading_vol: false, + }) + } + .boxed() + .compat(), ) } @@ -5221,15 +5578,23 @@ impl MmCoin for EthCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + include_refund_fee: bool, ) -> TradePreimageResult { - let gas_price = self.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); + let pay_for_gas_option = self + .get_swap_pay_for_gas_option(self.get_swap_transaction_fee_policy()) + .await?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); let gas_limit = match self.coin_type { EthCoinType::Eth => { - // this gas_limit includes gas for `ethPayment` and `senderRefund` contract calls - U256::from(300_000) + // this gas_limit includes gas for `ethPayment` and optionally `senderRefund` contract calls + if include_refund_fee { + U256::from(self.gas_limit.eth_payment) + U256::from(self.gas_limit.eth_sender_refund) + } else { + U256::from(self.gas_limit.eth_payment) + } }, EthCoinType::Erc20 { token_addr, .. } => { + let mut gas = U256::from(self.gas_limit.erc20_payment); let value = match value { TradePreimageValue::Exact(value) | TradePreimageValue::UpperBound(value) => { wei_from_big_decimal(&value, self.decimals)? @@ -5245,20 +5610,21 @@ impl MmCoin for EthCoin { let approve_data = approve_function.encode_input(&[Token::Address(spender), Token::Uint(value)])?; let approve_gas_limit = self .estimate_gas_for_contract_call(token_addr, Bytes::from(approve_data)) - .compat() .await?; - // this gas_limit includes gas for `approve`, `erc20Payment` and `senderRefund` contract calls - U256::from(300_000) + approve_gas_limit - } else { - // this gas_limit includes gas for `erc20Payment` and `senderRefund` contract calls - U256::from(300_000) + // this gas_limit includes gas for `approve`, `erc20Payment` contract calls + gas += approve_gas_limit; } + // add 'senderRefund' gas if requested + if include_refund_fee { + gas += U256::from(self.gas_limit.erc20_sender_refund); + } + gas }, EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; - let total_fee = gas_limit * gas_price; + let total_fee = calc_total_fee(gas_limit, &pay_for_gas_option)?; let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; let fee_coin = match &self.coin_type { EthCoinType::Eth => &self.ticker, @@ -5275,15 +5641,22 @@ impl MmCoin for EthCoin { fn get_receiver_trade_fee(&self, stage: FeeApproxStage) -> TradePreimageFut { let coin = self.clone(); let fut = async move { - let gas_price = coin.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); - let total_fee = gas_price * U256::from(ETH_GAS); - let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; - let fee_coin = match &coin.coin_type { - EthCoinType::Eth => &coin.ticker, - EthCoinType::Erc20 { platform, .. } => platform, + let pay_for_gas_option = coin + .get_swap_pay_for_gas_option(coin.get_swap_transaction_fee_policy()) + .await?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); + let (fee_coin, total_fee) = match &coin.coin_type { + EthCoinType::Eth => ( + &coin.ticker, + calc_total_fee(U256::from(coin.gas_limit.eth_receiver_spend), &pay_for_gas_option)?, + ), + EthCoinType::Erc20 { platform, .. } => ( + platform, + calc_total_fee(U256::from(coin.gas_limit.erc20_receiver_spend), &pay_for_gas_option)?, + ), EthCoinType::Nft { .. } => return MmError::err(TradePreimageError::NftProtocolNotSupported), }; + let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; Ok(TradeFee { coin: fee_coin.into(), amount: amount.into(), @@ -5314,24 +5687,24 @@ impl MmCoin for EthCoin { }; let my_address = self.derivation_method.single_addr_or_err().await?; - let gas_price = self.get_gas_price().compat().await?; - let gas_price = increase_gas_price_by_stage(gas_price, &stage); + let fee_policy_for_estimate = get_swap_fee_policy_for_estimate(self.get_swap_transaction_fee_policy()); + let pay_for_gas_option = self.get_swap_pay_for_gas_option(fee_policy_for_estimate).await?; + let pay_for_gas_option = increase_gas_price_by_stage(pay_for_gas_option, &stage); let estimate_gas_req = CallRequest { value: Some(eth_value), data: Some(data.clone().into()), from: Some(my_address), to: Some(*call_addr), gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), ..CallRequest::default() }; - + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + let estimate_gas_req = call_request_with_pay_for_gas_option(estimate_gas_req, pay_for_gas_option.clone()); // Please note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. // Ideally we should determine the case when we have the insufficient balance and return `TradePreimageError::NotSufficientBalance` error. let gas_limit = self.estimate_gas_wrapper(estimate_gas_req).compat().await?; - let total_fee = gas_limit * gas_price; + let total_fee = calc_total_fee(gas_limit, &pay_for_gas_option)?; let amount = u256_to_big_decimal(total_fee, ETH_DECIMALS)?; Ok(TradeFee { coin: fee_coin.into(), @@ -5598,11 +5971,13 @@ fn display_u256_with_decimal_point(number: U256, decimals: u8) -> String { string.trim_end_matches('0').into() } +/// Converts 'number' to value with decimal point and shifts it left by 'decimals' places pub fn u256_to_big_decimal(number: U256, decimals: u8) -> NumConversResult { let string = display_u256_with_decimal_point(number, decimals); Ok(string.parse::()?) } +/// Shifts 'number' with decimal point right by 'decimals' places and converts it to U256 value pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResult { let mut amount = amount.to_string(); let dot = amount.find(|c| c == '.'); @@ -5625,70 +6000,130 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } - fn tx_hash(&self) -> BytesJson { self.hash.0.to_vec().into() } + fn tx_hash_as_bytes(&self) -> BytesJson { self.tx_hash().as_bytes().into() } } fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result { + // Local function to map the access list + fn map_access_list(web3_access_list: &Option>) -> ethcore_transaction::AccessList { + match web3_access_list { + Some(list) => ethcore_transaction::AccessList( + list.iter() + .map(|item| ethcore_transaction::AccessListItem { + address: item.address, + storage_keys: item.storage_keys.clone(), + }) + .collect(), + ), + None => ethcore_transaction::AccessList(vec![]), + } + } + + // Define transaction types + let type_0: ethereum_types::U64 = 0.into(); + let type_1: ethereum_types::U64 = 1.into(); + let type_2: ethereum_types::U64 = 2.into(); + + // Determine the transaction type + let tx_type = match transaction.transaction_type { + None => TxType::Legacy, + Some(t) if t == type_0 => TxType::Legacy, + Some(t) if t == type_1 => TxType::Type1, + Some(t) if t == type_2 => TxType::Type2, + _ => return Err(ERRL!("'Transaction::transaction_type' unsupported")), + }; + + // Determine the action based on the presence of 'to' field + let action = match transaction.to { + Some(addr) => Action::Call(addr), + None => Action::Create, + }; + + // Initialize the transaction builder + let tx_builder = UnSignedEthTxBuilder::new( + tx_type.clone(), + transaction.nonce, + transaction.gas, + action, + transaction.value, + transaction.input.0, + ); + + // Modify the builder based on the transaction type + let tx_builder = match tx_type { + TxType::Legacy => { + let gas_price = transaction + .gas_price + .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; + tx_builder.with_gas_price(gas_price) + }, + TxType::Type1 => { + let gas_price = transaction + .gas_price + .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; + let chain_id = transaction + .chain_id + .ok_or_else(|| ERRL!("'Transaction::chain_id' is not set"))? + .to_string() + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?; + tx_builder + .with_gas_price(gas_price) + .with_chain_id(chain_id) + .with_access_list(map_access_list(&transaction.access_list)) + }, + TxType::Type2 => { + let max_fee_per_gas = transaction + .max_fee_per_gas + .ok_or_else(|| ERRL!("'Transaction::max_fee_per_gas' is not set"))?; + let max_priority_fee_per_gas = transaction + .max_priority_fee_per_gas + .ok_or_else(|| ERRL!("'Transaction::max_priority_fee_per_gas' is not set"))?; + let chain_id = transaction + .chain_id + .ok_or_else(|| ERRL!("'Transaction::chain_id' is not set"))? + .to_string() + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?; + tx_builder + .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) + .with_chain_id(chain_id) + .with_access_list(map_access_list(&transaction.access_list)) + }, + TxType::Invalid => return Err(ERRL!("Internal error: 'tx_type' invalid")), + }; + + // Build the unsigned transaction + let unsigned = tx_builder.build().map_err(|err| err.to_string())?; + + // Extract signature components let r = transaction.r.ok_or_else(|| ERRL!("'Transaction::r' is not set"))?; let s = transaction.s.ok_or_else(|| ERRL!("'Transaction::s' is not set"))?; let v = transaction .v .ok_or_else(|| ERRL!("'Transaction::v' is not set"))? .as_u64(); - let gas_price = transaction - .gas_price - .ok_or_else(|| ERRL!("'Transaction::gas_price' is not set"))?; - - let unverified = UnverifiedTransaction { - r, - s, - v, - hash: transaction.hash, - unsigned: UnSignedEthTx { - data: transaction.input.0, - gas_price, - gas: transaction.gas, - value: transaction.value, - nonce: transaction.nonce, - action: match transaction.to { - Some(addr) => Call(addr), - None => Action::Create, - }, - }, + + // Create the signed transaction + let unverified = match unsigned { + TransactionWrapper::Legacy(unsigned) => UnverifiedTransactionWrapper::Legacy( + UnverifiedLegacyTransaction::new_with_network_v(unsigned, r, s, v, transaction.hash) + .map_err(|err| ERRL!("'Transaction::new' error {}", err.to_string()))?, + ), + TransactionWrapper::Eip2930(unsigned) => UnverifiedTransactionWrapper::Eip2930( + UnverifiedEip2930Transaction::new(unsigned, r, s, v, transaction.hash) + .map_err(|err| ERRL!("'Transaction::new' error {}", err.to_string()))?, + ), + TransactionWrapper::Eip1559(unsigned) => UnverifiedTransactionWrapper::Eip1559( + UnverifiedEip1559Transaction::new(unsigned, r, s, v, transaction.hash) + .map_err(|err| ERRL!("'Transaction::new' error {}", err.to_string()))?, + ), }; + // Return the signed transaction Ok(try_s!(SignedEthTx::new(unverified))) } -#[derive(Deserialize, Debug, Serialize)] -pub struct GasStationData { - // matic gas station average fees is named standard, using alias to support both format. - #[serde(alias = "average", alias = "standard")] - average: MmNumber, - fast: MmNumber, -} - -impl GasStationData { - fn average_gwei(&self, decimals: u8, gas_price_policy: GasStationPricePolicy) -> NumConversResult { - let gas_price = match gas_price_policy { - GasStationPricePolicy::MeanAverageFast => ((&self.average + &self.fast) / MmNumber::from(2)).into(), - GasStationPricePolicy::Average => self.average.to_decimal(), - }; - wei_from_big_decimal(&gas_price, decimals) - } - - fn get_gas_price(uri: &str, decimals: u8, gas_price_policy: GasStationPricePolicy) -> Web3RpcFut { - let uri = uri.to_owned(); - let fut = async move { - make_gas_station_request(&uri) - .await? - .average_gwei(decimals, gas_price_policy) - .mm_err(|e| Web3RpcError::Internal(e.0)) - }; - Box::new(fut.boxed().compat()) - } -} - async fn get_token_decimals(web3: &Web3, token_addr: Address) -> Result { let function = try_s!(ERC20_CONTRACT.function("decimals")); let data = try_s!(function.encode_input(&[])); @@ -5764,6 +6199,39 @@ fn rpc_event_handlers_for_eth_transport(ctx: &MmArc, ticker: String) -> Vec Result, String> { + fn check_max_eth_tx_type_conf(conf: &Json) -> Result, String> { + if !conf["max_eth_tx_type"].is_null() { + let max_eth_tx_type = conf["max_eth_tx_type"] + .as_u64() + .ok_or_else(|| "max_eth_tx_type in coins is invalid".to_string())?; + if max_eth_tx_type > ETH_MAX_TX_TYPE { + return Err("max_eth_tx_type in coins is too big".to_string()); + } + Ok(Some(max_eth_tx_type)) + } else { + Ok(None) + } + } + + match &coin_type { + EthCoinType::Eth => check_max_eth_tx_type_conf(conf), + EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => { + let coin_max_eth_tx_type = check_max_eth_tx_type_conf(conf)?; + // Normally we suppose max_eth_tx_type is in platform coin but also try to get it from tokens for tests to work: + if let Some(coin_max_eth_tx_type) = coin_max_eth_tx_type { + Ok(Some(coin_max_eth_tx_type)) + } else { + let platform_coin = lp_coinfind_or_err(ctx, platform).await; + match platform_coin { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin.max_eth_tx_type), + _ => Ok(None), + } + } + }, + } +} + #[inline] fn new_nonce_lock() -> HashMap>> { HashMap::new() } @@ -5918,10 +6386,6 @@ pub async fn eth_coin_from_conf_and_request( HistorySyncState::NotEnabled }; - let gas_station_decimals: Option = try_s!(json::from_value(req["gas_station_decimals"].clone())); - let gas_station_policy: GasStationPricePolicy = - json::from_value(req["gas_station_policy"].clone()).unwrap_or_default(); - let key_lock = match &coin_type { EthCoinType::Eth => String::from(ticker), EthCoinType::Erc20 { platform, .. } | EthCoinType::Nft { platform } => String::from(platform), @@ -5938,6 +6402,10 @@ pub async fn eth_coin_from_conf_and_request( // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = try_s!(ctx.abortable_system.create_subsystem()); + 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 coin = EthCoinImpl { priv_key_policy: key_pair, derivation_method: Arc::new(derivation_method), @@ -5948,11 +6416,10 @@ pub async fn eth_coin_from_conf_and_request( contract_supports_watchers, decimals, ticker: ticker.into(), - gas_station_url: try_s!(json::from_value(req["gas_station_url"].clone())), - gas_station_decimals: gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), - gas_station_policy, web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(initial_history_state), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, ctx: ctx.weak(), required_confirmations, chain_id, @@ -5961,6 +6428,8 @@ pub async fn eth_coin_from_conf_and_request( address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), + platform_fee_estimator_state, + gas_limit, abortable_system, }; @@ -6023,21 +6492,28 @@ fn increase_by_percent_one_gwei(num: U256, percent: u64) -> U256 { } } -fn increase_gas_price_by_stage(gas_price: U256, level: &FeeApproxStage) -> U256 { - match level { - FeeApproxStage::WithoutApprox => gas_price, - FeeApproxStage::StartSwap => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP) - }, - FeeApproxStage::OrderIssue => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE) - }, - FeeApproxStage::TradePreimage => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE) - }, - FeeApproxStage::WatcherPreimage => { - increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_WATCHER_PREIMAGE) - }, +fn increase_gas_price_by_stage(pay_for_gas_option: PayForGasOption, level: &FeeApproxStage) -> PayForGasOption { + if let PayForGasOption::Legacy(LegacyGasPrice { gas_price }) = pay_for_gas_option { + let new_gas_price = match level { + FeeApproxStage::WithoutApprox => gas_price, + FeeApproxStage::StartSwap => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_START_SWAP) + }, + FeeApproxStage::OrderIssue => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_ORDER_ISSUE) + }, + FeeApproxStage::TradePreimage => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_TRADE_PREIMAGE) + }, + FeeApproxStage::WatcherPreimage => { + increase_by_percent_one_gwei(gas_price, GAS_PRICE_APPROXIMATION_PERCENT_ON_WATCHER_PREIMAGE) + }, + }; + PayForGasOption::Legacy(LegacyGasPrice { + gas_price: new_gas_price, + }) + } else { + pay_for_gas_option } } @@ -6147,13 +6623,16 @@ impl From for EthGasDetailsErr { fn from(e: Web3RpcError) -> Self { match e { Web3RpcError::Transport(tr) | Web3RpcError::InvalidResponse(tr) => EthGasDetailsErr::Transport(tr), - Web3RpcError::Internal(internal) | Web3RpcError::Timeout(internal) => EthGasDetailsErr::Internal(internal), + Web3RpcError::Internal(internal) + | Web3RpcError::Timeout(internal) + | Web3RpcError::NumConversError(internal) + | Web3RpcError::InvalidGasApiConfig(internal) => EthGasDetailsErr::Internal(internal), Web3RpcError::NftProtocolNotSupported => EthGasDetailsErr::NftProtocolNotSupported, } } } -async fn get_eth_gas_details( +async fn get_eth_gas_details_from_withdraw_fee( eth_coin: &EthCoin, fee: Option, eth_value: U256, @@ -6162,38 +6641,134 @@ async fn get_eth_gas_details( call_addr: Address, fungible_max: bool, ) -> MmResult { - match fee { + let pay_for_gas_option = match fee { Some(WithdrawFee::EthGas { gas_price, gas }) => { - let gas_price = wei_from_big_decimal(&gas_price, 9)?; - Ok((gas.into(), gas_price)) + let gas_price = wei_from_big_decimal(&gas_price, ETH_GWEI_DECIMALS)?; + return Ok((gas.into(), PayForGasOption::Legacy(LegacyGasPrice { gas_price }))); + }, + Some(WithdrawFee::EthGasEip1559 { + max_fee_per_gas, + max_priority_fee_per_gas, + gas_option: gas_limit, + }) => { + let max_fee_per_gas = wei_from_big_decimal(&max_fee_per_gas, ETH_GWEI_DECIMALS)?; + let max_priority_fee_per_gas = wei_from_big_decimal(&max_priority_fee_per_gas, ETH_GWEI_DECIMALS)?; + match gas_limit { + EthGasLimitOption::Set(gas) => { + return Ok(( + gas.into(), + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }), + )) + }, + EthGasLimitOption::Calc => + // go to gas estimate code + { + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) + }, + } }, Some(fee_policy) => { let error = format!("Expected 'EthGas' fee type, found {:?}", fee_policy); - MmError::err(EthGasDetailsErr::InvalidFeePolicy(error)) + return MmError::err(EthGasDetailsErr::InvalidFeePolicy(error)); }, None => { - let gas_price = eth_coin.get_gas_price().compat().await?; - // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH - let eth_value_for_estimate = if fungible_max && eth_coin.coin_type == EthCoinType::Eth { - eth_value - gas_price * U256::from(21000) - } else { - eth_value - }; - let estimate_gas_req = CallRequest { - value: Some(eth_value_for_estimate), - data: Some(data), - from: Some(sender_address), - to: Some(call_addr), - gas: None, - // gas price must be supplied because some smart contracts base their - // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 - gas_price: Some(gas_price), - ..CallRequest::default() - }; - // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. - // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. - let gas_limit = eth_coin.estimate_gas_wrapper(estimate_gas_req).compat().await?; - Ok((gas_limit, gas_price)) + // If WithdrawFee not set use legacy gas price (?) + let gas_price = eth_coin.get_gas_price().await?; + // go to gas estimate code + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) + }, + }; + + // covering edge case by deducting the standard transfer fee when we want to max withdraw ETH + let eth_value_for_estimate = if fungible_max && eth_coin.coin_type == EthCoinType::Eth { + eth_value - calc_total_fee(U256::from(eth_coin.gas_limit.eth_send_coins), &pay_for_gas_option)? + } else { + eth_value + }; + + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); + let estimate_gas_req = CallRequest { + value: Some(eth_value_for_estimate), + data: Some(data), + from: Some(sender_address), + to: Some(call_addr), + gas: None, + // gas price must be supplied because some smart contracts base their + // logic on gas price, e.g. TUSD: https://github.com/KomodoPlatform/atomicDEX-API/issues/643 + gas_price, + max_priority_fee_per_gas, + max_fee_per_gas, + ..CallRequest::default() + }; + // TODO Note if the wallet's balance is insufficient to withdraw, then `estimate_gas` may fail with the `Exception` error. + // TODO Ideally we should determine the case when we have the insufficient balance and return `WithdrawError::NotSufficientBalance`. + let gas_limit = eth_coin.estimate_gas_wrapper(estimate_gas_req).compat().await?; + Ok((gas_limit, pay_for_gas_option)) +} + +/// Calc estimated total gas fee or price +fn calc_total_fee(gas: U256, pay_for_gas_option: &PayForGasOption) -> NumConversResult { + match *pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => gas + .checked_mul(gas_price) + .or_mm_err(|| NumConversError("total fee overflow".into())), + PayForGasOption::Eip1559(Eip1559FeePerGas { max_fee_per_gas, .. }) => gas + .checked_mul(max_fee_per_gas) + .or_mm_err(|| NumConversError("total fee overflow".into())), + } +} + +#[allow(clippy::result_large_err)] +fn tx_builder_with_pay_for_gas_option( + eth_coin: &EthCoin, + tx_builder: UnSignedEthTxBuilder, + pay_for_gas_option: &PayForGasOption, +) -> MmResult { + let tx_builder = match *pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => tx_builder.with_gas_price(gas_price), + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_priority_fee_per_gas, + max_fee_per_gas, + }) => tx_builder + .with_priority_fee_per_gas(max_fee_per_gas, max_priority_fee_per_gas) + .with_chain_id(eth_coin.chain_id), + }; + Ok(tx_builder) +} + +/// convert fee policy for gas estimate requests +fn get_swap_fee_policy_for_estimate(swap_fee_policy: SwapTxFeePolicy) -> SwapTxFeePolicy { + match swap_fee_policy { + SwapTxFeePolicy::Internal => SwapTxFeePolicy::Internal, + // always use 'high' for estimate to avoid max_fee_per_gas less than base_fee errors: + SwapTxFeePolicy::Low | SwapTxFeePolicy::Medium | SwapTxFeePolicy::High => SwapTxFeePolicy::High, + SwapTxFeePolicy::Unsupported => SwapTxFeePolicy::Unsupported, + } +} + +fn call_request_with_pay_for_gas_option(call_request: CallRequest, pay_for_gas_option: PayForGasOption) -> CallRequest { + match pay_for_gas_option { + PayForGasOption::Legacy(LegacyGasPrice { gas_price }) => CallRequest { + gas_price: Some(gas_price), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + ..call_request + }, + PayForGasOption::Eip1559(Eip1559FeePerGas { + max_fee_per_gas, + max_priority_fee_per_gas, + }) => CallRequest { + gas_price: None, + max_fee_per_gas: Some(max_fee_per_gas), + max_priority_fee_per_gas: Some(max_priority_fee_per_gas), + ..call_request }, } } @@ -6278,7 +6853,7 @@ impl ParseCoinAssocTypes for EthCoin { } fn parse_tx(&self, tx: &[u8]) -> Result { - let unverified: UnverifiedTransaction = rlp::decode(tx).map_err(EthAssocTypesError::from)?; + let unverified: UnverifiedTransactionWrapper = rlp::decode(tx).map_err(EthAssocTypesError::from)?; SignedEthTx::new(unverified).map_to_mm(|e| EthAssocTypesError::TxParseError(e.to_string())) } @@ -6482,3 +7057,19 @@ pub fn pubkey_from_extended(extended_pubkey: &Secp256k1ExtendedPublicKey) -> Pub pubkey_uncompressed.as_mut().copy_from_slice(&serialized[1..]); pubkey_uncompressed } + +fn extract_gas_limit_from_conf(coin_conf: &Json) -> Result { + if coin_conf["gas_limit"].is_null() { + Ok(Default::default()) + } else { + json::from_value(coin_conf["gas_limit"].clone()).map_err(|e| e.to_string()) + } +} + +impl Eip1559Ops for EthCoin { + fn get_swap_transaction_fee_policy(&self) -> SwapTxFeePolicy { self.swap_txfee_policy.lock().unwrap().clone() } + + fn set_swap_transaction_fee_policy(&self, swap_txfee_policy: SwapTxFeePolicy) { + *self.swap_txfee_policy.lock().unwrap() = swap_txfee_policy + } +} diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs new file mode 100644 index 0000000000..4d33781f39 --- /dev/null +++ b/mm2src/coins/eth/eip1559_gas_fee.rs @@ -0,0 +1,499 @@ +//! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider + +use super::web3_transport::FeeHistoryResult; +use super::{Web3RpcError, Web3RpcResult}; +use crate::{wei_from_gwei_decimal, wei_to_gwei_decimal, EthCoin, NumConversError}; +use ethereum_types::U256; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::or_mm_error::OrMmError; +use mm2_number::BigDecimal; +use num_traits::FromPrimitive; +use std::convert::TryFrom; +use url::Url; +use web3::types::BlockNumber; + +pub(crate) use gas_api::BlocknativeGasApiCaller; +pub(crate) use gas_api::InfuraGasApiCaller; + +use gas_api::{BlocknativeBlockPricesResponse, InfuraFeePerGas}; + +const FEE_PER_GAS_LEVELS: usize = 3; + +/// Indicates which provider was used to get fee per gas estimations +#[derive(Clone, Debug)] +pub enum EstimationSource { + /// filled by default values + Empty, + /// internal simple estimator + Simple, + Infura, + Blocknative, +} + +impl ToString for EstimationSource { + fn to_string(&self) -> String { + match self { + EstimationSource::Empty => "empty".into(), + EstimationSource::Simple => "simple".into(), + EstimationSource::Infura => "infura".into(), + EstimationSource::Blocknative => "blocknative".into(), + } + } +} + +impl Default for EstimationSource { + fn default() -> Self { Self::Empty } +} + +enum PriorityLevelId { + Low = 0, + Medium = 1, + High = 2, +} + +/// Supported gas api providers +#[derive(Deserialize)] +pub enum GasApiProvider { + Infura, + Blocknative, +} + +#[derive(Deserialize)] +pub struct GasApiConfig { + /// gas api provider name to use + pub provider: GasApiProvider, + /// gas api provider or proxy base url (scheme, host and port without the relative part) + pub url: Url, +} + +/// Priority level estimated max fee per gas +#[derive(Clone, Debug, Default)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in wei + pub max_priority_fee_per_gas: U256, + /// estimated max fee per gas in wei + pub max_fee_per_gas: U256, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// Internal struct for estimated fee per gas for several priority levels, in wei +/// low/medium/high levels are supported +#[derive(Default, Debug, Clone)] +pub struct FeePerGasEstimated { + /// base fee for the next block in wei + pub base_fee: U256, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: EstimationSource, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(infura_fees: InfuraFeePerGas) -> Result { + Ok(Self { + base_fee: wei_from_gwei_decimal!(&infura_fees.estimated_base_fee)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_priority_fee_per_gas)?, + min_wait_time: Some(infura_fees.low.min_wait_time_estimate), + max_wait_time: Some(infura_fees.low.max_wait_time_estimate), + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.medium.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &infura_fees.medium.suggested_max_priority_fee_per_gas + )?, + min_wait_time: Some(infura_fees.medium.min_wait_time_estimate), + max_wait_time: Some(infura_fees.medium.max_wait_time_estimate), + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_priority_fee_per_gas)?, + min_wait_time: Some(infura_fees.high.min_wait_time_estimate), + max_wait_time: Some(infura_fees.high.max_wait_time_estimate), + }, + source: EstimationSource::Infura, + base_fee_trend: infura_fees.base_fee_trend, + priority_fee_trend: infura_fees.priority_fee_trend, + }) + } +} + +impl TryFrom for FeePerGasEstimated { + type Error = MmError; + + fn try_from(block_prices: BlocknativeBlockPricesResponse) -> Result { + if block_prices.block_prices.is_empty() { + return Ok(FeePerGasEstimated::default()); + } + if block_prices.block_prices[0].estimated_prices.len() < FEE_PER_GAS_LEVELS { + return Ok(FeePerGasEstimated::default()); + } + Ok(Self { + base_fee: wei_from_gwei_decimal!(&block_prices.block_prices[0].base_fee_per_gas)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas + )?, + min_wait_time: None, + max_wait_time: None, + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas + )?, + min_wait_time: None, + max_wait_time: None, + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas + )?, + max_priority_fee_per_gas: wei_from_gwei_decimal!( + &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas + )?, + min_wait_time: None, + max_wait_time: None, + }, + source: EstimationSource::Blocknative, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} + +/// Simple priority fee per gas estimator based on fee history +/// normally used if gas api provider is not available +pub(crate) struct FeePerGasSimpleEstimator {} + +impl FeePerGasSimpleEstimator { + // TODO: add minimal max fee and priority fee + /// depth to look for fee history to estimate priority fees + const FEE_PRIORITY_DEPTH: u64 = 5u64; + + /// percentiles to pass to eth_feeHistory + const HISTORY_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [25.0, 50.0, 75.0]; + + /// percentile to predict next base fee over historical rewards + const BASE_FEE_PERCENTILE: f64 = 75.0; + + /// percentiles to calc max priority fee over historical rewards + const PRIORITY_FEE_PERCENTILES: [f64; FEE_PER_GAS_LEVELS] = [50.0, 50.0, 50.0]; + + /// adjustment for max fee per gas picked up by sampling + const ADJUST_MAX_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.1, 1.175, 1.25]; // 1.25 assures max_fee_per_gas will be over next block base_fee + + /// adjustment for max priority fee picked up by sampling + const ADJUST_MAX_PRIORITY_FEE: [f64; FEE_PER_GAS_LEVELS] = [1.0, 1.0, 1.0]; + + /// block depth for eth_feeHistory + pub fn history_depth() -> u64 { Self::FEE_PRIORITY_DEPTH } + + /// percentiles for priority rewards obtained with eth_feeHistory + pub fn history_percentiles() -> &'static [f64] { &Self::HISTORY_PERCENTILES } + + /// percentile for vector + fn percentile_of(v: &[U256], percent: f64) -> U256 { + let mut v_mut = v.to_owned(); + v_mut.sort(); + + // validate bounds: + let percent = if percent > 100.0 { 100.0 } else { percent }; + let percent = if percent < 0.0 { 0.0 } else { percent }; + + let value_pos = ((v_mut.len() - 1) as f64 * percent / 100.0).round() as usize; + v_mut[value_pos] + } + + /// Estimate simplified gas priority fees based on fee history + pub async fn estimate_fee_by_history(coin: &EthCoin) -> Web3RpcResult { + let res: Result = coin + .eth_fee_history( + U256::from(Self::history_depth()), + BlockNumber::Latest, + Self::history_percentiles(), + ) + .await; + + match res { + Ok(fee_history) => Ok(Self::calculate_with_history(&fee_history)?), + Err(_) => MmError::err(Web3RpcError::Internal("Eth requests failed".into())), + } + } + + fn predict_base_fee(base_fees: &[U256]) -> U256 { Self::percentile_of(base_fees, Self::BASE_FEE_PERCENTILE) } + + fn priority_fee_for_level( + level: PriorityLevelId, + base_fee: BigDecimal, + fee_history: &FeeHistoryResult, + ) -> Web3RpcResult { + let level_index = level as usize; + let level_rewards = fee_history + .priority_rewards + .as_ref() + .or_mm_err(|| Web3RpcError::Internal("expected reward in eth_feeHistory".into()))? + .iter() + .map(|rewards| rewards.get(level_index).copied().unwrap_or_else(|| U256::from(0))) + .collect::>(); + + // Calculate the max priority fee per gas based on the rewards percentile. + let max_priority_fee_per_gas = Self::percentile_of(&level_rewards, Self::PRIORITY_FEE_PERCENTILES[level_index]); + // Convert the priority fee to BigDecimal gwei, falling back to 0 on error. + let max_priority_fee_per_gas_gwei = + wei_to_gwei_decimal!(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); + + // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. + let adjust_max_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + let adjust_max_priority_fee = + BigDecimal::from_f64(Self::ADJUST_MAX_PRIORITY_FEE[level_index]).unwrap_or_else(|| BigDecimal::from(0)); + + // TODO: consider use checked ops + let max_fee_per_gas_dec = base_fee * adjust_max_fee + max_priority_fee_per_gas_gwei * adjust_max_priority_fee; + + Ok(FeePerGasLevel { + max_priority_fee_per_gas, + max_fee_per_gas: wei_from_gwei_decimal!(&max_fee_per_gas_dec)?, + // TODO: Consider adding default wait times if applicable (and mark them as uncertain). + min_wait_time: None, + max_wait_time: None, + }) + } + + /// estimate priority fees by fee history + fn calculate_with_history(fee_history: &FeeHistoryResult) -> Web3RpcResult { + // For estimation of max fee and max priority fee we use latest block base_fee but adjusted. + // Apparently for this simple fee estimator for assured high priority we should assume + // that the real base_fee may go up by 1,25 (i.e. if the block is full). This is covered by high priority ADJUST_MAX_FEE multiplier + let latest_base_fee = fee_history + .base_fee_per_gas + .first() + .cloned() + .unwrap_or_else(|| U256::from(0)); + let latest_base_fee_dec = wei_to_gwei_decimal!(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); + + // The predicted base fee is not used for calculating eip1559 values here and is provided for other purposes + // (f.e if the caller would like to do own estimates of max fee and max priority fee) + let predicted_base_fee = Self::predict_base_fee(&fee_history.base_fee_per_gas); + Ok(FeePerGasEstimated { + base_fee: predicted_base_fee, + low: Self::priority_fee_for_level(PriorityLevelId::Low, latest_base_fee_dec.clone(), fee_history)?, + medium: Self::priority_fee_for_level(PriorityLevelId::Medium, latest_base_fee_dec.clone(), fee_history)?, + high: Self::priority_fee_for_level(PriorityLevelId::High, latest_base_fee_dec, fee_history)?, + source: EstimationSource::Simple, + base_fee_trend: String::default(), + priority_fee_trend: String::default(), + }) + } +} + +mod gas_api { + use std::convert::TryInto; + + use super::FeePerGasEstimated; + use crate::eth::{Web3RpcError, Web3RpcResult}; + use http::StatusCode; + use mm2_err_handle::mm_error::MmError; + use mm2_err_handle::prelude::*; + use mm2_net::transport::slurp_url_with_headers; + use mm2_number::BigDecimal; + use serde_json::{self as json}; + use url::Url; + + lazy_static! { + /// API key for testing + static ref INFURA_GAS_API_AUTH_TEST: String = std::env::var("INFURA_GAS_API_AUTH_TEST").unwrap_or_default(); + } + + #[derive(Clone, Debug, Deserialize)] + pub(crate) struct InfuraFeePerGasLevel { + #[serde(rename = "suggestedMaxPriorityFeePerGas")] + pub suggested_max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "suggestedMaxFeePerGas")] + pub suggested_max_fee_per_gas: BigDecimal, + #[serde(rename = "minWaitTimeEstimate")] + pub min_wait_time_estimate: u32, + #[serde(rename = "maxWaitTimeEstimate")] + pub max_wait_time_estimate: u32, + } + + /// Infura gas api response + /// see https://docs.infura.io/api/infura-expansion-apis/gas-api/api-reference/gasprices-type2 + #[allow(dead_code)] + #[derive(Debug, Deserialize)] + pub(crate) struct InfuraFeePerGas { + pub low: InfuraFeePerGasLevel, + pub medium: InfuraFeePerGasLevel, + pub high: InfuraFeePerGasLevel, + #[serde(rename = "estimatedBaseFee")] + pub estimated_base_fee: BigDecimal, + #[serde(rename = "networkCongestion")] + pub network_congestion: BigDecimal, + #[serde(rename = "latestPriorityFeeRange")] + pub latest_priority_fee_range: Vec, + #[serde(rename = "historicalPriorityFeeRange")] + pub historical_priority_fee_range: Vec, + #[serde(rename = "historicalBaseFeeRange")] + pub historical_base_fee_range: Vec, + #[serde(rename = "priorityFeeTrend")] + pub priority_fee_trend: String, // we are not using enum here bcz values not mentioned in docs could be received + #[serde(rename = "baseFeeTrend")] + pub base_fee_trend: String, + } + + /// Infura gas api provider caller + #[allow(dead_code)] + pub(crate) struct InfuraGasApiCaller {} + + #[allow(dead_code)] + impl InfuraGasApiCaller { + const INFURA_GAS_FEES_ENDPOINT: &'static str = "networks/1/suggestedGasFees"; // Support only main chain + + fn get_infura_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::INFURA_GAS_FEES_ENDPOINT); + let headers = vec![("Authorization", INFURA_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_infura_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let estimated_fees = json::from_slice(&resp.2).map_to_mm(|e| e.to_string())?; + Ok(estimated_fees) + } + + /// Fetch fee per gas estimations from infura provider + pub async fn fetch_infura_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_infura_gas_api_url(base_url); + let infura_estimated_fees = Self::make_infura_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + infura_estimated_fees.try_into().mm_err(Into::into) + } + } + + lazy_static! { + /// API key for testing + static ref BLOCKNATIVE_GAS_API_AUTH_TEST: String = std::env::var("BLOCKNATIVE_GAS_API_AUTH_TEST").unwrap_or_default(); + } + + #[allow(dead_code)] + #[derive(Clone, Debug, Deserialize)] + pub(crate) struct BlocknativeBlockPrices { + #[serde(rename = "blockNumber")] + pub block_number: u32, + #[serde(rename = "estimatedTransactionCount")] + pub estimated_transaction_count: u32, + #[serde(rename = "baseFeePerGas")] + pub base_fee_per_gas: BigDecimal, + #[serde(rename = "estimatedPrices")] + pub estimated_prices: Vec, + } + + #[allow(dead_code)] + #[derive(Clone, Debug, Deserialize)] + pub(crate) struct BlocknativeEstimatedPrices { + pub confidence: u32, + pub price: BigDecimal, + #[serde(rename = "maxPriorityFeePerGas")] + pub max_priority_fee_per_gas: BigDecimal, + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: BigDecimal, + } + + /// Blocknative gas prices response + /// see https://docs.blocknative.com/gas-prediction/gas-platform + #[allow(dead_code)] + #[derive(Debug, Deserialize)] + pub(crate) struct BlocknativeBlockPricesResponse { + pub system: String, + pub network: String, + pub unit: String, + #[serde(rename = "maxPrice")] + pub max_price: BigDecimal, + #[serde(rename = "currentBlockNumber")] + pub current_block_number: u32, + #[serde(rename = "msSinceLastBlock")] + pub ms_since_last_block: u32, + #[serde(rename = "blockPrices")] + pub block_prices: Vec, + } + + /// Blocknative gas api provider caller + #[allow(dead_code)] + pub(crate) struct BlocknativeGasApiCaller {} + + #[allow(dead_code)] + impl BlocknativeGasApiCaller { + const BLOCKNATIVE_GAS_PRICES_ENDPOINT: &'static str = "gasprices/blockprices"; + const BLOCKNATIVE_GAS_PRICES_LOW: &'static str = "10"; + const BLOCKNATIVE_GAS_PRICES_MEDIUM: &'static str = "50"; + const BLOCKNATIVE_GAS_PRICES_HIGH: &'static str = "90"; + + fn get_blocknative_gas_api_url(base_url: &Url) -> (Url, Vec<(&'static str, &'static str)>) { + let mut url = base_url.clone(); + url.set_path(Self::BLOCKNATIVE_GAS_PRICES_ENDPOINT); + url.query_pairs_mut() + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_LOW) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_MEDIUM) + .append_pair("confidenceLevels", Self::BLOCKNATIVE_GAS_PRICES_HIGH) + .append_pair("withBaseFees", "true"); + + let headers = vec![("Authorization", BLOCKNATIVE_GAS_API_AUTH_TEST.as_str())]; + (url, headers) + } + + async fn make_blocknative_gas_api_request( + url: &Url, + headers: Vec<(&'static str, &'static str)>, + ) -> Result> { + let resp = slurp_url_with_headers(url.as_str(), headers) + .await + .mm_err(|e| e.to_string())?; + if resp.0 != StatusCode::OK { + let error = format!("{} failed with status code {}", url, resp.0); + return MmError::err(error); + } + let block_prices = json::from_slice(&resp.2).map_err(|e| e.to_string())?; + Ok(block_prices) + } + + /// Fetch fee per gas estimations from blocknative provider + pub async fn fetch_blocknative_fee_estimation(base_url: &Url) -> Web3RpcResult { + let (url, headers) = Self::get_blocknative_gas_api_url(base_url); + let block_prices = Self::make_blocknative_gas_api_request(&url, headers) + .await + .mm_err(Web3RpcError::Transport)?; + block_prices.try_into().mm_err(Into::into) + } + } +} diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 961f9a6e54..9332594931 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -17,6 +17,8 @@ const GAS_PRICE_APPROXIMATION_ON_START_SWAP: u64 = 51_500_000_000; const GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE: u64 = 52_500_000_000; // `GAS_PRICE` increased by 7% const GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE: u64 = 53_500_000_000; +// old way to add some extra gas to the returned value from gas station (non-existent now), still used in tests +const GAS_PRICE_PERCENT: u64 = 10; const TAKER_PAYMENT_SPEND_SEARCH_INTERVAL: f64 = 1.; @@ -196,39 +198,6 @@ fn test_wait_for_payment_spend_timeout() { .is_err()); } -#[test] -fn test_gas_station() { - make_gas_station_request.mock_safe(|_| { - let data = GasStationData { - average: 500.into(), - fast: 1000.into(), - }; - MockResult::Return(Box::pin(async move { Ok(data) })) - }); - let res_eth = GasStationData::get_gas_price( - "https://ethgasstation.info/api/ethgasAPI.json", - 8, - GasStationPricePolicy::MeanAverageFast, - ) - .wait() - .unwrap(); - let one_gwei = U256::from(10u64.pow(9)); - - let expected_eth_wei = U256::from(75) * one_gwei; - assert_eq!(expected_eth_wei, res_eth); - - let res_polygon = GasStationData::get_gas_price( - "https://gasstation-mainnet.matic.network/", - 9, - GasStationPricePolicy::Average, - ) - .wait() - .unwrap(); - - let expected_eth_polygon = U256::from(500) * one_gwei; - assert_eq!(expected_eth_polygon, res_polygon); -} - #[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_manual_fee() { @@ -247,10 +216,11 @@ fn test_withdraw_impl_manual_fee() { coin: "ETH".to_string(), max: false, fee: Some(WithdrawFee::EthGas { - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), memo: None, + ibc_source_channel: None, }; coin.get_balance().wait().unwrap(); @@ -259,8 +229,10 @@ fn test_withdraw_impl_manual_fee() { EthTxFeeDetails { coin: "ETH".into(), gas_price: "0.000000001".parse().unwrap(), - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, } .into(), ); @@ -293,10 +265,11 @@ fn test_withdraw_impl_fee_details() { coin: "JST".to_string(), max: false, fee: Some(WithdrawFee::EthGas { - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, gas_price: 1.into(), }), memo: None, + ibc_source_channel: None, }; coin.get_balance().wait().unwrap(); @@ -305,8 +278,10 @@ fn test_withdraw_impl_fee_details() { EthTxFeeDetails { coin: "ETH".into(), gas_price: "0.000000001".parse().unwrap(), - gas: ETH_GAS, + gas: gas_limit::ETH_MAX_TRADE_GAS, total_fee: "0.00015".parse().unwrap(), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, } .into(), ); @@ -334,8 +309,8 @@ fn test_add_ten_pct_one_gwei() { #[test] fn get_sender_trade_preimage() { /// Trade fee for the ETH coin is `2 * 150_000 * gas_price` always. - fn expected_fee(gas_price: u64) -> TradeFee { - let amount = u256_to_big_decimal((2 * ETH_GAS * gas_price).into(), 18).expect("!u256_to_big_decimal"); + fn expected_fee(gas_price: u64, gas_limit: u64) -> TradeFee { + let amount = u256_to_big_decimal((gas_limit * gas_price).into(), 18).expect("!u256_to_big_decimal"); TradeFee { coin: "ETH".to_owned(), amount: amount.into(), @@ -343,34 +318,46 @@ fn get_sender_trade_preimage() { } } - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::UpperBound(150.into()), FeeApproxStage::WithoutApprox, + true, )) .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE); + let expected = expected_fee(GAS_PRICE, gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND); assert_eq!(actual, expected); let value = u256_to_big_decimal(100.into(), 18).expect("!u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue)) - .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue, true)) + .expect("!get_sender_trade_fee"); + let expected = expected_fee( + GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE, + gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND, + ); assert_eq!(actual, expected); let value = u256_to_big_decimal(1.into(), 18).expect("!u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::StartSwap)) + let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::StartSwap, true)) .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE_APPROXIMATION_ON_START_SWAP); + let expected = expected_fee( + GAS_PRICE_APPROXIMATION_ON_START_SWAP, + gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND, + ); assert_eq!(actual, expected); let value = u256_to_big_decimal(10000000000u64.into(), 18).expect("!u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage)) - .expect("!get_sender_trade_fee"); - let expected = expected_fee(GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage, true)) + .expect("!get_sender_trade_fee"); + let expected = expected_fee( + GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE, + gas_limit::ETH_PAYMENT + gas_limit::ETH_SENDER_REFUND, + ); assert_eq!(actual, expected); } @@ -383,7 +370,7 @@ fn get_erc20_sender_trade_preimage() { EthCoin::allowance .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(unsafe { ALLOWANCE.into() })))); - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); EthCoin::estimate_gas_wrapper.mock_safe(|_, _| { unsafe { ESTIMATE_GAS_CALLED = true }; MockResult::Return(Box::new(futures01::future::ok(APPROVE_GAS_LIMIT.into()))) @@ -411,59 +398,78 @@ fn get_erc20_sender_trade_preimage() { // value is allowed unsafe { ALLOWANCE = 1000 }; let value = u256_to_big_decimal(1000.into(), 18).expect("u256_to_big_decimal"); - let actual = - block_on(coin.get_sender_trade_fee(TradePreimageValue::UpperBound(value), FeeApproxStage::WithoutApprox)) - .expect("!get_sender_trade_fee"); + let actual = block_on(coin.get_sender_trade_fee( + TradePreimageValue::UpperBound(value), + FeeApproxStage::WithoutApprox, + true, + )) + .expect("!get_sender_trade_fee"); log!("{:?}", actual.amount.to_decimal()); unsafe { assert!(!ESTIMATE_GAS_CALLED) } - assert_eq!(actual, expected_trade_fee(300_000, GAS_PRICE)); + assert_eq!( + actual, + expected_trade_fee(gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND, GAS_PRICE) + ); // value is greater than allowance unsafe { ALLOWANCE = 999 }; let value = u256_to_big_decimal(1000.into(), 18).expect("u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::UpperBound(value), FeeApproxStage::StartSwap)) - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::UpperBound(value), FeeApproxStage::StartSwap, true)) + .expect("!get_sender_trade_fee"); unsafe { assert!(ESTIMATE_GAS_CALLED); ESTIMATE_GAS_CALLED = false; } assert_eq!( actual, - expected_trade_fee(360_000, GAS_PRICE_APPROXIMATION_ON_START_SWAP) + expected_trade_fee( + gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND + APPROVE_GAS_LIMIT, + GAS_PRICE_APPROXIMATION_ON_START_SWAP + ) ); // value is allowed unsafe { ALLOWANCE = 1000 }; let value = u256_to_big_decimal(999.into(), 18).expect("u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue)) - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::OrderIssue, true)) + .expect("!get_sender_trade_fee"); unsafe { assert!(!ESTIMATE_GAS_CALLED) } assert_eq!( actual, - expected_trade_fee(300_000, GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE) + expected_trade_fee( + gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND, + GAS_PRICE_APPROXIMATION_ON_ORDER_ISSUE + ) ); // value is greater than allowance unsafe { ALLOWANCE = 1000 }; let value = u256_to_big_decimal(1500.into(), 18).expect("u256_to_big_decimal"); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage)) - .expect("!get_sender_trade_fee"); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(value), FeeApproxStage::TradePreimage, true)) + .expect("!get_sender_trade_fee"); unsafe { assert!(ESTIMATE_GAS_CALLED); ESTIMATE_GAS_CALLED = false; } assert_eq!( actual, - expected_trade_fee(360_000, GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE) + expected_trade_fee( + gas_limit::ERC20_PAYMENT + gas_limit::ERC20_SENDER_REFUND + APPROVE_GAS_LIMIT, + GAS_PRICE_APPROXIMATION_ON_TRADE_PREIMAGE + ) ); } #[test] fn get_receiver_trade_preimage() { - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, &["http://dummy.dummy"], None, ETH_SEPOLIA_CHAIN_ID); - let amount = u256_to_big_decimal((ETH_GAS * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); + let amount = + u256_to_big_decimal((gas_limit::ETH_RECEIVER_SPEND * GAS_PRICE).into(), 18).expect("!u256_to_big_decimal"); let expected_fee = TradeFee { coin: "ETH".to_owned(), amount: amount.into(), @@ -482,7 +488,7 @@ fn test_get_fee_to_send_taker_fee() { const DEX_FEE_AMOUNT: u64 = 100_000; const TRANSFER_GAS_LIMIT: u64 = 40_000; - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(GAS_PRICE.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(GAS_PRICE.into())))); EthCoin::estimate_gas_wrapper .mock_safe(|_, _| MockResult::Return(Box::new(futures01::future::ok(TRANSFER_GAS_LIMIT.into())))); @@ -532,7 +538,7 @@ fn test_get_fee_to_send_taker_fee() { fn test_get_fee_to_send_taker_fee_insufficient_balance() { const DEX_FEE_AMOUNT: u64 = 100_000_000_000; - EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::new(futures01::future::ok(40.into())))); + EthCoin::get_gas_price.mock_safe(|_| MockResult::Return(Box::pin(futures::future::ok(40.into())))); let (_ctx, coin) = eth_coin_for_test( EthCoinType::Erc20 { platform: "ETH".to_string(), @@ -815,7 +821,7 @@ fn polygon_check_if_my_payment_sent() { .unwrap() .unwrap(); let expected_hash = BytesJson::from("69a20008cea0c15ee483b5bbdff942752634aa072dfd2ff715fe87eec302de11"); - assert_eq!(expected_hash, my_payment.tx_hash()); + assert_eq!(expected_hash, my_payment.tx_hash_as_bytes()); } #[test] @@ -961,3 +967,60 @@ fn test_eth_validate_valid_and_invalid_pubkey() { assert!(coin.validate_other_pubkey(&[1u8; 20]).is_err()); assert!(coin.validate_other_pubkey(&[1u8; 8]).is_err()); } + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_fee_history() { + use mm2_test_helpers::for_tests::ETH_SEPOLIA_NODES; + + let (_ctx, coin) = eth_coin_for_test(EthCoinType::Eth, ETH_SEPOLIA_NODES, None, ETH_SEPOLIA_CHAIN_ID); + // check fee history without percentiles decoded okay + let res = block_on(coin.eth_fee_history(U256::from(1u64), BlockNumber::Latest, &[])); + assert!(res.is_ok()); +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_gas_limit_conf() { + use mm2_test_helpers::for_tests::ETH_SEPOLIA_SWAP_CONTRACT; + + let conf = json!({ + "coins": [{ + "coin": "ETH", + "name": "ethereum", + "fname": "Ethereum", + "chain_id": 1337, + "protocol":{ + "type": "ETH" + }, + "chain_id": 1, + "rpcport": 80, + "mm2": 1, + "gas_limit": { + "erc20_payment": 120000, + "erc20_receiver_spend": 130000, + "erc20_sender_refund": 110000 + } + }] + }); + + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123456").unwrap(); + + let req = json!({ + "urls":ETH_SEPOLIA_NODES, + "swap_contract_address":ETH_SEPOLIA_SWAP_CONTRACT + }); + let coin = block_on(lp_coininit(&ctx, "ETH", &req)).unwrap(); + let eth_coin = match coin { + MmCoinEnum::EthCoin(eth_coin) => eth_coin, + _ => panic!("not eth coin"), + }; + assert!( + eth_coin.gas_limit.eth_send_coins == 21_000 + && eth_coin.gas_limit.erc20_payment == 120000 + && eth_coin.gas_limit.erc20_receiver_spend == 130000 + && eth_coin.gas_limit.erc20_sender_refund == 110000 + && eth_coin.gas_limit.eth_max_trade_gas == 150_000 + ); +} diff --git a/mm2src/coins/eth/eth_wasm_tests.rs b/mm2src/coins/eth/eth_wasm_tests.rs index 24838ab9e0..a36e0ac45a 100644 --- a/mm2src/coins/eth/eth_wasm_tests.rs +++ b/mm2src/coins/eth/eth_wasm_tests.rs @@ -24,8 +24,10 @@ async fn init_eth_coin_helper() -> Result<(MmArc, MmCoinEnum), String> { "protocol":{ "type": "ETH" }, + "chain_id": 1, "rpcport": 80, - "mm2": 1 + "mm2": 1, + "max_eth_tx_type": 2 }] }); @@ -64,3 +66,31 @@ async fn wasm_test_sign_eth_tx() { console::log_1(&format!("res={:?}", res).into()); assert!(res.is_ok()); } + +#[wasm_bindgen_test] +async fn wasm_test_sign_eth_tx_with_priority_fee() { + // we need to hold ref to _ctx until the end of the test (because of the weak ref to MmCtx in EthCoinImpl) + let (_ctx, coin) = init_eth_coin_helper().await.unwrap(); + let sign_req = json::from_value(json!({ + "coin": "ETH", + "type": "ETH", + "tx": { + "to": "0x7Bc1bBDD6A0a722fC9bffC49c921B685ECB84b94".to_string(), + "value": "1.234", + "gas_limit": "21000", + "pay_for_gas": { + "tx_type": "Eip1559", + "max_fee_per_gas": "1234.567", + "max_priority_fee_per_gas": "1.2", + } + } + })) + .unwrap(); + let res = coin.sign_raw_tx(&sign_req).await; + console::log_1(&format!("res={:?}", res).into()); + assert!(res.is_ok()); + let tx: UnverifiedTransactionWrapper = rlp::decode(&res.unwrap().tx_hex).expect("decoding signed tx okay"); + if !matches!(tx, UnverifiedTransactionWrapper::Eip1559(..)) { + panic!("expected eip1559 tx"); + } +} diff --git a/mm2src/coins/eth/eth_withdraw.rs b/mm2src/coins/eth/eth_withdraw.rs index d059065985..b3de177a89 100644 --- a/mm2src/coins/eth/eth_withdraw.rs +++ b/mm2src/coins/eth/eth_withdraw.rs @@ -1,10 +1,12 @@ -use super::{checksum_address, get_eth_gas_details, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, - EthDerivationMethod, EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, - ERC20_CONTRACT, H160, H256}; -use crate::eth::{Action, Address, EthTxFeeDetails, KeyPair, SignedEthTx, UnSignedEthTx}; +use super::{checksum_address, u256_to_big_decimal, wei_from_big_decimal, EthCoinType, EthDerivationMethod, + EthPrivKeyPolicy, Public, WithdrawError, WithdrawRequest, WithdrawResult, ERC20_CONTRACT, H160, H256}; +use crate::eth::{calc_total_fee, get_eth_gas_details_from_withdraw_fee, tx_builder_with_pay_for_gas_option, + tx_type_from_pay_for_gas_option, Action, Address, EthTxFeeDetails, KeyPair, PayForGasOption, + SignedEthTx, TransactionWrapper, UnSignedEthTxBuilder}; use crate::hd_wallet::{HDCoinWithdrawOps, HDWalletOps, WithdrawFrom, WithdrawSenderAddress}; use crate::rpc_command::init_withdraw::{WithdrawInProgressStatus, WithdrawTaskHandleShared}; -use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionDetails}; +use crate::{BytesJson, CoinWithDerivationMethod, EthCoin, GetWithdrawSenderAddress, PrivKeyPolicy, TransactionData, + TransactionDetails}; use async_trait::async_trait; use bip32::DerivationPath; use common::custom_futures::timeout::FutureTimerExt; @@ -48,7 +50,7 @@ where async fn sign_tx_with_trezor( &self, derivation_path: &DerivationPath, - unsigned_tx: &UnSignedEthTx, + unsigned_tx: &TransactionWrapper, ) -> Result>; /// Transforms the `from` parameter of the withdrawal request into an address. @@ -120,22 +122,22 @@ where async fn sign_withdraw_tx( &self, req: &WithdrawRequest, - unsigned_tx: UnSignedEthTx, + unsigned_tx: TransactionWrapper, ) -> Result<(H256, BytesJson), MmError> { let coin = self.coin(); match coin.priv_key_policy { EthPrivKeyPolicy::Iguana(_) | EthPrivKeyPolicy::HDWallet { .. } => { let key_pair = self.get_key_pair(req)?; - let signed = unsigned_tx.sign(key_pair.secret(), Some(coin.chain_id)); + let signed = unsigned_tx.sign(key_pair.secret(), Some(coin.chain_id))?; let bytes = rlp::encode(&signed); - Ok((signed.hash, BytesJson::from(bytes.to_vec()))) + Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) }, EthPrivKeyPolicy::Trezor => { let derivation_path = self.get_withdraw_derivation_path(req).await?; let signed = self.sign_tx_with_trezor(&derivation_path, &unsigned_tx).await?; let bytes = rlp::encode(&signed); - Ok((signed.hash, BytesJson::from(bytes.to_vec()))) + Ok((signed.tx_hash(), BytesJson::from(bytes.to_vec()))) }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => MmError::err(WithdrawError::InternalError("invalid policy".to_owned())), @@ -222,7 +224,7 @@ where }; let eth_value_dec = u256_to_big_decimal(eth_value, coin.decimals)?; - let (gas, gas_price) = get_eth_gas_details( + let (gas, pay_for_gas_option) = get_eth_gas_details_from_withdraw_fee( coin, req.fee.clone(), eth_value, @@ -232,7 +234,7 @@ where false, ) .await?; - let total_fee = gas * gas_price; + let total_fee = calc_total_fee(gas, &pay_for_gas_option)?; let total_fee_dec = u256_to_big_decimal(total_fee, coin.decimals)?; if req.max && coin.coin_type == EthCoinType::Eth { @@ -260,23 +262,29 @@ where .await? .map_to_mm(WithdrawError::Transport)?; - let unsigned_tx = UnSignedEthTx { - nonce, - value: eth_value, - action: Action::Call(call_addr), - data: data.clone(), - gas, - gas_price, - }; + let tx_type = tx_type_from_pay_for_gas_option!(pay_for_gas_option); + if !coin.is_tx_type_supported(&tx_type) { + return MmError::err(WithdrawError::TxTypeNotSupported); + } + let tx_builder = + UnSignedEthTxBuilder::new(tx_type, nonce, gas, Action::Call(call_addr), eth_value, data); + let tx_builder = tx_builder_with_pay_for_gas_option(coin, tx_builder, &pay_for_gas_option)?; + let unsigned_tx = tx_builder + .build() + .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; self.sign_withdraw_tx(&req, unsigned_tx).await? }, #[cfg(target_arch = "wasm32")] EthPrivKeyPolicy::Metamask(_) => { + let gas_price = pay_for_gas_option.get_gas_price(); + let (max_fee_per_gas, max_priority_fee_per_gas) = pay_for_gas_option.get_fee_per_gas(); let tx_to_send = TransactionRequest { from: my_address, to: Some(to_addr), gas: Some(gas), - gas_price: Some(gas_price), + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, value: Some(eth_value), data: Some(data.into()), nonce: None, @@ -297,7 +305,7 @@ where } else { 0.into() }; - let fee_details = EthTxFeeDetails::new(gas, gas_price, fee_coin)?; + let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; if coin.coin_type == EthCoinType::Eth { spent_by_me += &fee_details.total_fee; } @@ -308,8 +316,7 @@ where my_balance_change: &received_by_me - &spent_by_me, spent_by_me, received_by_me, - tx_hex, - tx_hash: tx_hash_str, + tx: TransactionData::new_signed(tx_hex, tx_hash_str), block_height: 0, fee_details: Some(fee_details.into()), coin: coin.ticker.clone(), @@ -351,7 +358,7 @@ impl EthWithdraw for InitEthWithdraw { async fn sign_tx_with_trezor( &self, derivation_path: &DerivationPath, - unsigned_tx: &UnSignedEthTx, + unsigned_tx: &TransactionWrapper, ) -> Result> { let coin = self.coin(); let crypto_ctx = CryptoCtx::from_ctx(&self.ctx)?; @@ -410,7 +417,7 @@ impl EthWithdraw for StandardEthWithdraw { async fn sign_tx_with_trezor( &self, _derivation_path: &DerivationPath, - _unsigned_tx: &UnSignedEthTx, + _unsigned_tx: &TransactionWrapper, ) -> Result> { async { Err(MmError::new(WithdrawError::UnsupportedError(String::from( diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index 11a29fc741..72f3d080ad 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -49,14 +49,13 @@ pub(crate) fn eth_coin_from_keypair( EthCoinType::Nft { ref platform } => platform.to_string(), }; 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 eth_coin = EthCoin(Arc::new(EthCoinImpl { coin_type, decimals: 18, - gas_station_url: None, - gas_station_decimals: ETH_GAS_STATION_DECIMALS, history_sync_state: Mutex::new(HistorySyncState::NotEnabled), - gas_station_policy: GasStationPricePolicy::MeanAverageFast, sign_message_prefix: Some(String::from("Ethereum Signed Message:\n")), priv_key_policy: key_pair.into(), derivation_method: Arc::new(DerivationMethod::SingleAddress(my_address)), @@ -67,12 +66,16 @@ pub(crate) fn eth_coin_from_keypair( web3_instances: AsyncMutex::new(web3_instances), ctx: ctx.weak(), required_confirmations: 1.into(), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), chain_id, trezor_coin: None, logs_block_range: DEFAULT_LOGS_BLOCK_RANGE, address_nonce_locks: Arc::new(AsyncMutex::new(new_nonce_lock())), + max_eth_tx_type: None, erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(Default::default()), + platform_fee_estimator_state: Arc::new(FeeEstimatorState::CoinNotSupported), + gas_limit, 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 67ec624ff0..9e6afcbbcd 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -1,6 +1,6 @@ use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use ethabi::{Contract, Token}; -use ethcore_transaction::{Action, UnverifiedTransaction}; +use ethcore_transaction::{Action, UnverifiedTransactionWrapper}; use ethereum_types::{Address, U256}; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; @@ -15,7 +15,7 @@ use structs::{ExpectedHtlcParams, PaymentType, ValidationParams}; use super::ContractType; use crate::eth::{addr_from_raw_pubkey, decode_contract_call, EthCoin, EthCoinType, MakerPaymentStateV2, SignedEthTx, - TryToAddress, ERC1155_CONTRACT, ERC721_CONTRACT, ETH_GAS, NFT_SWAP_CONTRACT}; + TryToAddress, ERC1155_CONTRACT, ERC721_CONTRACT, NFT_SWAP_CONTRACT}; use crate::{ParseCoinAssocTypes, RefundPaymentArgs, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, TransactionErr, ValidateNftMakerPaymentArgs}; @@ -39,7 +39,7 @@ impl EthCoin { 0.into(), Action::Call(*args.nft_swap_info.token_address), data, - U256::from(ETH_GAS), + U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value ) .compat() .await @@ -79,12 +79,12 @@ impl EthCoin { ) .await?; let tx_from_rpc = self - .transaction(TransactionId::Hash(args.maker_payment_tx.hash)) + .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.hash + args.maker_payment_tx.tx_hash() )) })?; validate_from_to_and_maker_status(tx_from_rpc, maker_address, *token_address, maker_status).await?; @@ -138,7 +138,7 @@ impl EthCoin { let contract_type = args.contract_type; let (decoded, index_bytes) = try_tx_s!(get_decoded_tx_data_and_index_bytes( contract_type, - &args.maker_payment_tx.data + args.maker_payment_tx.unsigned().data() )); let (state, htlc_params) = try_tx_s!( @@ -154,9 +154,14 @@ impl EthCoin { match self.coin_type { EthCoinType::Nft { .. } => { let data = try_tx_s!(self.prepare_spend_nft_maker_v2_data(&args, decoded, htlc_params, state)); - self.sign_and_send_transaction(0.into(), Action::Call(*etomic_swap_contract), data, U256::from(ETH_GAS)) - .compat() - .await + self.sign_and_send_transaction( + 0.into(), + Action::Call(*etomic_swap_contract), + data, + U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + ) + .compat() + .await }, EthCoinType::Eth | EthCoinType::Erc20 { .. } => Err(TransactionErr::ProtocolNotSupported( "ETH and ERC20 Protocols are not supported for NFT Swaps".to_string(), @@ -169,7 +174,7 @@ impl EthCoin { args: RefundPaymentArgs<'_>, ) -> Result { let _etomic_swap_contract = try_tx_s!(args.swap_contract_address.try_to_address()); - let tx: UnverifiedTransaction = try_tx_s!(rlp::decode(args.payment_tx)); + let tx: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.payment_tx)); let _payment = try_tx_s!(SignedEthTx::new(tx)); todo!() } diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 5a79e19f9e..03b27654c0 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -127,6 +127,10 @@ impl From for EthActivationV2Error { fn from(e: ParseChainTypeError) -> Self { EthActivationV2Error::InternalError(e.to_string()) } } +impl From for EthActivationV2Error { + fn from(e: String) -> Self { EthActivationV2Error::InternalError(e) } +} + impl From for EthActivationV2Error { fn from(e: EnableCoinBalanceError) -> Self { match e { @@ -179,10 +183,6 @@ pub struct EthActivationV2Request { pub fallback_swap_contract: Option
, #[serde(default)] pub contract_supports_watchers: bool, - pub gas_station_url: Option, - pub gas_station_decimals: Option, - #[serde(default)] - pub gas_station_policy: GasStationPricePolicy, pub mm2: Option, pub required_confirmations: Option, #[serde(default)] @@ -256,6 +256,10 @@ impl From for EthTokenActivationError { fn from(e: ParseChainTypeError) -> Self { EthTokenActivationError::InternalError(e.to_string()) } } +impl From for EthTokenActivationError { + fn from(e: String) -> Self { EthTokenActivationError::InternalError(e) } +} + /// Represents the parameters required for activating either an ERC-20 token or an NFT on the Ethereum platform. #[derive(Clone, Deserialize)] #[serde(untagged)] @@ -390,27 +394,32 @@ impl EthCoin { // all spawned futures related to `ERC20` coin will be aborted as well. let abortable_system = ctx.abortable_system.create_subsystem()?; + let coin_type = EthCoinType::Erc20 { + platform: protocol.platform, + token_addr: protocol.token_addr, + }; + 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) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + let token = EthCoinImpl { priv_key_policy: self.priv_key_policy.clone(), // We inherit the derivation method from the parent/platform coin // If we want a new wallet for each token we can add this as an option in the future // storage ticker will be the platform coin ticker derivation_method: self.derivation_method.clone(), - coin_type: EthCoinType::Erc20 { - platform: protocol.platform, - token_addr: protocol.token_addr, - }, + coin_type, sign_message_prefix: self.sign_message_prefix.clone(), swap_contract_address: self.swap_contract_address, fallback_swap_contract: self.fallback_swap_contract, contract_supports_watchers: self.contract_supports_watchers, decimals, ticker, - gas_station_url: self.gas_station_url.clone(), - gas_station_decimals: self.gas_station_decimals, - gas_station_policy: self.gas_station_policy.clone(), web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, ctx: self.ctx.clone(), required_confirmations, chain_id: self.chain_id, @@ -419,6 +428,8 @@ impl EthCoin { address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), + platform_fee_estimator_state, + gas_limit, abortable_system, }; @@ -437,6 +448,12 @@ impl EthCoin { let chain = Chain::from_ticker(self.ticker())?; let ticker = chain.to_nft_ticker().to_string(); + let ctx = MmArc::from_weak(&self.ctx) + .ok_or_else(|| String::from("No context")) + .map_err(EthTokenActivationError::InternalError)?; + + let conf = coin_conf(&ctx, &ticker); + // Create an abortable system linked to the `platform_coin` (which is self) so if the platform coin is disabled, // all spawned futures related to global Non-Fungible Token will be aborted as well. let abortable_system = self.abortable_system.create_subsystem()?; @@ -444,12 +461,17 @@ impl EthCoin { // Todo: support HD wallet for NFTs, currently we get nfts for enabled address only and there might be some issues when activating NFTs while ETH is activated with HD wallet let my_address = self.derivation_method.single_addr_or_err().await?; let nft_infos = get_nfts_for_activation(&chain, &my_address, url).await?; + let coin_type = EthCoinType::Nft { + platform: self.ticker.clone(), + }; + 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) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; let global_nft = EthCoinImpl { ticker, - coin_type: EthCoinType::Nft { - platform: self.ticker.clone(), - }, + coin_type, priv_key_policy: self.priv_key_policy.clone(), derivation_method: self.derivation_method.clone(), sign_message_prefix: self.sign_message_prefix.clone(), @@ -458,10 +480,9 @@ impl EthCoin { contract_supports_watchers: self.contract_supports_watchers, web3_instances: self.web3_instances.lock().await.clone().into(), decimals: self.decimals, - gas_station_url: self.gas_station_url.clone(), - gas_station_decimals: self.gas_station_decimals, - gas_station_policy: self.gas_station_policy.clone(), history_sync_state: Mutex::new(self.history_sync_state.lock().unwrap().clone()), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, required_confirmations: AtomicU64::new(self.required_confirmations.load(Ordering::Relaxed)), ctx: self.ctx.clone(), chain_id: self.chain_id, @@ -470,6 +491,8 @@ impl EthCoin { address_nonce_locks: self.address_nonce_locks.clone(), erc20_tokens_infos: Default::default(), nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), + platform_fee_estimator_state, + gas_limit, abortable_system, }; Ok(EthCoin(Arc::new(global_nft))) @@ -579,22 +602,26 @@ pub async fn eth_coin_from_conf_and_request_v2( // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, // all spawned futures related to `ETH` coin will be aborted as well. let abortable_system = ctx.abortable_system.create_subsystem()?; + 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) + .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; let coin = EthCoinImpl { priv_key_policy, derivation_method: Arc::new(derivation_method), - coin_type: EthCoinType::Eth, + coin_type, sign_message_prefix, swap_contract_address: req.swap_contract_address, fallback_swap_contract: req.fallback_swap_contract, contract_supports_watchers: req.contract_supports_watchers, decimals: ETH_DECIMALS, ticker: ticker.to_string(), - gas_station_url: req.gas_station_url, - gas_station_decimals: req.gas_station_decimals.unwrap_or(ETH_GAS_STATION_DECIMALS), - gas_station_policy: req.gas_station_policy, web3_instances: AsyncMutex::new(web3_instances), history_sync_state: Mutex::new(HistorySyncState::NotEnabled), + swap_txfee_policy: Mutex::new(SwapTxFeePolicy::Internal), + max_eth_tx_type, ctx: ctx.weak(), required_confirmations, chain_id, @@ -603,6 +630,8 @@ pub async fn eth_coin_from_conf_and_request_v2( address_nonce_locks, erc20_tokens_infos: Default::default(), nfts_infos: Default::default(), + platform_fee_estimator_state, + gas_limit, abortable_system, }; diff --git a/mm2src/coins/eth/web3_transport/mod.rs b/mm2src/coins/eth/web3_transport/mod.rs index 30cb8b8dcb..dcbdf6ef90 100644 --- a/mm2src/coins/eth/web3_transport/mod.rs +++ b/mm2src/coins/eth/web3_transport/mod.rs @@ -128,6 +128,10 @@ pub struct FeeHistoryResult { pub oldest_block: U256, #[serde(rename = "baseFeePerGas")] pub base_fee_per_gas: Vec, + #[serde(rename = "gasUsedRatio")] + pub gas_used_ratio: Vec, + #[serde(rename = "reward")] + pub priority_rewards: Option>>, } /// Generates a signed message and inserts it into the request payload. diff --git a/mm2src/coins/hd_wallet/errors.rs b/mm2src/coins/hd_wallet/errors.rs index 6667fd2021..8b517bc609 100644 --- a/mm2src/coins/hd_wallet/errors.rs +++ b/mm2src/coins/hd_wallet/errors.rs @@ -156,6 +156,7 @@ impl From for AccountUpdatingError { fn from(e: HDWalletStorageError) -> Self { AccountUpdatingError::WalletStorageError(e) } } +#[derive(Display)] pub enum HDWithdrawError { UnexpectedFromAddress(String), UnknownAccount { account_id: u32 }, diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index f6ccf0363e..2feb7ef014 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -152,7 +152,7 @@ pub(crate) struct GetOpenChannelsResult { impl Transaction for PaymentHash { fn tx_hex(&self) -> Vec { self.0.to_vec() } - fn tx_hash(&self) -> BytesJson { self.0.to_vec().into() } + fn tx_hash_as_bytes(&self) -> BytesJson { self.0.to_vec().into() } } impl LightningCoin { @@ -610,7 +610,7 @@ impl LightningCoin { #[async_trait] impl SwapOps for LightningCoin { // Todo: This uses dummy data for now for the sake of swap P.O.C., this should be implemented probably after agreeing on how fees will work for lightning - fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let fut = async move { Ok(TransactionEnum::LightningPayment(PaymentHash([1; 32]))) }; Box::new(fut.boxed().compat()) } @@ -1336,6 +1336,7 @@ impl MmCoin for LightningCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { Ok(TradeFee { coin: self.ticker().to_owned(), diff --git a/mm2src/coins/lightning/ln_events.rs b/mm2src/coins/lightning/ln_events.rs index 3a761cc2b3..d6c78f9ad9 100644 --- a/mm2src/coins/lightning/ln_events.rs +++ b/mm2src/coins/lightning/ln_events.rs @@ -2,7 +2,6 @@ use super::*; use crate::lightning::ln_db::{DBChannelDetails, HTLCStatus, LightningDB, PaymentType}; use crate::lightning::ln_errors::{SaveChannelClosingError, SaveChannelClosingResult}; use crate::lightning::ln_sql::SqliteLightningDB; -use crate::utxo::UtxoCommonOps; use bitcoin::blockdata::script::Script; use bitcoin::blockdata::transaction::Transaction; use bitcoin::consensus::encode::serialize_hex; @@ -209,25 +208,15 @@ async fn sign_funding_transaction( }; unsigned.outputs[0].script_pubkey = output_script_pubkey.to_bytes().into(); - let my_address = coin - .as_ref() - .derivation_method - .single_addr_or_err() - .await - .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let key_pair = coin .as_ref() .priv_key_policy .activated_key_or_err() .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; - let prev_script = coin - .script_for_address(&my_address) - .map_err(|e| SignFundingTransactionError::Internal(e.to_string()))?; let signed = sign_tx( unsigned, key_pair, - prev_script, SignatureVersion::WitnessV0, coin.as_ref().conf.fork_id, ) diff --git a/mm2src/coins/lightning/ln_p2p.rs b/mm2src/coins/lightning/ln_p2p.rs index 7db3a0b4e2..64c7c940aa 100644 --- a/mm2src/coins/lightning/ln_p2p.rs +++ b/mm2src/coins/lightning/ln_p2p.rs @@ -110,7 +110,6 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { if addr == Ipv4Addr::new(0, 0, 0, 0) || addr == Ipv4Addr::new(127, 0, 0, 1) { return Vec::new(); } - let mut addresses = Vec::new(); let address = match addr { IpAddr::V4(addr) => NetAddress::IPv4 { addr: u32::from(addr).to_be_bytes(), @@ -121,8 +120,7 @@ fn netaddress_from_ipaddr(addr: IpAddr, port: u16) -> Vec { port, }, }; - addresses.push(address); - addresses + vec![address] } pub async fn ln_node_announcement_loop( diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index 51f683b313..4d0573af95 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -546,7 +546,7 @@ impl Platform { .await .map_to_mm(|e| SaveChannelClosingError::WaitForFundingTxSpendError(e.get_plain_text_format()))?; - let closing_tx_hash = format!("{:02x}", closing_tx.tx_hash()); + let closing_tx_hash = format!("{:02x}", closing_tx.tx_hash_as_bytes()); Ok(closing_tx_hash) } diff --git a/mm2src/coins/lightning/ln_sql.rs b/mm2src/coins/lightning/ln_sql.rs index 53eab1ac78..4b5dd2f7c8 100644 --- a/mm2src/coins/lightning/ln_sql.rs +++ b/mm2src/coins/lightning/ln_sql.rs @@ -609,11 +609,14 @@ pub struct SqliteLightningDB { } impl SqliteLightningDB { - pub fn new(ticker: String, sqlite_connection: SqliteConnShared) -> Self { - Self { - db_ticker: ticker.replace('-', "_"), + pub fn new(ticker: String, sqlite_connection: SqliteConnShared) -> Result { + let db_ticker = ticker.replace('-', "_"); + validate_table_name(&db_ticker)?; + + Ok(Self { + db_ticker, sqlite_connection, - } + }) } } @@ -1047,7 +1050,7 @@ mod tests { use super::*; use crate::lightning::ln_db::DBChannelDetails; use common::{block_on, new_uuid}; - use db_common::sqlite::rusqlite::Connection; + use db_common::sqlite::rusqlite::{self, Connection}; use rand::distributions::Alphanumeric; use rand::{Rng, RngCore}; use secp256k1v24::{Secp256k1, SecretKey}; @@ -1056,7 +1059,7 @@ mod tests { fn generate_random_channels(num: u64) -> Vec { let mut rng = rand::thread_rng(); - let mut channels = vec![]; + let mut channels = Vec::with_capacity(num.try_into().expect("Shouldn't overflow.")); let s = Secp256k1::new(); let mut bytes = [0; 32]; for _i in 0..num { @@ -1108,7 +1111,7 @@ mod tests { fn generate_random_payments(num: u64) -> Vec { let mut rng = rand::thread_rng(); - let mut payments = vec![]; + let mut payments = Vec::with_capacity(num.try_into().expect("Shouldn't overflow.")); let s = Secp256k1::new(); let mut bytes = [0; 32]; for _ in 0..num { @@ -1157,7 +1160,8 @@ mod tests { let db = SqliteLightningDB::new( "init_sql_collection".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); let initialized = block_on(db.is_db_initialized()).unwrap(); assert!(!initialized); @@ -1174,7 +1178,8 @@ mod tests { let db = SqliteLightningDB::new( "add_get_channel".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1282,7 +1287,8 @@ mod tests { let db = SqliteLightningDB::new( "add_get_payment".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1371,7 +1377,8 @@ mod tests { let db = SqliteLightningDB::new( "test_get_payments_by_filter".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); @@ -1485,12 +1492,48 @@ mod tests { assert_eq!(expected_payments, actual_payments); } + #[test] + fn test_invalid_lightning_db_name() { + let db = SqliteLightningDB::new("123".into(), Mutex::new(Connection::open_in_memory().unwrap()).into()); + + let expected = || { + SqlError::SqliteFailure( + rusqlite::ffi::Error { + code: rusqlite::ErrorCode::ApiMisuse, + extended_code: rusqlite::ffi::SQLITE_MISUSE, + }, + None, + ) + }; + + assert_eq!(db.err(), Some(expected())); + + let db = SqliteLightningDB::new( + "t".repeat(u8::MAX as usize + 1), + Mutex::new(Connection::open_in_memory().unwrap()).into(), + ); + + assert_eq!(db.err(), Some(expected())); + + let db = SqliteLightningDB::new( + "PROCEDURE".to_owned(), + Mutex::new(Connection::open_in_memory().unwrap()).into(), + ); + + assert_eq!(db.err(), Some(expected())); + + let db = SqliteLightningDB::new(String::new(), Mutex::new(Connection::open_in_memory().unwrap()).into()); + + assert_eq!(db.err(), Some(expected())); + } + #[test] fn test_get_channels_by_filter() { let db = SqliteLightningDB::new( "test_get_channels_by_filter".into(), Arc::new(Mutex::new(Connection::open_in_memory().unwrap())), - ); + ) + .unwrap(); block_on(db.init_db()).unwrap(); diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 88af1d68cc..693b7c3a4f 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -76,7 +76,7 @@ pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult, zcash or komodo optional consensus branch id, used for signing transactions ahead of current height } +#[derive(Clone, Debug, Deserialize)] +pub struct LegacyGasPrice { + /// Gas price in decimal gwei + pub gas_price: BigDecimal, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Eip1559FeePerGas { + /// Max fee per gas in decimal gwei + pub max_fee_per_gas: BigDecimal, + /// Max priority fee per gas in decimal gwei + pub max_priority_fee_per_gas: BigDecimal, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "tx_type")] +pub enum PayForGasParams { + Legacy(LegacyGasPrice), + Eip1559(Eip1559FeePerGas), +} + /// sign_raw_transaction RPC request's params for signing raw eth transactions #[derive(Clone, Debug, Deserialize)] pub struct SignEthTransactionParams { @@ -481,6 +502,8 @@ pub struct SignEthTransactionParams { data: Option, /// Eth gas use limit gas_limit: U256, + /// Optional gas price or fee per gas params + pay_for_gas: Option, } #[derive(Clone, Debug, Deserialize)] @@ -577,7 +600,7 @@ pub trait Transaction: fmt::Debug + 'static { /// Raw transaction bytes of the transaction fn tx_hex(&self) -> Vec; /// Serializable representation of tx hash for displaying purpose - fn tx_hash(&self) -> BytesJson; + fn tx_hash_as_bytes(&self) -> BytesJson; } #[derive(Clone, Debug, PartialEq)] @@ -1040,7 +1063,7 @@ pub enum WatcherRewardError { /// Swap operations (mostly based on the Hash/Time locked transactions implemented by coin wallets). #[async_trait] pub trait SwapOps { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut; + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut; fn send_maker_payment(&self, maker_payment_args: SendPaymentArgs<'_>) -> TransactionFut; @@ -1913,6 +1936,15 @@ pub trait MarketCoinOps { fn is_trezor(&self) -> bool; } +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum EthGasLimitOption { + /// Use this value as gas limit + Set(u64), + /// Make MM2 calculate gas limit + Calc, +} + #[derive(Clone, Debug, Deserialize, PartialEq)] #[serde(tag = "type")] pub enum WithdrawFee { @@ -1927,6 +1959,12 @@ pub enum WithdrawFee { gas_price: BigDecimal, gas: u64, }, + EthGasEip1559 { + /// in gwei + max_priority_fee_per_gas: BigDecimal, + max_fee_per_gas: BigDecimal, + gas_option: EthGasLimitOption, + }, Qrc20Gas { /// in satoshi gas_limit: u64, @@ -1950,6 +1988,10 @@ pub trait GetWithdrawSenderAddress { ) -> MmResult, WithdrawError>; } +/// TODO: Avoid using a single request structure on every platform. +/// Instead, accept a generic type from withdraw implementations. +/// This way we won't have to update the payload for every platform when +/// one of them requires specific addition. #[derive(Clone, Deserialize)] pub struct WithdrawRequest { coin: String, @@ -1961,6 +2003,8 @@ pub struct WithdrawRequest { max: bool, fee: Option, memo: Option, + /// Tendermint specific field used for manually providing the IBC channel IDs. + ibc_source_channel: Option, /// Currently, this flag is used by ETH/ERC20 coins activated with MetaMask **only**. #[cfg(target_arch = "wasm32")] #[serde(default)] @@ -2015,6 +2059,7 @@ impl WithdrawRequest { max: true, fee: None, memo: None, + ibc_source_channel: None, #[cfg(target_arch = "wasm32")] broadcast: false, } @@ -2157,15 +2202,14 @@ pub enum TransactionType { token_id: Option, }, NftTransfer, + TendermintIBCTransfer, } /// Transaction details #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct TransactionDetails { - /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction - pub tx_hex: BytesJson, - /// Transaction hash in hexadecimal format - tx_hash: String, + #[serde(flatten)] + pub tx: TransactionData, /// Coins are sent from these addresses from: Vec, /// Coins are sent to these addresses @@ -2199,6 +2243,40 @@ pub struct TransactionDetails { memo: Option, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum TransactionData { + Signed { + /// Raw bytes of signed transaction, this should be sent as is to `send_raw_transaction_bytes` RPC to broadcast the transaction + tx_hex: BytesJson, + /// Transaction hash in hexadecimal format + tx_hash: String, + }, + /// This can contain entirely different data depending on the platform. + /// TODO: Perhaps using generics would be more suitable here? + Unsigned(Json), +} + +impl TransactionData { + pub fn new_signed(tx_hex: BytesJson, tx_hash: String) -> Self { Self::Signed { tx_hex, tx_hash } } + + pub fn new_unsigned(unsigned_tx_data: Json) -> Self { Self::Unsigned(unsigned_tx_data) } + + pub fn tx_hex(&self) -> Option<&BytesJson> { + match self { + TransactionData::Signed { tx_hex, .. } => Some(tx_hex), + TransactionData::Unsigned(_) => None, + } + } + + pub fn tx_hash(&self) -> Option<&str> { + match self { + TransactionData::Signed { tx_hash, .. } => Some(tx_hash), + TransactionData::Unsigned(_) => None, + } + } +} + #[derive(Clone, Copy, Debug)] pub struct BlockHeightAndTime { height: u64, @@ -2328,6 +2406,45 @@ pub enum TradePreimageValue { UpperBound(BigDecimal), } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum SwapTxFeePolicy { + Unsupported, + Internal, + Low, + Medium, + High, +} + +impl Default for SwapTxFeePolicy { + fn default() -> Self { SwapTxFeePolicy::Unsupported } +} + +#[derive(Debug, Deserialize)] +pub struct SwapTxFeePolicyRequest { + coin: String, + #[serde(default)] + swap_tx_fee_policy: SwapTxFeePolicy, +} + +#[derive(Debug, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum SwapTxFeePolicyError { + #[from_stringify("CoinFindError")] + NoSuchCoin(String), + #[display(fmt = "eip-1559 policy is not supported for coin {}", _0)] + NotSupported(String), +} + +impl HttpStatusCode for SwapTxFeePolicyError { + fn status_code(&self) -> StatusCode { + match self { + SwapTxFeePolicyError::NoSuchCoin(_) | SwapTxFeePolicyError::NotSupported(_) => StatusCode::BAD_REQUEST, + } + } +} + +pub type SwapTxFeePolicyResult = Result>; + #[derive(Debug, Display, EnumFromStringify, PartialEq)] pub enum TradePreimageError { #[display( @@ -2807,6 +2924,21 @@ pub enum WithdrawError { }, #[display(fmt = "Nft Protocol is not supported yet!")] NftProtocolNotSupported, + #[display(fmt = "Chain id must be set for typed transaction for coin {}", coin)] + NoChainIdSet { + coin: String, + }, + #[display(fmt = "Signing error {}", _0)] + SigningError(String), + #[display(fmt = "Eth transaction type not supported")] + TxTypeNotSupported, + #[display(fmt = "'chain_registry_name' was not found in coins configuration for '{}'", _0)] + RegistryNameIsMissing(String), + #[display( + fmt = "IBC channel could not found for '{}' address. Consider providing it manually with 'ibc_source_channel' in the request.", + _0 + )] + IBCChannelCouldNotFound(String), } impl HttpStatusCode for WithdrawError { @@ -2832,6 +2964,11 @@ impl HttpStatusCode for WithdrawError { | WithdrawError::ContractTypeDoesntSupportNftWithdrawing(_) | WithdrawError::CoinDoesntSupportNftWithdraw { .. } | WithdrawError::NotEnoughNftsAmount { .. } + | WithdrawError::NoChainIdSet { .. } + | WithdrawError::TxTypeNotSupported + | WithdrawError::SigningError(_) + | WithdrawError::RegistryNameIsMissing(_) + | WithdrawError::IBCChannelCouldNotFound(_) | WithdrawError::MyAddressNotNftOwner { .. } => StatusCode::BAD_REQUEST, WithdrawError::HwError(_) => StatusCode::GONE, #[cfg(target_arch = "wasm32")] @@ -3132,11 +3269,12 @@ pub trait MmCoin: /// Get fee to be paid per 1 swap transaction fn get_trade_fee(&self) -> Box + Send>; - /// Get fee to be paid by sender per whole swap using the sending value and check if the wallet has sufficient balance to pay the fee. + /// Get fee to be paid by sender per whole swap (including possible refund) using the sending value and check if the wallet has sufficient balance to pay the fee. async fn get_sender_trade_fee( &self, value: TradePreimageValue, stage: FeeApproxStage, + include_refund_fee: bool, ) -> TradePreimageResult; /// Get fee to be paid by receiver per whole swap and check if the wallet has sufficient balance to pay the fee. @@ -4704,6 +4842,12 @@ pub async fn my_tx_history(ctx: MmArc, req: Json) -> Result>, S Ok(try_s!(Response::builder().body(body))) } +/// `get_trade_fee` rpc implementation. +/// There is some consideration about this rpc: +/// for eth coin this rpc returns max possible trade fee (estimated for maximum possible gas limit for any kind of swap). +/// However for eth coin, as part of fixing this issue https://github.com/KomodoPlatform/komodo-defi-framework/issues/1848, +/// `max_taker_vol' and `trade_preimage` rpc now return more accurate required gas calculations. +/// So maybe it would be better to deprecate this `get_trade_fee` rpc pub async fn get_trade_fee(ctx: MmArc, req: Json) -> Result>, String> { let ticker = try_s!(req["coin"].as_str().ok_or("No 'coin' field")).to_owned(); let coin = match lp_coinfind(&ctx, &ticker).await { @@ -5214,6 +5358,41 @@ fn coins_conf_check(ctx: &MmArc, coins_en: &Json, ticker: &str, req: Option<&Jso Ok(()) } +#[async_trait] +pub trait Eip1559Ops { + /// Return swap transaction fee policy + fn get_swap_transaction_fee_policy(&self) -> SwapTxFeePolicy; + + /// set swap transaction fee policy + fn set_swap_transaction_fee_policy(&self, swap_txfee_policy: SwapTxFeePolicy); +} + +/// Get eip 1559 transaction fee per gas policy (low, medium, high) set for the coin +pub async fn get_swap_transaction_fee_policy(ctx: MmArc, req: SwapTxFeePolicyRequest) -> SwapTxFeePolicyResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::EthCoin(eth_coin) => Ok(eth_coin.get_swap_transaction_fee_policy()), + MmCoinEnum::Qrc20Coin(qrc20_coin) => Ok(qrc20_coin.get_swap_transaction_fee_policy()), + _ => MmError::err(SwapTxFeePolicyError::NotSupported(req.coin)), + } +} + +/// Set eip 1559 transaction fee per gas policy (low, medium, high) +pub async fn set_swap_transaction_fee_policy(ctx: MmArc, req: SwapTxFeePolicyRequest) -> SwapTxFeePolicyResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; + match coin { + MmCoinEnum::EthCoin(eth_coin) => { + eth_coin.set_swap_transaction_fee_policy(req.swap_tx_fee_policy); + Ok(eth_coin.get_swap_transaction_fee_policy()) + }, + MmCoinEnum::Qrc20Coin(qrc20_coin) => { + qrc20_coin.set_swap_transaction_fee_policy(req.swap_tx_fee_policy); + Ok(qrc20_coin.get_swap_transaction_fee_policy()) + }, + _ => MmError::err(SwapTxFeePolicyError::NotSupported(req.coin)), + } +} + /// Checks addresses that either had empty transaction history last time we checked or has not been checked before. /// The checking stops at the moment when we find `gap_limit` consecutive empty addresses. pub async fn scan_for_new_addresses_impl( @@ -5381,6 +5560,7 @@ pub mod for_tests { max: false, fee, memo: None, + ibc_source_channel: None, }; let init = init_withdraw(ctx.clone(), withdraw_req).await.unwrap(); let timeout = wait_until_ms(150000); diff --git a/mm2src/coins/my_tx_history_v2.rs b/mm2src/coins/my_tx_history_v2.rs index 517eb1ca47..20cfb9b8b1 100644 --- a/mm2src/coins/my_tx_history_v2.rs +++ b/mm2src/coins/my_tx_history_v2.rs @@ -5,8 +5,8 @@ use crate::tx_history_storage::{CreateTxHistoryStorageError, FilteringAddresses, use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; use crate::MyAddressError; use crate::{coin_conf, lp_coinfind_or_err, BlockHeightAndTime, CoinFindError, HDPathAccountToAddressId, - HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionDetails, TransactionType, TxFeeDetails, - UtxoRpcError}; + HistorySyncState, MmCoin, MmCoinEnum, Transaction, TransactionData, TransactionDetails, TransactionType, + TxFeeDetails, UtxoRpcError}; use async_trait::async_trait; use bitcrypto::sha256; use common::{calc_total_pages, ten, HttpStatusCode, PagingOptionsEnum, StatusCode}; @@ -217,7 +217,7 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T let mut to: Vec<_> = self.to_addresses.iter().map(DisplayAddress::display_address).collect(); to.sort(); - let tx_hash = self.tx.tx_hash(); + let tx_hash = self.tx.tx_hash_as_bytes(); let internal_id = match &self.transaction_type { TransactionType::TokenTransfer(token_id) => { let mut bytes_for_hash = tx_hash.0.clone(); @@ -237,13 +237,13 @@ impl<'a, Addr: Clone + DisplayAddress + Eq + std::hash::Hash, Tx: Transaction> T | TransactionType::RemoveDelegation | TransactionType::FeeForTokenTx | TransactionType::StandardTransfer - | TransactionType::NftTransfer => tx_hash.clone(), + | TransactionType::NftTransfer + | TransactionType::TendermintIBCTransfer => tx_hash.clone(), }; TransactionDetails { coin: self.coin, - tx_hex: self.tx.tx_hex().into(), - tx_hash: tx_hash.to_tx_hash(), + tx: TransactionData::new_signed(self.tx.tx_hex().into(), tx_hash.to_tx_hash()), from, to, total_amount: self.total_amount, diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 508016af54..7002f97fbf 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -8,16 +8,15 @@ pub(crate) mod storage; #[cfg(any(test, target_arch = "wasm32"))] mod nft_tests; -use crate::{coin_conf, get_my_address, lp_coinfind_or_err, CoinsContext, MarketCoinOps, MmCoinEnum, MmCoinStruct, - MyAddressReq, WithdrawError}; +use crate::{coin_conf, get_my_address, lp_coinfind_or_err, CoinsContext, HDPathAccountToAddressId, MarketCoinOps, + MmCoinEnum, MmCoinStruct, MyAddressReq, WithdrawError}; use nft_errors::{GetNftInfoError, UpdateNftError}; use nft_structs::{Chain, ContractType, ConvertChain, Nft, NftFromMoralis, NftList, NftListReq, NftMetadataReq, NftTransferHistory, NftTransferHistoryFromMoralis, NftTransfersReq, NftsTransferHistoryList, TransactionNftDetails, UpdateNftReq, WithdrawNftReq}; use crate::eth::{eth_addr_to_hex, get_eth_address, withdraw_erc1155, withdraw_erc721, EthCoin, EthCoinType, - EthTxFeeDetails}; -use crate::hd_wallet::HDPathAccountToAddressId; + EthTxFeeDetails, LegacyGasPrice, PayForGasOption}; use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamError, TransferConfirmationsError, UpdateSpamPhishingError}; use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, NftInfo, @@ -821,7 +820,12 @@ async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option { let gas_used = r.gas_used.unwrap_or_default(); match r.effective_gas_price { - Some(gas_price) => EthTxFeeDetails::new(gas_used, gas_price, fee_coin).ok(), + Some(gas_price) => EthTxFeeDetails::new( + gas_used, + PayForGasOption::Legacy(LegacyGasPrice { gas_price }), + fee_coin, + ) + .ok(), None => { let web3_tx = eth_coin .web3() @@ -832,7 +836,12 @@ async fn get_fee_details(eth_coin: &EthCoin, transaction_hash: &str) -> Option, { let mut filtered_nfts = Vec::new(); + for nft_table in nfts { let nft = nft_details_from_item(nft_table)?; match filters { @@ -82,6 +83,7 @@ where I: Iterator, { let mut filtered_transfers = Vec::new(); + for transfers_table in transfers { let transfer = transfer_details_from_item(transfers_table)?; match filters { @@ -705,7 +707,7 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { let table = db_transaction.table::().await?; let items = table.get_items("chain", chain.to_string()).await?; - let mut token_addresses = HashSet::new(); + let mut token_addresses = HashSet::with_capacity(items.len()); for (_item_id, item) in items.into_iter() { let transfer = transfer_details_from_item(item)?; token_addresses.insert(transfer.common.token_address); diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index d014e027e5..3ee9e7761b 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -17,19 +17,19 @@ use crate::utxo::{qtum, ActualTxFee, AdditionalTxData, AddrFromStrError, Broadca UtxoFromLegacyReqErr, UtxoTx, UtxoTxBroadcastOps, UtxoTxGenerationOps, VerboseTransactionFrom, UTXO_LOCK}; use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, - DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, + DexFee, Eip1559Ops, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, IguanaPrivKey, MakerSwapTakerCoin, MarketCoinOps, MmCoin, MmCoinEnum, NegotiateSwapContractAddrErr, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, PrivKeyBuildPolicy, PrivKeyPolicyNotAllowed, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, - SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, - ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, - WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, - WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; + SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxFeePolicy, + TakerSwapMakerCoin, TradeFee, TradePreimageError, TradePreimageFut, TradePreimageResult, + TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, + TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, + ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, + ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, + WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest, WithdrawResult}; use async_trait::async_trait; use bitcrypto::{dhash160, sha256}; use chain::TransactionOutput; @@ -539,16 +539,11 @@ impl Qrc20Coin { .build() .await?; - let my_address = self.utxo.derivation_method.single_addr_or_err().await?; let key_pair = self.utxo.priv_key_policy.activated_key_or_err()?; - let prev_script = self - .script_for_address(&my_address) - .map_err(|e| Qrc20GenTxError::InvalidAddress(e.to_string()))?; let signed = sign_tx( unsigned, key_pair, - prev_script, self.utxo.conf.signature_version, self.utxo.conf.fork_id, )?; @@ -763,7 +758,7 @@ impl UtxoCommonOps for Qrc20Coin { #[async_trait] impl SwapOps for Qrc20Coin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let to_address = try_tx_fus!(self.contract_address_from_raw_pubkey(fee_addr)); let amount = try_tx_fus!(wei_from_big_decimal(&dex_fee.fee_amount().into(), self.utxo.decimals)); let transfer_output = @@ -1366,6 +1361,7 @@ impl MmCoin for Qrc20Coin { &self, value: TradePreimageValue, stage: FeeApproxStage, + include_refund_fee: bool, ) -> TradePreimageResult { let decimals = self.utxo.decimals; // pass the dummy params @@ -1397,14 +1393,18 @@ impl MmCoin for Qrc20Coin { .await? }; - let sender_refund_fee = { + // Optionally calculate refund fee. + let sender_refund_fee = if include_refund_fee { let sender_refund_output = self.sender_refund_output(&self.swap_contract_address, swap_id, value, secret_hash, receiver_addr)?; self.preimage_trade_fee_required_to_send_outputs(vec![sender_refund_output], &stage) .await? + } else { + BigDecimal::from(0) // No refund fee if not included. }; let total_fee = erc20_payment_fee + sender_refund_fee; + Ok(TradeFee { coin: self.platform.clone(), amount: total_fee.into(), @@ -1503,8 +1503,10 @@ impl MmCoin for Qrc20Coin { } pub fn qrc20_swap_id(time_lock: u32, secret_hash: &[u8]) -> Vec { - let mut input = vec![]; - input.extend_from_slice(&time_lock.to_le_bytes()); + let timelock_bytes = time_lock.to_le_bytes(); + let mut input = Vec::with_capacity(timelock_bytes.len() + secret_hash.len()); + + input.extend_from_slice(&timelock_bytes); input.extend_from_slice(secret_hash); sha256(&input).to_vec() } @@ -1607,8 +1609,10 @@ async fn qrc20_withdraw(coin: Qrc20Coin, req: WithdrawRequest) -> WithdrawResult spent_by_me: qrc20_amount, received_by_me, my_balance_change, - tx_hash: signed.hash().reversed().to_vec().to_tx_hash(), - tx_hex: serialize(&signed).into(), + tx: TransactionData::new_signed( + serialize(&signed).into(), + signed.hash().reversed().to_vec().to_tx_hash(), + ), fee_details: Some(fee_details.into()), block_height: 0, coin: conf.ticker.clone(), @@ -1675,3 +1679,9 @@ fn transfer_event_from_log(log: &LogEntry) -> Result SwapTxFeePolicy { SwapTxFeePolicy::Unsupported } + + fn set_swap_transaction_fee_policy(&self, _swap_txfee_policy: SwapTxFeePolicy) {} +} diff --git a/mm2src/coins/qrc20/history.rs b/mm2src/coins/qrc20/history.rs index d49905b53d..af3c41f078 100644 --- a/mm2src/coins/qrc20/history.rs +++ b/mm2src/coins/qrc20/history.rs @@ -194,7 +194,10 @@ impl Qrc20Coin { let mut input_transactions = HistoryUtxoTxMap::new(); let qtum_details = try_s!(utxo_common::tx_details_by_hash(self, &tx_hash.0, &mut input_transactions).await); // Deserialize the UtxoTx to get a script pubkey - let qtum_tx: UtxoTx = try_s!(deserialize(qtum_details.tx_hex.as_slice()).map_err(|e| ERRL!("{:?}", e))); + let qtum_tx: UtxoTx = try_s!(deserialize( + try_s!(qtum_details.tx.tx_hex().ok_or("unexpected tx type")).as_slice() + ) + .map_err(|e| ERRL!("{:?}", e))); let miner_fee = { let total_qtum_fee = match qtum_details.fee_details { @@ -227,7 +230,11 @@ impl Qrc20Coin { miner_fee: BigDecimal, ) -> Result { let my_address = try_s!(self.utxo.derivation_method.single_addr_or_err().await); - let tx_hash: H256Json = try_s!(H256Json::from_str(&qtum_details.tx_hash)); + let tx_hash: H256Json = try_s!(H256Json::from_str(try_s!(qtum_details + .tx + .tx_hash() + .ok_or("unexpected tx type")))); + if qtum_tx.outputs.len() <= (receipt.output_index as usize) { return ERR!( "Length of the transaction {:?} outputs less than output_index {}", diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 9df455a188..2caf87c3bf 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -74,14 +74,15 @@ fn test_withdraw_to_p2sh_address_should_fail() { let p2sh_address = AddressBuilder::new( UtxoAddressFormat::Standard, - block_on(coin.as_ref().derivation_method.unwrap_single_addr()) - .hash() - .clone(), *block_on(coin.as_ref().derivation_method.unwrap_single_addr()).checksum_type(), coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) - .as_sh() + .as_sh( + block_on(coin.as_ref().derivation_method.unwrap_single_addr()) + .hash() + .clone(), + ) .build() .expect("valid address props"); @@ -93,6 +94,7 @@ fn test_withdraw_to_p2sh_address_should_fail() { max: false, fee: None, memo: None, + ibc_source_channel: None, }; let err = coin.withdraw(req).wait().unwrap_err().into_inner(); let expect = WithdrawError::InvalidAddress("QRC20 can be sent to P2PKH addresses only".to_owned()); @@ -102,6 +104,13 @@ fn test_withdraw_to_p2sh_address_should_fail() { #[cfg(not(target_arch = "wasm32"))] #[test] fn test_withdraw_impl_fee_details() { + // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG + let priv_key = [ + 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, + 172, 110, 180, 13, 123, 179, 10, 49, + ]; + let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); + Qrc20Coin::get_unspent_ordered_list.mock_safe(|coin, _| { let cache = block_on(coin.as_ref().recently_spent_outpoints.lock()); let unspents = vec![UnspentInfo { @@ -111,17 +120,13 @@ fn test_withdraw_impl_fee_details() { }, value: 1000000000, height: Default::default(), + script: coin + .script_for_address(&block_on(coin.as_ref().derivation_method.unwrap_single_addr())) + .unwrap(), }]; MockResult::Return(Box::pin(futures::future::ok((unspents, cache)))) }); - // priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG - let priv_key = [ - 3, 98, 177, 3, 108, 39, 234, 144, 131, 178, 103, 103, 127, 80, 230, 166, 53, 68, 147, 215, 42, 216, 144, 72, - 172, 110, 180, 13, 123, 179, 10, 49, - ]; - let (_ctx, coin) = qrc20_coin_for_test(priv_key, None); - let withdraw_req = WithdrawRequest { amount: 10.into(), from: None, @@ -133,6 +138,7 @@ fn test_withdraw_impl_fee_details() { gas_price: 40, }), memo: None, + ibc_source_channel: None, }; let tx_details = coin.withdraw(withdraw_req).wait().unwrap(); @@ -594,8 +600,7 @@ fn test_transfer_details_by_hash() { // qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8 is UTXO representation of 1549128bbfb33b997949b4105b6a6371c998e212 contract address let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -619,8 +624,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -644,8 +648,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], to: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], total_amount: BigDecimal::from_str("0.003").unwrap(), @@ -669,8 +672,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex: tx_hex.clone(), - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex.clone(), tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00295").unwrap(), @@ -694,8 +696,7 @@ fn test_transfer_details_by_hash() { let (_id, actual) = it.next().unwrap(); let expected = TransactionDetails { - tx_hex, - tx_hash: tx_hash_bytes.to_tx_hash(), + tx: TransactionData::new_signed(tx_hex, tx_hash_bytes.to_tx_hash()), from: vec!["qKVvtDqpnFGDxsDzck5jmLwdnD2jRH6aM8".into()], to: vec!["qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG".into()], total_amount: BigDecimal::from_str("0.00005000").unwrap(), @@ -767,7 +768,7 @@ fn test_sender_trade_preimage_zero_allowance() { let sender_refund_fee = big_decimal_from_sat(CONTRACT_CALL_GAS_FEE + EXPECTED_TX_FEE, coin.utxo.decimals); let actual = - block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox)) + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(1.into()), FeeApproxStage::WithoutApprox, true)) .expect("!get_sender_trade_fee"); // one `approve` contract call should be included into the expected trade fee let expected = TradeFee { @@ -807,6 +808,7 @@ fn test_sender_trade_preimage_with_allowance() { let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::Exact(BigDecimal::try_from(2.5).unwrap()), FeeApproxStage::WithoutApprox, + true, )) .expect("!get_sender_trade_fee"); // the expected fee should not include any `approve` contract call @@ -820,6 +822,7 @@ fn test_sender_trade_preimage_with_allowance() { let actual = block_on(coin.get_sender_trade_fee( TradePreimageValue::Exact(BigDecimal::try_from(3.5).unwrap()), FeeApproxStage::WithoutApprox, + true, )) .expect("!get_sender_trade_fee"); // two `approve` contract calls should be included into the expected trade fee @@ -875,10 +878,11 @@ fn test_get_sender_trade_fee_preimage_for_correct_ticker() { )) .unwrap(); - let actual = block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(0.into()), FeeApproxStage::OrderIssue)) - .err() - .unwrap() - .into_inner(); + let actual = + block_on(coin.get_sender_trade_fee(TradePreimageValue::Exact(0.into()), FeeApproxStage::OrderIssue, true)) + .err() + .unwrap() + .into_inner(); // expecting TradePreimageError::NotSufficientBalance let expected = TradePreimageError::NotSufficientBalance { coin: "tQTUM".to_string(), diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs new file mode 100644 index 0000000000..b62e572756 --- /dev/null +++ b/mm2src/coins/rpc_command/get_estimated_fees.rs @@ -0,0 +1,331 @@ +//! RPCs to start/stop gas fee estimator and get estimated base and priority fee per gas + +use crate::eth::{EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; +use crate::{lp_coinfind_or_err, wei_to_gwei_decimal, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; +use common::executor::{spawn_abortable, Timer}; +use common::log::debug; +use common::{HttpStatusCode, StatusCode}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use serde::{Deserialize, Serialize}; +use serde_json::{self as json, Value as Json}; +use std::convert::{TryFrom, TryInto}; +use std::ops::Deref; +use std::sync::Arc; + +const FEE_ESTIMATOR_NAME: &str = "eth_gas_fee_estimator_loop"; + +/// Estimated fee per gas units +#[derive(Clone, Debug, Serialize)] +pub enum EstimationUnits { + Gwei, +} + +impl Default for EstimationUnits { + fn default() -> Self { Self::Gwei } +} + +/// Priority level estimated max fee per gas +#[derive(Clone, Debug, Default, Serialize)] +pub struct FeePerGasLevel { + /// estimated max priority tip fee per gas in gwei + pub max_priority_fee_per_gas: BigDecimal, + /// estimated max fee per gas in gwei + pub max_fee_per_gas: BigDecimal, + /// estimated transaction min wait time in mempool in ms for this priority level + pub min_wait_time: Option, + /// estimated transaction max wait time in mempool in ms for this priority level + pub max_wait_time: Option, +} + +/// External struct for estimated fee per gas for several priority levels, in gwei +/// low/medium/high levels are supported +#[derive(Default, Debug, Clone, Serialize)] +pub struct FeePerGasEstimatedExt { + /// base fee for the next block in gwei + pub base_fee: BigDecimal, + /// estimated low priority fee + pub low: FeePerGasLevel, + /// estimated medium priority fee + pub medium: FeePerGasLevel, + /// estimated high priority fee + pub high: FeePerGasLevel, + /// which estimator used + pub source: String, + /// base trend (up or down) + pub base_fee_trend: String, + /// priority trend (up or down) + pub priority_fee_trend: String, + /// fee units + pub units: EstimationUnits, +} + +impl TryFrom for FeePerGasEstimatedExt { + type Error = MmError; + + fn try_from(fees: FeePerGasEstimated) -> Result { + Ok(Self { + base_fee: wei_to_gwei_decimal!(fees.base_fee)?, + low: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_priority_fee_per_gas)?, + min_wait_time: fees.low.min_wait_time, + max_wait_time: fees.low.max_wait_time, + }, + medium: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_priority_fee_per_gas)?, + min_wait_time: fees.medium.min_wait_time, + max_wait_time: fees.medium.max_wait_time, + }, + high: FeePerGasLevel { + max_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_priority_fee_per_gas)?, + min_wait_time: fees.high.min_wait_time, + max_wait_time: fees.high.max_wait_time, + }, + source: fees.source.to_string(), + base_fee_trend: fees.base_fee_trend, + priority_fee_trend: fees.priority_fee_trend, + units: EstimationUnits::Gwei, + }) + } +} + +#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum FeeEstimatorError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Gas fee estimation not supported for this coin")] + CoinNotSupported, + #[display(fmt = "Platform coin needs to be enabled for gas fee estimation")] + PlatformCoinRequired, + #[display(fmt = "Gas fee estimator is already started")] + AlreadyStarted, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Gas fee estimator is not running")] + NotRunning, + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for FeeEstimatorError { + fn status_code(&self) -> StatusCode { + match self { + FeeEstimatorError::NoSuchCoin { .. } + | FeeEstimatorError::CoinNotSupported + | FeeEstimatorError::PlatformCoinRequired + | FeeEstimatorError::AlreadyStarted + | FeeEstimatorError::NotRunning => StatusCode::BAD_REQUEST, + FeeEstimatorError::Transport(_) | FeeEstimatorError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for FeeEstimatorError { + fn from(e: NumConversError) -> Self { FeeEstimatorError::InternalError(e.to_string()) } +} + +impl From for FeeEstimatorError { + fn from(e: String) -> Self { FeeEstimatorError::InternalError(e) } +} + +impl From for FeeEstimatorError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => FeeEstimatorError::NoSuchCoin { coin }, + } + } +} + +/// Gas fee estimator configuration +#[derive(Deserialize)] +enum FeeEstimatorConf { + NotConfigured, + #[serde(rename = "simple")] + Simple, + #[serde(rename = "provider")] + Provider, +} + +impl Default for FeeEstimatorConf { + fn default() -> Self { Self::NotConfigured } +} + +impl FeeEstimatorState { + /// Creates gas FeeEstimatorContext if configured for this coin and chain id, otherwise returns None. + /// The created context object (or None) is wrapped into a FeeEstimatorState so a gas fee rpc caller may know the reason why it was not created + pub(crate) async fn init_fee_estimator( + ctx: &MmArc, + conf: &Json, + coin_type: &EthCoinType, + ) -> Result, String> { + let fee_estimator_json = conf["gas_fee_estimator"].clone(); + let fee_estimator_conf: FeeEstimatorConf = if !fee_estimator_json.is_null() { + try_s!(json::from_value(fee_estimator_json)) + } else { + Default::default() + }; + match (fee_estimator_conf, coin_type) { + (FeeEstimatorConf::Simple, EthCoinType::Eth) => { + let fee_estimator_state = FeeEstimatorState::Simple(FeeEstimatorContext::new()); + Ok(Arc::new(fee_estimator_state)) + }, + (FeeEstimatorConf::Provider, EthCoinType::Eth) => { + let fee_estimator_state = FeeEstimatorState::Provider(FeeEstimatorContext::new()); + Ok(Arc::new(fee_estimator_state)) + }, + (_, EthCoinType::Erc20 { platform, .. }) | (_, EthCoinType::Nft { platform, .. }) => { + let platform_coin = lp_coinfind_or_err(ctx, platform).await; + match platform_coin { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin.platform_fee_estimator_state.clone()), + _ => Ok(Arc::new(FeeEstimatorState::PlatformCoinRequired)), + } + }, + (FeeEstimatorConf::NotConfigured, _) => Ok(Arc::new(FeeEstimatorState::CoinNotSupported)), + } + } +} + +impl FeeEstimatorContext { + fn new() -> AsyncMutex { + AsyncMutex::new(FeeEstimatorContext { + estimated_fees: Default::default(), + abort_handler: AsyncMutex::new(None), + }) + } + + /// Fee estimation update period in secs, basically equals to eth blocktime + const fn get_refresh_interval() -> f64 { 15.0 } + + fn get_estimator_ctx(coin: &EthCoin) -> Result<&AsyncMutex, MmError> { + match coin.platform_fee_estimator_state.deref() { + FeeEstimatorState::CoinNotSupported => MmError::err(FeeEstimatorError::CoinNotSupported), + FeeEstimatorState::PlatformCoinRequired => MmError::err(FeeEstimatorError::PlatformCoinRequired), + FeeEstimatorState::Simple(fee_estimator_ctx) | FeeEstimatorState::Provider(fee_estimator_ctx) => { + Ok(fee_estimator_ctx) + }, + } + } + + async fn start_if_not_running(coin: &EthCoin) -> Result<(), MmError> { + let estimator_ctx = Self::get_estimator_ctx(coin)?; + let estimator_ctx = estimator_ctx.lock().await; + let mut handler = estimator_ctx.abort_handler.lock().await; + if handler.is_some() { + return MmError::err(FeeEstimatorError::AlreadyStarted); + } + *handler = Some(spawn_abortable(Self::fee_estimator_loop(coin.clone()))); + Ok(()) + } + + async fn request_to_stop(coin: &EthCoin) -> Result<(), MmError> { + let estimator_ctx = Self::get_estimator_ctx(coin)?; + let estimator_ctx = estimator_ctx.lock().await; + let mut handle_guard = estimator_ctx.abort_handler.lock().await; + // Handler will be dropped here, stopping the spawned loop immediately + handle_guard + .take() + .map(|_| ()) + .or_mm_err(|| FeeEstimatorError::NotRunning) + } + + async fn get_estimated_fees(coin: &EthCoin) -> Result> { + let estimator_ctx = Self::get_estimator_ctx(coin)?; + let estimator_ctx = estimator_ctx.lock().await; + let estimated_fees = estimator_ctx.estimated_fees.lock().await; + Ok(estimated_fees.clone()) + } + + async fn check_if_estimator_supported(ctx: &MmArc, ticker: &str) -> Result> { + let eth_coin = match lp_coinfind_or_err(ctx, ticker).await? { + MmCoinEnum::EthCoin(eth) => eth, + _ => return MmError::err(FeeEstimatorError::CoinNotSupported), + }; + let _ = Self::get_estimator_ctx(ð_coin)?; + Ok(eth_coin) + } + + /// Loop polling gas fee estimator + /// + /// This loop periodically calls get_eip1559_gas_fee which fetches fee per gas estimations from a gas api provider or calculates them internally + /// The retrieved data are stored in the fee estimator context + /// To connect to the chain and gas api provider the web3 instances are used from an EthCoin coin passed in the start rpc param, + /// so this coin must be enabled first. + /// Once the loop started any other EthCoin in mainnet may request fee estimations. + /// It is up to GUI to start and stop the loop when it needs it (considering that the data in context may be used + /// for any coin with Eth or Erc20 type from the mainnet). + async fn fee_estimator_loop(coin: EthCoin) { + loop { + let started = common::now_float(); + if let Ok(estimator_ctx) = Self::get_estimator_ctx(&coin) { + let estimated_fees = coin.get_eip1559_gas_fee().await.unwrap_or_default(); + let estimator_ctx = estimator_ctx.lock().await; + *estimator_ctx.estimated_fees.lock().await = estimated_fees; + } + + let elapsed = common::now_float() - started; + debug!("{FEE_ESTIMATOR_NAME} call to provider processed in {} seconds", elapsed); + + let wait_secs = FeeEstimatorContext::get_refresh_interval() - elapsed; + let wait_secs = if wait_secs < 0.0 { 0.0 } else { wait_secs }; + Timer::sleep(wait_secs).await; + } + } +} + +/// Rpc request to start or stop gas fee estimator +#[derive(Deserialize)] +pub struct FeeEstimatorStartStopRequest { + coin: String, +} + +/// Rpc response to request to start or stop gas fee estimator +#[derive(Serialize)] +pub struct FeeEstimatorStartStopResponse { + result: String, +} + +pub type FeeEstimatorStartStopResult = Result>; + +/// Rpc request to get latest estimated fee per gas +#[derive(Deserialize)] +pub struct FeeEstimatorRequest { + /// coin ticker + coin: String, +} + +pub type FeeEstimatorResult = Result>; + +/// Start gas priority fee estimator loop +pub async fn start_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { + let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; + FeeEstimatorContext::start_if_not_running(&coin).await?; + Ok(FeeEstimatorStartStopResponse { + result: "Success".to_string(), + }) +} + +/// Stop gas priority fee estimator loop +pub async fn stop_eth_fee_estimator(ctx: MmArc, req: FeeEstimatorStartStopRequest) -> FeeEstimatorStartStopResult { + let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; + FeeEstimatorContext::request_to_stop(&coin).await?; + Ok(FeeEstimatorStartStopResponse { + result: "Success".to_string(), + }) +} + +/// Get latest estimated fee per gas for a eth coin +/// +/// Estimation loop for this coin must be stated. +/// Only main chain is supported +/// +/// Returns latest estimated fee per gas for the next block +pub async fn get_eth_estimated_fee_per_gas(ctx: MmArc, req: FeeEstimatorRequest) -> FeeEstimatorResult { + let coin = FeeEstimatorContext::check_if_estimator_supported(&ctx, &req.coin).await?; + let estimated_fees = FeeEstimatorContext::get_estimated_fees(&coin).await?; + estimated_fees.try_into().mm_err(Into::into) +} diff --git a/mm2src/coins/rpc_command/mod.rs b/mm2src/coins/rpc_command/mod.rs index c401853b2d..0bec5ef493 100644 --- a/mm2src/coins/rpc_command/mod.rs +++ b/mm2src/coins/rpc_command/mod.rs @@ -1,6 +1,7 @@ pub mod account_balance; pub mod get_current_mtp; pub mod get_enabled_coins; +pub mod get_estimated_fees; pub mod get_new_address; pub mod hd_account_balance_rpc_error; pub mod init_account_balance; diff --git a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs index fce69042c6..4edcd0cd55 100644 --- a/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs +++ b/mm2src/coins/rpc_command/tendermint/ibc_transfer_channels.rs @@ -2,14 +2,14 @@ use common::HttpStatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; -use crate::{lp_coinfind_or_err, MmCoinEnum}; +use crate::{coin_conf, tendermint::get_ibc_transfer_channels}; pub type IBCTransferChannelsResult = Result>; #[derive(Clone, Deserialize)] pub struct IBCTransferChannelsRequest { - pub(crate) coin: String, - pub(crate) destination_chain_registry_name: String, + pub(crate) source_coin: String, + pub(crate) destination_coin: String, } #[derive(Clone, Serialize)] @@ -42,10 +42,17 @@ pub enum IBCTransferChannelsRequestError { _0 )] UnsupportedCoin(String), + #[display( + fmt = "'chain_registry_name' was not found in coins configuration for '{}' prefix. Either update the coins configuration or use 'ibc_source_channel' in the request.", + _0 + )] + RegistryNameIsMissing(String), #[display(fmt = "Could not find '{}' registry source.", _0)] RegistrySourceCouldNotFound(String), #[display(fmt = "Transport error: {}", _0)] Transport(String), + #[display(fmt = "Could not found channel for '{}'.", _0)] + CouldNotFindChannel(String), #[display(fmt = "Internal error: {}", _0)] InternalError(String), } @@ -56,7 +63,9 @@ impl HttpStatusCode for IBCTransferChannelsRequestError { IBCTransferChannelsRequestError::UnsupportedCoin(_) | IBCTransferChannelsRequestError::NoSuchCoin(_) => { common::StatusCode::BAD_REQUEST }, - IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, + IBCTransferChannelsRequestError::CouldNotFindChannel(_) + | IBCTransferChannelsRequestError::RegistryNameIsMissing(_) + | IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(_) => common::StatusCode::NOT_FOUND, IBCTransferChannelsRequestError::Transport(_) => common::StatusCode::SERVICE_UNAVAILABLE, IBCTransferChannelsRequestError::InternalError(_) => common::StatusCode::INTERNAL_SERVER_ERROR, } @@ -64,13 +73,33 @@ impl HttpStatusCode for IBCTransferChannelsRequestError { } pub async fn ibc_transfer_channels(ctx: MmArc, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin) - .await - .map_err(|_| IBCTransferChannelsRequestError::NoSuchCoin(req.coin.clone()))?; + let source_coin_conf = coin_conf(&ctx, &req.source_coin); + let source_registry_name = source_coin_conf + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); - match coin { - MmCoinEnum::Tendermint(coin) => coin.get_ibc_transfer_channels(req).await, - MmCoinEnum::TendermintToken(token) => token.platform_coin.get_ibc_transfer_channels(req).await, - _ => MmError::err(IBCTransferChannelsRequestError::UnsupportedCoin(req.coin)), - } + let Some(source_registry_name) = source_registry_name else { + return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing(req.source_coin)); + }; + + let destination_coin_conf = coin_conf(&ctx, &req.destination_coin); + let destination_registry_name = destination_coin_conf + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); + + let Some(destination_registry_name) = destination_registry_name else { + return MmError::err(IBCTransferChannelsRequestError::RegistryNameIsMissing( + req.destination_coin, + )); + }; + + get_ibc_transfer_channels(source_registry_name, destination_registry_name).await } diff --git a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs b/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs deleted file mode 100644 index 037823ee66..0000000000 --- a/mm2src/coins/rpc_command/tendermint/ibc_withdraw.rs +++ /dev/null @@ -1,29 +0,0 @@ -use common::Future01CompatExt; -use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::MmError; -use mm2_number::BigDecimal; - -use crate::{lp_coinfind_or_err, MmCoinEnum, WithdrawError, WithdrawFee, WithdrawFrom, WithdrawResult}; - -#[derive(Clone, Deserialize)] -pub struct IBCWithdrawRequest { - pub(crate) ibc_source_channel: String, - pub(crate) from: Option, - pub(crate) coin: String, - pub(crate) to: String, - #[serde(default)] - pub(crate) amount: BigDecimal, - #[serde(default)] - pub(crate) max: bool, - pub(crate) memo: Option, - pub(crate) fee: Option, -} - -pub async fn ibc_withdraw(ctx: MmArc, req: IBCWithdrawRequest) -> WithdrawResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin).await?; - match coin { - MmCoinEnum::Tendermint(coin) => coin.ibc_withdraw(req).compat().await, - MmCoinEnum::TendermintToken(token) => token.ibc_withdraw(req).compat().await, - _ => MmError::err(WithdrawError::ActionNotAllowed(req.coin)), - } -} diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index d8211abeac..3e2b664aec 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,14 +1,12 @@ mod ibc_chains; mod ibc_transfer_channels; -mod ibc_withdraw; pub use ibc_chains::*; pub use ibc_transfer_channels::*; -pub use ibc_withdraw::*; // Global constants for interacting with https://github.com/KomodoPlatform/chain-registry repository // using `mm2_git` crate. pub(crate) const CHAIN_REGISTRY_REPO_OWNER: &str = "KomodoPlatform"; pub(crate) const CHAIN_REGISTRY_REPO_NAME: &str = "chain-registry"; -pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "master"; +pub(crate) const CHAIN_REGISTRY_BRANCH: &str = "nucl"; pub(crate) const CHAIN_REGISTRY_IBC_DIR_NAME: &str = "_IBC"; diff --git a/mm2src/coins/sia.rs b/mm2src/coins/sia.rs index ceb7b78f20..446a507070 100644 --- a/mm2src/coins/sia.rs +++ b/mm2src/coins/sia.rs @@ -239,6 +239,7 @@ impl MmCoin for SiaCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } @@ -377,7 +378,9 @@ impl MarketCoinOps for SiaCoin { #[async_trait] impl SwapOps for SiaCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], _dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } diff --git a/mm2src/coins/solana.rs b/mm2src/coins/solana.rs index b897503006..2a454fd90c 100644 --- a/mm2src/coins/solana.rs +++ b/mm2src/coins/solana.rs @@ -10,7 +10,7 @@ use crate::{BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, TradePreimageValue, - TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, + TransactionData, TransactionDetails, TransactionFut, TransactionResult, TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, @@ -274,8 +274,7 @@ async fn withdraw_base_coin_impl(coin: SolanaCoin, req: WithdrawRequest) -> With }; let spent_by_me = &total_amount + &res.sol_required; Ok(TransactionDetails { - tx_hex: serialized_tx.into(), - tx_hash: tx.signatures[0].to_string(), + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), from: vec![coin.my_address.clone()], to: vec![req.to], total_amount: spent_by_me.clone(), @@ -481,7 +480,9 @@ impl MarketCoinOps for SolanaCoin { #[async_trait] impl SwapOps for SolanaCoin { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } @@ -760,6 +761,7 @@ impl MmCoin for SolanaCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } diff --git a/mm2src/coins/solana/solana_decode_tx_helpers.rs b/mm2src/coins/solana/solana_decode_tx_helpers.rs index 2ac0876809..bd22fc044e 100644 --- a/mm2src/coins/solana/solana_decode_tx_helpers.rs +++ b/mm2src/coins/solana/solana_decode_tx_helpers.rs @@ -1,6 +1,6 @@ extern crate serde_derive; -use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionDetails, TransactionType}; +use crate::{NumConversResult, SolanaCoin, SolanaFeeDetails, TransactionData, TransactionDetails, TransactionType}; use mm2_number::BigDecimal; use solana_sdk::native_token::lamports_to_sol; use std::convert::TryFrom; @@ -54,8 +54,7 @@ impl SolanaConfirmedTransaction { }; let fee = BigDecimal::try_from(lamports_to_sol(self.meta.fee))?; let tx = TransactionDetails { - tx_hex: Default::default(), - tx_hash: self.transaction.signatures[0].to_string(), + tx: TransactionData::new_signed(Default::default(), self.transaction.signatures[0].to_string()), from: vec![instruction.parsed.info.source.clone()], to: vec![instruction.parsed.info.destination.clone()], total_amount: amount, diff --git a/mm2src/coins/solana/solana_tests.rs b/mm2src/coins/solana/solana_tests.rs index 77a8f7fda4..fb1a7b958c 100644 --- a/mm2src/coins/solana/solana_tests.rs +++ b/mm2src/coins/solana/solana_tests.rs @@ -164,6 +164,7 @@ fn solana_transaction_simulations() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -192,6 +193,7 @@ fn solana_transaction_zero_balance() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ); @@ -221,6 +223,7 @@ fn solana_transaction_simulations_not_enough_for_fees() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ); @@ -255,6 +258,7 @@ fn solana_transaction_simulations_max() { max: true, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -284,16 +288,22 @@ fn solana_test_transactions() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) .unwrap(); log!("{:?}", valid_tx_details); - let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let tx_str = hex::encode(&*valid_tx_details.tx.tx_hex().unwrap().0); let res = block_on(sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); - let res2 = block_on(sol_coin.send_raw_tx_bytes(&valid_tx_details.tx_hex.0).compat()).unwrap(); + let res2 = block_on( + sol_coin + .send_raw_tx_bytes(&valid_tx_details.tx.tx_hex().unwrap().0) + .compat(), + ) + .unwrap(); assert_eq!(res, res2); //log!("{:?}", res); diff --git a/mm2src/coins/solana/spl.rs b/mm2src/coins/solana/spl.rs index bfecd9351f..7bc720e5e7 100644 --- a/mm2src/coins/solana/spl.rs +++ b/mm2src/coins/solana/spl.rs @@ -8,8 +8,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPayment RawTransactionRequest, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SolanaCoin, SpendPaymentArgs, TakerSwapMakerCoin, TradePreimageFut, TradePreimageResult, - TradePreimageValue, TransactionDetails, TransactionFut, TransactionResult, TransactionType, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + TradePreimageValue, TransactionData, TransactionDetails, TransactionFut, TransactionResult, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, @@ -149,8 +149,7 @@ async fn withdraw_spl_token_impl(coin: SplToken, req: WithdrawRequest) -> Withdr 0.into() }; Ok(TransactionDetails { - tx_hex: serialized_tx.into(), - tx_hash: tx.signatures[0].to_string(), + tx: TransactionData::new_signed(serialized_tx.into(), tx.signatures[0].to_string()), from: vec![coin.platform_coin.my_address.clone()], to: vec![req.to], total_amount: res.to_send.clone(), @@ -300,7 +299,9 @@ impl MarketCoinOps for SplToken { #[async_trait] impl SwapOps for SplToken { - fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, _fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } @@ -552,6 +553,7 @@ impl MmCoin for SplToken { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } diff --git a/mm2src/coins/solana/spl_tests.rs b/mm2src/coins/solana/spl_tests.rs index 9b6f985203..10943e6e33 100644 --- a/mm2src/coins/solana/spl_tests.rs +++ b/mm2src/coins/solana/spl_tests.rs @@ -113,6 +113,7 @@ fn test_spl_transactions() { max: false, fee: None, memo: None, + ibc_source_channel: None, }) .compat(), ) @@ -123,10 +124,15 @@ fn test_spl_transactions() { assert_eq!(valid_tx_details.coin, "USDC".to_string()); assert_ne!(valid_tx_details.timestamp, 0); - let tx_str = hex::encode(&*valid_tx_details.tx_hex.0); + let tx_str = hex::encode(&*valid_tx_details.tx.tx_hex().unwrap().0); let res = block_on(usdc_sol_coin.send_raw_tx(&tx_str).compat()).unwrap(); log!("{:?}", res); - let res2 = block_on(usdc_sol_coin.send_raw_tx_bytes(&valid_tx_details.tx_hex.0).compat()).unwrap(); + let res2 = block_on( + usdc_sol_coin + .send_raw_tx_bytes(&valid_tx_details.tx.tx_hex().unwrap().0) + .compat(), + ) + .unwrap(); assert_eq!(res, res2); } diff --git a/mm2src/coins/tendermint/ibc/transfer_v1.rs b/mm2src/coins/tendermint/ibc/transfer_v1.rs index e7bf37697f..c5780e32b7 100644 --- a/mm2src/coins/tendermint/ibc/transfer_v1.rs +++ b/mm2src/coins/tendermint/ibc/transfer_v1.rs @@ -1,6 +1,5 @@ use super::{ibc_proto::IBCTransferV1Proto, IBC_OUT_SOURCE_PORT, IBC_OUT_TIMEOUT_IN_NANOS}; use crate::tendermint::ibc::IBC_TRANSFER_TYPE_URL; -use common::number_type_casting::SafeTypeCastingNumbers; use cosmrs::proto::traits::TypeUrl; use cosmrs::{tx::Msg, AccountId, Coin, ErrorReport}; use std::convert::TryFrom; @@ -34,10 +33,7 @@ impl MsgTransfer { receiver: AccountId, token: Coin, ) -> Self { - let timestamp_as_nanos: u64 = common::get_local_duration_since_epoch() - .expect("get_local_duration_since_epoch shouldn't fail") - .as_nanos() - .into_or_max(); + let timestamp_as_nanos = common::get_utc_timestamp_nanos() as u64; Self { source_port: IBC_OUT_SOURCE_PORT.to_owned(), diff --git a/mm2src/coins/tendermint/mod.rs b/mm2src/coins/tendermint/mod.rs index a1fd9beb57..78009b5db8 100644 --- a/mm2src/coins/tendermint/mod.rs +++ b/mm2src/coins/tendermint/mod.rs @@ -11,6 +11,8 @@ mod tendermint_coin; mod tendermint_token; pub mod tendermint_tx_history_v2; +pub use cosmrs::tendermint::PublicKey as TendermintPublicKey; +pub use cosmrs::AccountId; pub use tendermint_coin::*; pub use tendermint_token::*; diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index cee6fc0f75..f073d31d29 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -3,13 +3,12 @@ use super::htlc::{ClaimHtlcMsg, ClaimHtlcProto, CreateHtlcMsg, CreateHtlcProto, QueryHtlcResponse, TendermintHtlc, HTLC_STATE_COMPLETED, HTLC_STATE_OPEN, HTLC_STATE_REFUNDED}; use super::ibc::transfer_v1::MsgTransfer; use super::ibc::IBC_GAS_LIMIT_DEFAULT; -use super::rpc::*; +use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; -use crate::hd_wallet::HDPathAccountToAddressId; +use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, - IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequest, - IBCTransferChannelsRequestError, IBCTransferChannelsResponse, - IBCTransferChannelsResult, IBCWithdrawRequest, CHAIN_REGISTRY_BRANCH, + IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, + IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, CHAIN_REGISTRY_IBC_DIR_NAME, CHAIN_REGISTRY_REPO_NAME, CHAIN_REGISTRY_REPO_OWNER}; use crate::tendermint::ibc::IBC_OUT_SOURCE_PORT; use crate::utxo::sat_from_big_decimal; @@ -21,16 +20,17 @@ use crate::{big_decimal_from_sat_unsigned, BalanceError, BalanceFut, BigDecimal, PrivKeyPolicyNotAllowed, RawTransactionError, RawTransactionFut, RawTransactionRequest, RawTransactionRes, RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, RpcCommonOps, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, - SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, TradeFee, - TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, - TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, TxFeeDetails, - TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + SignatureError, SignatureResult, SpendPaymentArgs, SwapOps, TakerSwapMakerCoin, ToBytes, TradeFee, + TradePreimageError, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, + TransactionDetails, TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TransactionType, + TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawError, WithdrawFee, WithdrawFut, WithdrawRequest}; use async_std::prelude::FutureExt as AsyncStdFutureExt; use async_trait::async_trait; +use bip32::DerivationPath; use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; @@ -59,8 +59,9 @@ use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use hex::FromHexError; +use instant::Duration; use itertools::Itertools; -use keys::KeyPair; +use keys::{KeyPair, Public}; use mm2_core::mm_ctx::{MmArc, MmWeak}; use mm2_err_handle::prelude::*; use mm2_git::{FileMetadata, GitController, GithubClient, RepositoryOperations, GITHUB_API_URI}; @@ -71,11 +72,11 @@ use rpc::v1::types::Bytes as BytesJson; use serde_json::{self as json, Value as Json}; use std::collections::HashMap; use std::convert::TryFrom; +use std::io; use std::num::NonZeroU32; use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, Mutex}; -use std::time::Duration; use uuid::Uuid; // ABCI Request Paths @@ -97,6 +98,7 @@ const ABCI_REQUEST_PROVE: bool = false; const DEFAULT_GAS_PRICE: f64 = 0.25; pub(super) const TIMEOUT_HEIGHT_DELTA: u64 = 100; pub const GAS_LIMIT_DEFAULT: u64 = 125_000; +pub const GAS_WANTED_BASE_VALUE: f64 = 50_000.; pub(crate) const TX_DEFAULT_MEMO: &str = ""; // https://github.com/irisnet/irismod/blob/5016c1be6fdbcffc319943f33713f4a057622f0a/modules/htlc/types/validation.go#L19-L22 @@ -105,7 +107,21 @@ const MIN_TIME_LOCK: i64 = 50; const ACCOUNT_SEQUENCE_ERR: &str = "incorrect account sequence"; -type TendermintPrivKeyPolicy = PrivKeyPolicy; +type TendermintPrivKeyPolicy = PrivKeyPolicy; + +pub struct TendermintKeyPair { + private_key_secret: Secp256k1Secret, + public_key: Public, +} + +impl TendermintKeyPair { + fn new(private_key_secret: Secp256k1Secret, public_key: Public) -> Self { + Self { + private_key_secret, + public_key, + } + } +} #[async_trait] pub trait TendermintCommons { @@ -183,6 +199,98 @@ impl TendermintConf { } } +pub enum TendermintActivationPolicy { + PrivateKey(PrivKeyPolicy), + PublicKey(PublicKey), +} + +impl TendermintActivationPolicy { + pub fn with_private_key_policy(private_key_policy: PrivKeyPolicy) -> Self { + Self::PrivateKey(private_key_policy) + } + + pub fn with_public_key(account_public_key: PublicKey) -> Self { Self::PublicKey(account_public_key) } + + fn generate_account_id(&self, account_prefix: &str) -> Result { + match self { + Self::PrivateKey(priv_key_policy) => { + let pk = priv_key_policy.activated_key().ok_or_else(|| { + ErrorReport::new(io::Error::new(io::ErrorKind::NotFound, "Activated key not found")) + })?; + + Ok( + account_id_from_privkey(pk.private_key_secret.as_slice(), account_prefix) + .map_err(|e| ErrorReport::new(io::Error::new(io::ErrorKind::InvalidData, e.to_string())))?, + ) + }, + + Self::PublicKey(account_public_key) => { + account_id_from_raw_pubkey(account_prefix, &account_public_key.to_bytes()) + }, + } + } + + fn public_key(&self) -> Result { + match self { + Self::PrivateKey(private_key_policy) => match private_key_policy { + PrivKeyPolicy::Iguana(pair) => PublicKey::from_raw_secp256k1(&pair.public_key.to_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")), + + PrivKeyPolicy::HDWallet { activated_key, .. } => { + PublicKey::from_raw_secp256k1(&activated_key.public_key.to_bytes()) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Couldn't generate public key")) + }, + + PrivKeyPolicy::Trezor => Err(io::Error::new( + io::ErrorKind::Unsupported, + "Trezor is not supported yet!", + )), + + #[cfg(target_arch = "wasm32")] + PrivKeyPolicy::Metamask(_) => unreachable!(), + }, + Self::PublicKey(account_public_key) => Ok(*account_public_key), + } + } + + pub(crate) fn activated_key_or_err(&self) -> Result<&Secp256k1Secret, MmError> { + match self { + Self::PrivateKey(private_key) => Ok(private_key.activated_key_or_err()?.private_key_secret.as_ref()), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`activated_key_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } + + pub(crate) fn activated_key(&self) -> Option { + match self { + Self::PrivateKey(private_key) => Some(*private_key.activated_key()?.private_key_secret.as_ref()), + Self::PublicKey(_) => None, + } + } + + pub(crate) fn path_to_coin_or_err(&self) -> Result<&HDPathToCoin, MmError> { + match self { + Self::PrivateKey(private_key) => Ok(private_key.path_to_coin_or_err()?), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`path_to_coin_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } + + pub(crate) fn hd_wallet_derived_priv_key_or_err( + &self, + path_to_address: &DerivationPath, + ) -> Result> { + match self { + Self::PrivateKey(pair) => pair.hd_wallet_derived_priv_key_or_err(path_to_address), + Self::PublicKey(_) => MmError::err(PrivKeyPolicyNotAllowed::UnsupportedMethod( + "`hd_wallet_derived_priv_key_or_err` is not supported for pubkey-only activations".to_string(), + )), + } + } +} + struct TendermintRpcClient(AsyncMutex); struct TendermintRpcClientImpl { @@ -225,7 +333,7 @@ pub struct TendermintCoinImpl { /// My address pub account_id: AccountId, pub(super) account_prefix: String, - pub(super) priv_key_policy: TendermintPrivKeyPolicy, + pub(super) activation_policy: TendermintActivationPolicy, pub(crate) decimals: u8, pub(super) denom: Denom, chain_id: ChainId, @@ -236,7 +344,7 @@ pub struct TendermintCoinImpl { pub(super) abortable_system: AbortableQueue, pub(crate) history_sync_state: Mutex, client: TendermintRpcClient, - chain_registry_name: Option, + pub(crate) chain_registry_name: Option, pub(crate) ctx: MmWeak, } @@ -282,6 +390,8 @@ pub enum TendermintInitErrorKind { #[display(fmt = "avg_blocktime must be in-between '0' and '255'.")] AvgBlockTimeInvalid, BalanceStreamInitError(String), + #[display(fmt = "Watcher features can not be used with pubkey-only activation policy.")] + CantUseWatchersWithPubkeyPolicy, } #[derive(Display, Debug, Serialize, SerializeErrorType)] @@ -366,7 +476,7 @@ pub struct CosmosTransaction { impl crate::Transaction for CosmosTransaction { fn tx_hex(&self) -> Vec { self.data.encode_to_vec() } - fn tx_hash(&self) -> BytesJson { + fn tx_hash_as_bytes(&self) -> BytesJson { let bytes = self.data.encode_to_vec(); let hash = sha256(&bytes); hash.to_vec().into() @@ -397,10 +507,14 @@ impl From for AccountIdFromPubkeyHexErr { fn from(err: ErrorReport) -> Self { AccountIdFromPubkeyHexErr::CouldNotCreateAccountId(err) } } -pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> MmResult { +pub fn account_id_from_pubkey_hex(prefix: &str, pubkey: &str) -> Result { let pubkey_bytes = hex::decode(pubkey)?; - let pubkey_hash = dhash160(&pubkey_bytes); - Ok(AccountId::new(prefix, pubkey_hash.as_slice())?) + Ok(account_id_from_raw_pubkey(prefix, &pubkey_bytes)?) +} + +pub fn account_id_from_raw_pubkey(prefix: &str, pubkey: &[u8]) -> Result { + let pubkey_hash = dhash160(pubkey); + AccountId::new(prefix, pubkey_hash.as_slice()) } #[derive(Debug, Clone, PartialEq)] @@ -458,7 +572,7 @@ impl TendermintCommons for TendermintCoin { let platform_balance = big_decimal_from_sat_unsigned(platform_balance_denom, self.decimals); let ibc_assets_info = self.tokens_info.lock().clone(); - let mut requests = Vec::new(); + let mut requests = Vec::with_capacity(ibc_assets_info.len()); for (denom, info) in ibc_assets_info { let fut = async move { let balance_denom = self @@ -492,7 +606,7 @@ impl TendermintCoin { protocol_info: TendermintProtocolInfo, rpc_urls: Vec, tx_history: bool, - priv_key_policy: TendermintPrivKeyPolicy, + activation_policy: TendermintActivationPolicy, ) -> MmResult { if rpc_urls.is_empty() { return MmError::err(TendermintInitError { @@ -501,17 +615,11 @@ impl TendermintCoin { }); } - let priv_key = priv_key_policy.activated_key_or_err().mm_err(|e| TendermintInitError { - ticker: ticker.clone(), - kind: TendermintInitErrorKind::Internal(e.to_string()), - })?; - - let account_id = - account_id_from_privkey(priv_key.as_slice(), &protocol_info.account_prefix).mm_err(|kind| { - TendermintInitError { - ticker: ticker.clone(), - kind, - } + let account_id = activation_policy + .generate_account_id(&protocol_info.account_prefix) + .map_to_mm(|e| TendermintInitError { + ticker: ticker.clone(), + kind: TendermintInitErrorKind::CouldNotGenerateAccountId(e.to_string()), })?; let rpc_clients = clients_from_urls(rpc_urls.as_ref()).mm_err(|kind| TendermintInitError { @@ -551,7 +659,7 @@ impl TendermintCoin { ticker, account_id, account_prefix: protocol_info.account_prefix, - priv_key_policy, + activation_policy, decimals: protocol_info.decimals, denom, chain_id, @@ -566,247 +674,31 @@ impl TendermintCoin { }))) } - pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { - let coin = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), - }; - - let (balance_denom, balance_dec) = coin - .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) - .await?; - - // << BEGIN TX SIMULATION FOR FEE CALCULATION - let (amount_denom, amount_dec) = if req.max { - let amount_denom = balance_denom; - (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) - } else { - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) - }; - - if !coin.is_tx_amount_enough(coin.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: coin.min_tx_amount(), - }); - } - - let received_by_me = if to_address == account_id { - amount_dec - } else { - BigDecimal::default() - }; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let current_block = coin - .current_block() - .compat() - .await - .map_to_mm(WithdrawError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - // >> END TX SIMULATION FOR FEE CALCULATION - - let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); - - let fee_amount_u64 = coin - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_transfer.clone(), - timeout_height, - memo.clone(), - req.fee, - ) - .await?; - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, coin.decimals()); - - let fee_amount = Coin { - denom: coin.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - - let (amount_denom, total_amount) = if req.max { - if balance_denom < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_dec, - required: fee_amount_dec, - }); - } - let amount_denom = balance_denom - fee_amount_u64; - (amount_denom, balance_dec) - } else { - let total = &req.amount + &fee_amount_dec; - if balance_dec < total { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: coin.ticker.clone(), - available: balance_dec, - required: total, - }); - } - - (sat_from_big_decimal(&req.amount, coin.decimals)?, total) - }; - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let account_info = coin.account_info(&account_id).await?; - let tx_raw = coin - .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let tx_bytes = tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let hash = sha256(&tx_bytes); - Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), - from: vec![account_id.to_string()], - to: vec![req.to], - my_balance_change: &received_by_me - &total_amount, - spent_by_me: total_amount.clone(), - total_amount, - received_by_me, - block_height: 0, - timestamp: 0, - fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { - coin: coin.ticker.clone(), - amount: fee_amount_dec, - uamount: fee_amount_u64, - gas_limit, - })), - coin: coin.ticker.to_string(), - internal_id: hash.to_vec().into(), - kmd_rewards: None, - transaction_type: TransactionType::default(), - memo: Some(memo), - }) - }; - Box::new(fut.boxed().compat()) - } - - pub async fn get_ibc_transfer_channels(&self, req: IBCTransferChannelsRequest) -> IBCTransferChannelsResult { - #[derive(Deserialize)] - struct ChainRegistry { - channels: Vec, - } - - #[derive(Deserialize)] - struct ChannelInfo { - channel_id: String, - port_id: String, - } - - #[derive(Deserialize)] - struct IbcChannel { - chain_1: ChannelInfo, - #[allow(dead_code)] - chain_2: ChannelInfo, - ordering: String, - version: String, - tags: Option, - } - - let src_chain_registry_name = self.chain_registry_name.as_ref().or_mm_err(|| { - IBCTransferChannelsRequestError::InternalError(format!( - "`chain_registry_name` is not set for '{}'", - self.platform_ticker() - )) - })?; - - let source_filename = format!( - "{}-{}.json", - src_chain_registry_name, req.destination_chain_registry_name - ); - - let git_controller: GitController = GitController::new(GITHUB_API_URI); + /// Extracts corresponding IBC channel ID for `AccountId` from https://github.com/KomodoPlatform/chain-registry/tree/nucl. + pub(crate) async fn detect_channel_id_for_ibc_transfer( + &self, + to_address: &AccountId, + ) -> Result> { + let ctx = MmArc::from_weak(&self.ctx).ok_or_else(|| WithdrawError::InternalError("No context".to_owned()))?; - let metadata_list = git_controller - .client - .get_file_metadata_list( - CHAIN_REGISTRY_REPO_OWNER, - CHAIN_REGISTRY_REPO_NAME, - CHAIN_REGISTRY_BRANCH, - CHAIN_REGISTRY_IBC_DIR_NAME, - ) - .await - .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + let source_registry_name = self + .chain_registry_name + .clone() + .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - let source_channel_file = metadata_list - .iter() - .find(|metadata| metadata.name == source_filename) - .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + let destination_registry_name = chain_registry_name_from_account_prefix(&ctx, to_address.prefix()) + .ok_or_else(|| WithdrawError::RegistryNameIsMissing(to_address.prefix().to_owned()))?; - let mut registry_object = git_controller - .client - .deserialize_json_source::(source_channel_file.to_owned()) + let channels = get_ibc_transfer_channels(source_registry_name, destination_registry_name) .await - .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + .map_err(|_| WithdrawError::IBCChannelCouldNotFound(to_address.to_string()))?; - registry_object - .channels - .retain(|ch| ch.chain_1.port_id == *IBC_OUT_SOURCE_PORT); - - let result: Vec = registry_object - .channels - .iter() - .map(|ch| IBCTransferChannel { - channel_id: ch.chain_1.channel_id.clone(), - ordering: ch.ordering.clone(), - version: ch.version.clone(), - tags: ch.tags.clone().map(|t| IBCTransferChannelTag { - status: t.status, - preferred: t.preferred, - dex: t.dex, - }), - }) - .collect(); - - Ok(IBCTransferChannelsResponse { - ibc_transfer_channels: result, - }) + Ok(channels + .ibc_transfer_channels + .last() + .ok_or_else(|| WithdrawError::InternalError("channel list can not be empty".to_owned()))? + .channel_id + .clone()) } #[inline(always)] @@ -896,7 +788,38 @@ impl TendermintCoin { sha256(&htlc_id).to_string().to_uppercase() } - pub(super) async fn seq_safe_send_raw_tx_bytes( + async fn common_send_raw_tx_bytes( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + timeout: Duration, + ) -> Result<(String, Raw), TransactionErr> { + // As there wouldn't be enough time to process the data, to mitigate potential edge problems (such as attempting to send transaction + // bytes half a second before expiration, which may take longer to send and result in the transaction amount being wasted due to a timeout), + // reduce the expiration time by 5 seconds. + let expiration = timeout - Duration::from_secs(5); + + match self.activation_policy { + TendermintActivationPolicy::PrivateKey(_) => { + try_tx_s!( + self.seq_safe_send_raw_tx_bytes(tx_payload, fee, timeout_height, memo) + .timeout(expiration) + .await + ) + }, + TendermintActivationPolicy::PublicKey(_) => { + try_tx_s!( + self.send_unsigned_tx_externally(tx_payload, fee, timeout_height, memo, expiration) + .timeout(expiration) + .await + ) + }, + } + } + + async fn seq_safe_send_raw_tx_bytes( &self, tx_payload: Any, fee: Fee, @@ -905,7 +828,7 @@ impl TendermintCoin { ) -> Result<(String, Raw), TransactionErr> { let (tx_id, tx_raw) = loop { let tx_raw = try_tx_s!(self.any_to_signed_raw_tx( - try_tx_s!(self.priv_key_policy.activated_key_or_err()), + try_tx_s!(self.activation_policy.activated_key_or_err()), try_tx_s!(self.account_info(&self.account_id).await), tx_payload.clone(), fee.clone(), @@ -929,6 +852,55 @@ impl TendermintCoin { Ok((tx_id, tx_raw)) } + async fn send_unsigned_tx_externally( + &self, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + timeout: Duration, + ) -> Result<(String, Raw), TransactionErr> { + #[derive(Deserialize)] + struct TxHashData { + hash: String, + } + + let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already"))); + + let account_info = try_tx_s!(self.account_info(&self.account_id).await); + let sign_doc = try_tx_s!(self.any_to_sign_doc(account_info, tx_payload, fee, timeout_height, memo)); + + let unsigned_tx = json!({ + "sign_doc": { + "body_bytes": sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }); + + let data: TxHashData = try_tx_s!(ctx + .ask_for_data(&format!("TX_HASH:{}", self.ticker()), unsigned_tx, timeout) + .await + .map_err(|e| ERRL!("{}", e))); + + let tx = try_tx_s!(self.request_tx(data.hash.clone()).await.map_err(|e| ERRL!("{}", e))); + + let tx_raw_inner = TxRaw { + body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + signatures: tx.signatures, + }; + + if sign_doc.body_bytes != tx_raw_inner.body_bytes { + return Err(crate::TransactionErr::Plain(ERRL!( + "Unsigned transaction don't match with the externally provided transaction." + ))); + } + + Ok((data.hash, Raw::from(tx_raw_inner))) + } + #[allow(deprecated)] pub(super) async fn calculate_fee( &self, @@ -937,9 +909,20 @@ impl TendermintCoin { memo: String, withdraw_fee: Option, ) -> MmResult { + let Ok(activated_priv_key) = self.activation_policy.activated_key_or_err() else { + let (gas_price, gas_limit) = self.gas_info_for_withdraw(&withdraw_fee, GAS_LIMIT_DEFAULT); + let amount = ((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil(); + + let fee_amount = Coin { + denom: self.platform_denom().clone(), + amount: (amount as u64).into(), + }; + + return Ok(Fee::from_amount_and_gas(fee_amount, gas_limit)); + }; + let (response, raw_response) = loop { let account_info = self.account_info(&self.account_id).await?; - let activated_priv_key = self.priv_key_policy.activated_key_or_err()?; let tx_bytes = self .gen_simulated_tx( account_info, @@ -1003,16 +986,21 @@ impl TendermintCoin { pub(super) async fn calculate_account_fee_amount_as_u64( &self, account_id: &AccountId, - priv_key: &Secp256k1Secret, + priv_key: Option, msg: Any, timeout_height: u64, memo: String, withdraw_fee: Option, ) -> MmResult { + let Some(priv_key) = priv_key else { + let (gas_price, _) = self.gas_info_for_withdraw(&withdraw_fee, 0); + return Ok(((GAS_WANTED_BASE_VALUE * 1.5) * gas_price).ceil() as u64); + }; + let (response, raw_response) = loop { let account_info = self.account_info(account_id).await?; let tx_bytes = self - .gen_simulated_tx(account_info, priv_key, msg.clone(), timeout_height, memo.clone()) + .gen_simulated_tx(account_info, &priv_key, msg.clone(), timeout_height, memo.clone()) .map_to_mm(|e| TendermintCoinRpcError::InternalError(format!("{}", e)))?; let request = AbciRequest::new( @@ -1117,6 +1105,82 @@ impl TendermintCoin { .map_to_mm(|e| TendermintCoinRpcError::InvalidResponse(format!("balance is not u64, err {}", e))) } + #[allow(clippy::result_large_err)] + pub(super) fn account_id_and_pk_for_withdraw( + &self, + withdraw_from: Option, + ) -> Result<(AccountId, Option), WithdrawError> { + if let TendermintActivationPolicy::PublicKey(_) = self.activation_policy { + return Ok((self.account_id.clone(), None)); + } + + match withdraw_from { + Some(from) => { + let path_to_coin = self + .activation_policy + .path_to_coin_or_err() + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let path_to_address = from + .to_address_path(path_to_coin.coin_type()) + .map_err(|e| WithdrawError::InternalError(e.to_string()))? + .to_derivation_path(path_to_coin) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let priv_key = self + .activation_policy + .hd_wallet_derived_priv_key_or_err(&path_to_address) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + let account_id = account_id_from_privkey(priv_key.as_slice(), &self.account_prefix) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + Ok((account_id, Some(priv_key))) + }, + None => { + let activated_key = self + .activation_policy + .activated_key_or_err() + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + + Ok((self.account_id.clone(), Some(*activated_key))) + }, + } + } + + pub(super) fn any_to_transaction_data( + &self, + maybe_pk: Option, + message: Any, + account_info: BaseAccount, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> Result { + if let Some(priv_key) = maybe_pk { + let tx_raw = self.any_to_signed_raw_tx(&priv_key, account_info, message, fee, timeout_height, memo)?; + let tx_bytes = tx_raw.to_bytes()?; + let hash = sha256(&tx_bytes); + + Ok(TransactionData::new_signed( + tx_bytes.into(), + hex::encode_upper(hash.as_slice()), + )) + } else { + let sign_doc = self.any_to_sign_doc(account_info, message, fee, timeout_height, memo)?; + + let tx = json!({ + "sign_doc": { + "body_bytes": sign_doc.body_bytes, + "auth_info_bytes": sign_doc.auth_info_bytes, + "chain_id": sign_doc.chain_id, + "account_number": sign_doc.account_number, + } + }); + + Ok(TransactionData::Unsigned(tx)) + } + } + fn gen_create_htlc_tx( &self, denom: Denom, @@ -1189,6 +1253,20 @@ impl TendermintCoin { sign_doc.sign(&signkey) } + pub(super) fn any_to_sign_doc( + &self, + account_info: BaseAccount, + tx_payload: Any, + fee: Fee, + timeout_height: u64, + memo: String, + ) -> cosmrs::Result { + let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32); + let pubkey = self.activation_policy.public_key()?.into(); + let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee); + SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number) + } + pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) { self.tokens_info .lock() @@ -1318,11 +1396,12 @@ impl TendermintCoin { ); let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( create_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(time_lock_duration), ) .await ); @@ -1342,6 +1421,7 @@ impl TendermintCoin { denom: Denom, decimals: u8, uuid: &[u8], + expires_at: u64, ) -> TransactionFut { let memo = try_tx_fus!(Uuid::from_slice(uuid)).to_string(); let from_address = self.account_id.clone(); @@ -1370,9 +1450,16 @@ impl TendermintCoin { .await ); + let timeout = expires_at.checked_sub(now_sec()).unwrap_or_default(); let (_tx_id, tx_raw) = try_tx_s!( - coin.seq_safe_send_raw_tx_bytes(tx_payload.clone(), fee.clone(), timeout_height, memo.clone()) - .await + coin.common_send_raw_tx_bytes( + tx_payload.clone(), + fee.clone(), + timeout_height, + memo.clone(), + Duration::from_secs(timeout) + ) + .await ); Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { @@ -1581,7 +1668,7 @@ impl TendermintCoin { drop_mutability!(sec); let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) - .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&amount, decimals)?; @@ -1606,9 +1693,7 @@ impl TendermintCoin { let fee_uamount = self .calculate_account_fee_amount_as_u64( &self.account_id, - self.priv_key_policy - .activated_key_or_err() - .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, + self.activation_policy.activated_key(), create_htlc_tx.msg_payload.clone(), timeout_height, TX_DEFAULT_MEMO.to_owned(), @@ -1633,7 +1718,7 @@ impl TendermintCoin { dex_fee_amount: DexFee, ) -> TradePreimageResult { let to_address = account_id_from_pubkey_hex(&self.account_prefix, DEX_FEE_ADDR_PUBKEY) - .map_err(|e| MmError::new(TradePreimageError::InternalError(e.into_inner().to_string())))?; + .map_err(|e| MmError::new(TradePreimageError::InternalError(e.to_string())))?; let amount = sat_from_big_decimal(&dex_fee_amount.fee_amount().into(), decimals)?; let current_block = self.current_block().compat().await.map_err(|e| { @@ -1659,9 +1744,7 @@ impl TendermintCoin { let fee_uamount = self .calculate_account_fee_amount_as_u64( &self.account_id, - self.priv_key_policy - .activated_key_or_err() - .mm_err(|e| TradePreimageError::InternalError(e.to_string()))?, + self.activation_policy.activated_key(), msg_send, timeout_height, TX_DEFAULT_MEMO.to_owned(), @@ -1948,38 +2031,19 @@ impl MmCoin for TendermintCoin { let fut = async move { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - if to_address.prefix() != coin.account_prefix { - return MmError::err(WithdrawError::InvalidAddress(format!( - "expected {} address prefix", - coin.account_prefix - ))); - } - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = coin.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = coin - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &coin.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => (coin.account_id.clone(), *coin.priv_key_policy.activated_key_or_err()?), - }; + let is_ibc_transfer = to_address.prefix() != coin.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_pk) = coin.account_id_and_pk_for_withdraw(req.from)?; let (balance_denom, balance_dec) = coin .get_balance_as_unsigned_and_decimal(&account_id, &coin.denom, coin.decimals()) .await?; - // << BEGIN TX SIMULATION FOR FEE CALCULATION let (amount_denom, amount_dec) = if req.max { let amount_denom = balance_denom; (amount_denom, big_decimal_from_sat_unsigned(amount_denom, coin.decimals)) } else { - let total = req.amount.clone(); - (sat_from_big_decimal(&req.amount, coin.decimals)?, req.amount.clone()) }; @@ -1996,18 +2060,32 @@ impl MmCoin for TendermintCoin { BigDecimal::default() }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address: to_address.clone(), - amount: vec![Coin { + let msg_payload = if is_ibc_transfer { + let channel_id = match req.ibc_source_channel { + Some(channel_id) => channel_id, + None => coin.detect_channel_id_for_ibc_transfer(&to_address).await?, + }; + + MsgTransfer::new_with_default_timeout(channel_id, account_id.clone(), to_address.clone(), Coin { denom: coin.denom.clone(), amount: amount_denom.into(), - }], + }) + .to_any() + } else { + MsgSend { + from_address: account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom: coin.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() } - .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); + let current_block = coin .current_block() .compat() @@ -2015,15 +2093,18 @@ impl MmCoin for TendermintCoin { .map_to_mm(WithdrawError::Transport)?; let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - // >> END TX SIMULATION FOR FEE CALCULATION - let (_, gas_limit) = coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let (_, gas_limit) = if is_ibc_transfer { + coin.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT) + } else { + coin.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT) + }; let fee_amount_u64 = coin .calculate_account_fee_amount_as_u64( &account_id, - &priv_key, - msg_send, + maybe_pk, + msg_payload.clone(), timeout_height, memo.clone(), req.fee, @@ -2061,31 +2142,19 @@ impl MmCoin for TendermintCoin { (sat_from_big_decimal(&req.amount, coin.decimals)?, total) }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address, - amount: vec![Coin { - denom: coin.denom.clone(), - amount: amount_denom.into(), - }], - } - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let account_info = coin.account_info(&account_id).await?; - let tx_raw = coin - .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let tx_bytes = tx_raw - .to_bytes() + let tx = coin + .any_to_transaction_data(maybe_pk, msg_payload, account_info, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let hash = sha256(&tx_bytes); + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), + tx, from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, @@ -2101,9 +2170,13 @@ impl MmCoin for TendermintCoin { gas_limit, })), coin: coin.ticker.to_string(), - internal_id: hash.to_vec().into(), + internal_id, kmd_rewards: None, - transaction_type: TransactionType::default(), + transaction_type: if is_ibc_transfer { + TransactionType::TendermintIBCTransfer + } else { + TransactionType::StandardTransfer + }, memo: Some(memo), }) }; @@ -2143,14 +2216,6 @@ impl MmCoin for TendermintCoin { fn validate_address(&self, address: &str) -> ValidateAddressResult { match AccountId::from_str(address) { - Ok(account) if account.prefix() != self.account_prefix => ValidateAddressResult { - is_valid: false, - reason: Some(format!( - "Expected {} account prefix, got {}", - self.account_prefix, - account.prefix() - )), - }, Ok(_) => ValidateAddressResult { is_valid: true, reason: None, @@ -2177,6 +2242,7 @@ impl MmCoin for TendermintCoin { &self, value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { let amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, @@ -2250,7 +2316,7 @@ impl MarketCoinOps for TendermintCoin { fn my_address(&self) -> MmResult { Ok(self.account_id.to_string()) } async fn get_public_key(&self) -> Result> { - let key = SigningKey::from_slice(self.priv_key_policy.activated_key_or_err()?.as_slice()) + let key = SigningKey::from_slice(self.activation_policy.activated_key_or_err()?.as_slice()) .expect("privkey validity is checked on coin creation"); Ok(key.public_key().to_string()) } @@ -2444,7 +2510,7 @@ impl MarketCoinOps for TendermintCoin { fn display_priv_key(&self) -> Result { Ok(self - .priv_key_policy + .activation_policy .activated_key_or_err() .map_err(|e| e.to_string())? .to_string()) @@ -2456,19 +2522,25 @@ impl MarketCoinOps for TendermintCoin { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } - fn is_trezor(&self) -> bool { self.priv_key_policy.is_trezor() } + fn is_trezor(&self) -> bool { + match &self.activation_policy { + TendermintActivationPolicy::PrivateKey(pk) => pk.is_trezor(), + TendermintActivationPolicy::PublicKey(_) => false, + } + } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut { self.send_taker_fee_for_denom( fee_addr, dex_fee.fee_amount().into(), self.denom.clone(), self.decimals, uuid, + expire_at, ) } @@ -2520,6 +2592,11 @@ impl SwapOps for TendermintCoin { let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, maker_spends_payment_args.secret_hash); let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, maker_spends_payment_args.secret)); + let timeout = maker_spends_payment_args + .time_lock + .checked_sub(now_sec()) + .unwrap_or_default(); + let coin = self.clone(); let current_block = try_tx_s!(self.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; @@ -2535,11 +2612,12 @@ impl SwapOps for TendermintCoin { ); let (_tx_id, tx_raw) = try_tx_s!( - self.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(timeout), ) .await ); @@ -2574,7 +2652,12 @@ impl SwapOps for TendermintCoin { let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), &amount, taker_spends_payment_args.secret_hash); + let timeout = taker_spends_payment_args + .time_lock + .checked_sub(now_sec()) + .unwrap_or_default(); let claim_htlc_tx = try_tx_s!(self.gen_claim_htlc_tx(htlc_id, taker_spends_payment_args.secret)); + let coin = self.clone(); let current_block = try_tx_s!(self.current_block().compat().await); let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; @@ -2590,11 +2673,12 @@ impl SwapOps for TendermintCoin { ); let (tx_id, tx_raw) = try_tx_s!( - self.seq_safe_send_raw_tx_bytes( + coin.common_send_raw_tx_bytes( claim_htlc_tx.msg_payload.clone(), fee.clone(), timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(timeout), ) .await ); @@ -2705,9 +2789,9 @@ impl SwapOps for TendermintCoin { } #[inline] - fn derive_htlc_key_pair(&self, swap_unique_data: &[u8]) -> KeyPair { + fn derive_htlc_key_pair(&self, _swap_unique_data: &[u8]) -> KeyPair { key_pair_from_secret( - self.priv_key_policy + self.activation_policy .activated_key_or_err() .expect("valid priv key") .as_ref(), @@ -2716,8 +2800,8 @@ impl SwapOps for TendermintCoin { } #[inline] - fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + fn derive_htlc_pubkey(&self, _swap_unique_data: &[u8]) -> Vec { + self.activation_policy.public_key().expect("valid pubkey").to_bytes() } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -2854,7 +2938,16 @@ pub fn tendermint_priv_key_policy( path_to_address: HDPathAccountToAddressId, ) -> MmResult { match priv_key_build_policy { - PrivKeyBuildPolicy::IguanaPrivKey(iguana) => Ok(TendermintPrivKeyPolicy::Iguana(iguana)), + PrivKeyBuildPolicy::IguanaPrivKey(iguana) => { + let mm2_internal_key_pair = key_pair_from_secret(iguana.as_ref()).mm_err(|e| TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + })?; + + let tendermint_pair = TendermintKeyPair::new(iguana, *mm2_internal_key_pair.public()); + + Ok(TendermintPrivKeyPolicy::Iguana(tendermint_pair)) + }, PrivKeyBuildPolicy::GlobalHDAccount(global_hd) => { let path_to_coin = conf.derivation_path.as_ref().or_mm_err(|| TendermintInitError { ticker: ticker.to_string(), @@ -2872,9 +2965,18 @@ pub fn tendermint_priv_key_policy( kind: TendermintInitErrorKind::InvalidPrivKey(e.to_string()), })?; let bip39_secp_priv_key = global_hd.root_priv_key().clone(); + let pubkey = Public::from_slice(&bip39_secp_priv_key.public_key().to_bytes()).map_to_mm(|e| { + TendermintInitError { + ticker: ticker.to_string(), + kind: TendermintInitErrorKind::Internal(e.to_string()), + } + })?; + + let tendermint_pair = TendermintKeyPair::new(activated_priv_key, pubkey); + Ok(TendermintPrivKeyPolicy::HDWallet { path_to_coin: path_to_coin.clone(), - activated_key: activated_priv_key, + activated_key: tendermint_pair, bip39_secp_priv_key, }) }, @@ -2889,6 +2991,125 @@ pub fn tendermint_priv_key_policy( } } +pub(crate) fn chain_registry_name_from_account_prefix(ctx: &MmArc, prefix: &str) -> Option { + let Some(coins) = ctx.conf["coins"].as_array() else { + return None; + }; + + for coin in coins { + let protocol = coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("type") + .unwrap_or(&serde_json::Value::Null) + .as_str(); + + if protocol != Some(TENDERMINT_COIN_PROTOCOL_TYPE) { + continue; + } + + let coin_account_prefix = coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("account_prefix") + .map(|t| t.as_str().unwrap_or_default()); + + if coin_account_prefix == Some(prefix) { + return coin + .get("protocol") + .unwrap_or(&serde_json::Value::Null) + .get("protocol_data") + .unwrap_or(&serde_json::Value::Null) + .get("chain_registry_name") + .map(|t| t.as_str().unwrap_or_default().to_owned()); + } + } + + None +} + +pub async fn get_ibc_transfer_channels( + source_registry_name: String, + destination_registry_name: String, +) -> IBCTransferChannelsResult { + #[derive(Deserialize)] + struct ChainRegistry { + channels: Vec, + } + + #[derive(Deserialize)] + struct ChannelInfo { + channel_id: String, + port_id: String, + } + + #[derive(Deserialize)] + struct IbcChannel { + #[allow(dead_code)] + chain_1: ChannelInfo, + chain_2: ChannelInfo, + ordering: String, + version: String, + tags: Option, + } + + let source_filename = format!("{}-{}.json", source_registry_name, destination_registry_name); + let git_controller: GitController = GitController::new(GITHUB_API_URI); + + let metadata_list = git_controller + .client + .get_file_metadata_list( + CHAIN_REGISTRY_REPO_OWNER, + CHAIN_REGISTRY_REPO_NAME, + CHAIN_REGISTRY_BRANCH, + CHAIN_REGISTRY_IBC_DIR_NAME, + ) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + let source_channel_file = metadata_list + .iter() + .find(|metadata| metadata.name == source_filename) + .or_mm_err(|| IBCTransferChannelsRequestError::RegistrySourceCouldNotFound(source_filename))?; + + let mut registry_object = git_controller + .client + .deserialize_json_source::(source_channel_file.to_owned()) + .await + .map_err(|e| IBCTransferChannelsRequestError::Transport(format!("{:?}", e)))?; + + registry_object + .channels + .retain(|ch| ch.chain_2.port_id == *IBC_OUT_SOURCE_PORT); + + let result: Vec = registry_object + .channels + .iter() + .map(|ch| IBCTransferChannel { + channel_id: ch.chain_2.channel_id.clone(), + ordering: ch.ordering.clone(), + version: ch.version.clone(), + tags: ch.tags.clone().map(|t| IBCTransferChannelTag { + status: t.status, + preferred: t.preferred, + dex: t.dex, + }), + }) + .collect(); + + if result.is_empty() { + return MmError::err(IBCTransferChannelsRequestError::CouldNotFindChannel( + destination_registry_name, + )); + } + + Ok(IBCTransferChannelsResponse { + ibc_transfer_channels: result, + }) +} + #[cfg(test)] pub mod tendermint_coin_tests { use super::*; @@ -2981,7 +3202,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -2990,7 +3213,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3030,11 +3253,12 @@ pub mod tendermint_coin_tests { .unwrap() }); - let send_tx_fut = coin.seq_safe_send_raw_tx_bytes( + let send_tx_fut = coin.common_send_raw_tx_bytes( create_htlc_tx.msg_payload.clone(), fee, timeout_height, TX_DEFAULT_MEMO.into(), + Duration::from_secs(10), ); block_on(async { send_tx_fut.await.unwrap(); @@ -3075,8 +3299,13 @@ pub mod tendermint_coin_tests { .unwrap() }); - let send_tx_fut = - coin.seq_safe_send_raw_tx_bytes(claim_htlc_tx.msg_payload, fee, timeout_height, TX_DEFAULT_MEMO.into()); + let send_tx_fut = coin.common_send_raw_tx_bytes( + claim_htlc_tx.msg_payload, + fee, + timeout_height, + TX_DEFAULT_MEMO.into(), + Duration::from_secs(30), + ); let (tx_id, _tx_raw) = block_on(async { send_tx_fut.await.unwrap() }); @@ -3098,7 +3327,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3107,7 +3338,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3158,7 +3389,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3167,7 +3400,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3211,7 +3444,7 @@ pub mod tendermint_coin_tests { // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; - let hash = spend_tx.tx_hash(); + let hash = spend_tx.tx_hash_as_bytes(); assert_eq!(hex::encode_upper(hash.0), expected_spend_hash); } @@ -3229,7 +3462,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3238,7 +3473,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3423,7 +3658,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3432,7 +3669,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3503,7 +3740,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3512,7 +3751,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3558,7 +3797,7 @@ pub mod tendermint_coin_tests { // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 let expected_spend_hash = "565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137"; - let hash = spend_tx.tx_hash(); + let hash = spend_tx.tx_hash_as_bytes(); assert_eq!(hex::encode_upper(hash.0), expected_spend_hash); } @@ -3576,7 +3815,9 @@ pub mod tendermint_coin_tests { }; let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = block_on(TendermintCoin::init( &ctx, @@ -3585,7 +3826,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3645,7 +3886,9 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3654,7 +3897,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3697,7 +3940,9 @@ pub mod tendermint_coin_tests { let ctx = mm2_core::mm_ctx::MmCtxBuilder::default().into_mm_arc(); let key_pair = key_pair_from_seed(IRIS_TESTNET_HTLC_PAIR1_SEED).unwrap(); - let priv_key_policy = TendermintPrivKeyPolicy::Iguana(key_pair.private().secret); + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); let coin = common::block_on(TendermintCoin::init( &ctx, @@ -3706,7 +3951,7 @@ pub mod tendermint_coin_tests { protocol_conf, rpc_urls, false, - priv_key_policy, + activation_policy, )) .unwrap(); @@ -3742,4 +3987,27 @@ pub mod tendermint_coin_tests { block_on(coin.wait_for_confirmations(confirm_payment_input).compat()).unwrap_err(); } } + + #[test] + fn test_generate_account_id() { + let key_pair = key_pair_from_seed("best seed").unwrap(); + + let tendermint_pair = TendermintKeyPair::new(key_pair.private().secret, *key_pair.public()); + let pb = PublicKey::from_raw_secp256k1(&key_pair.public().to_bytes()).unwrap(); + + let pk_activation_policy = + TendermintActivationPolicy::with_private_key_policy(TendermintPrivKeyPolicy::Iguana(tendermint_pair)); + // Derive account id from the private key. + let pk_account_id = pk_activation_policy.generate_account_id("cosmos").unwrap(); + assert_eq!( + pk_account_id.to_string(), + "cosmos1aghdjgt5gzntzqgdxdzhjfry90upmtfsy2wuwp" + ); + + let pb_activation_policy = TendermintActivationPolicy::with_public_key(pb); + // Derive account id from the public key. + let pb_account_id = pb_activation_policy.generate_account_id("cosmos").unwrap(); + // Public and private keys are from the same keypair, account ids must be equal. + assert_eq!(pk_account_id, pb_account_id); + } } diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 2e109291b7..894982aa83 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -5,8 +5,6 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{TendermintCoin, TendermintFeeDetails, GAS_LIMIT_DEFAULT, MIN_TX_SATOSHIS, TIMEOUT_HEIGHT_DELTA, TX_DEFAULT_MEMO}; use crate::coin_errors::ValidatePaymentResult; -use crate::rpc_command::tendermint::IBCWithdrawRequest; -use crate::tendermint::account_id_from_privkey; use crate::utxo::utxo_common::big_decimal_from_sat; use crate::{big_decimal_from_sat_unsigned, utxo::sat_from_big_decimal, BalanceFut, BigDecimal, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, @@ -104,174 +102,19 @@ impl TendermintToken { }; Ok(TendermintToken(Arc::new(token_impl))) } - - pub fn ibc_withdraw(&self, req: IBCWithdrawRequest) -> WithdrawFut { - let platform = self.platform_coin.clone(); - let token = self.clone(); - let fut = async move { - let to_address = - AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = platform.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = platform - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => ( - platform.account_id.clone(), - *platform.priv_key_policy.activated_key_or_err()?, - ), - }; - - let (base_denom_balance, base_denom_balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) - .await?; - - let (balance_denom, balance_dec) = platform - .get_balance_as_unsigned_and_decimal(&account_id, &token.denom, token.decimals()) - .await?; - - let (amount_denom, amount_dec, total_amount) = if req.max { - ( - balance_denom, - big_decimal_from_sat_unsigned(balance_denom, token.decimals), - balance_dec, - ) - } else { - if balance_dec < req.amount { - return MmError::err(WithdrawError::NotSufficientBalance { - coin: token.ticker.clone(), - available: balance_dec, - required: req.amount, - }); - } - - ( - sat_from_big_decimal(&req.amount, token.decimals())?, - req.amount.clone(), - req.amount, - ) - }; - - if !platform.is_tx_amount_enough(token.decimals, &amount_dec) { - return MmError::err(WithdrawError::AmountTooLow { - amount: amount_dec, - threshold: token.min_tx_amount(), - }); - } - - let received_by_me = if to_address == account_id { - amount_dec - } else { - BigDecimal::default() - }; - - let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); - - let msg_transfer = MsgTransfer::new_with_default_timeout( - req.ibc_source_channel.clone(), - account_id.clone(), - to_address.clone(), - Coin { - denom: token.denom.clone(), - amount: amount_denom.into(), - }, - ) - .to_any() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let current_block = token - .current_block() - .compat() - .await - .map_to_mm(WithdrawError::Transport)?; - - let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - - let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT); - - let fee_amount_u64 = platform - .calculate_account_fee_amount_as_u64( - &account_id, - &priv_key, - msg_transfer.clone(), - timeout_height, - memo.clone(), - req.fee, - ) - .await?; - - let fee_amount_dec = big_decimal_from_sat_unsigned(fee_amount_u64, platform.decimals()); - - if base_denom_balance < fee_amount_u64 { - return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { - coin: platform.ticker().to_string(), - available: base_denom_balance_dec, - required: fee_amount_dec, - }); - } - - let fee_amount = Coin { - denom: platform.denom.clone(), - amount: fee_amount_u64.into(), - }; - - let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); - - let account_info = platform.account_info(&account_id).await?; - let tx_raw = platform - .any_to_signed_raw_tx(&priv_key, account_info, msg_transfer, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let tx_bytes = tx_raw - .to_bytes() - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - - let hash = sha256(&tx_bytes); - Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), - from: vec![account_id.to_string()], - to: vec![req.to], - my_balance_change: &received_by_me - &total_amount, - spent_by_me: total_amount.clone(), - total_amount, - received_by_me, - block_height: 0, - timestamp: 0, - fee_details: Some(TxFeeDetails::Tendermint(TendermintFeeDetails { - coin: platform.ticker().to_string(), - amount: fee_amount_dec, - uamount: fee_amount_u64, - gas_limit, - })), - coin: token.ticker.clone(), - internal_id: hash.to_vec().into(), - kmd_rewards: None, - transaction_type: TransactionType::default(), - memo: Some(memo), - }) - }; - Box::new(fut.boxed().compat()) - } } #[async_trait] #[allow(unused_variables)] impl SwapOps for TendermintToken { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], expire_at: u64) -> TransactionFut { self.platform_coin.send_taker_fee_for_denom( fee_addr, dex_fee.fee_amount().into(), self.denom.clone(), self.decimals, uuid, + expire_at, ) } @@ -417,7 +260,7 @@ impl SwapOps for TendermintToken { #[inline] fn derive_htlc_pubkey(&self, swap_unique_data: &[u8]) -> Vec { - self.derive_htlc_key_pair(swap_unique_data).public_slice().to_vec() + self.platform_coin.derive_htlc_pubkey(swap_unique_data) } fn validate_other_pubkey(&self, raw_pubkey: &[u8]) -> MmResult<(), ValidateOtherPubKeyErr> { @@ -624,7 +467,7 @@ impl MarketCoinOps for TendermintToken { #[inline] fn min_trading_vol(&self) -> MmNumber { self.min_tx_amount().into() } - fn is_trezor(&self) -> bool { self.platform_coin.priv_key_policy.is_trezor() } + fn is_trezor(&self) -> bool { self.platform_coin.is_trezor() } } #[async_trait] @@ -640,29 +483,10 @@ impl MmCoin for TendermintToken { let fut = async move { let to_address = AccountId::from_str(&req.to).map_to_mm(|e| WithdrawError::InvalidAddress(e.to_string()))?; - if to_address.prefix() != platform.account_prefix { - return MmError::err(WithdrawError::InvalidAddress(format!( - "expected {} address prefix", - platform.account_prefix - ))); - } - let (account_id, priv_key) = match req.from { - Some(from) => { - let path_to_coin = platform.priv_key_policy.path_to_coin_or_err()?; - let path_to_address = from.to_address_path(path_to_coin.coin_type())?; - let priv_key = platform - .priv_key_policy - .hd_wallet_derived_priv_key_or_err(&path_to_address.to_derivation_path(path_to_coin)?)?; - let account_id = account_id_from_privkey(priv_key.as_slice(), &platform.account_prefix) - .map_err(|e| WithdrawError::InternalError(e.to_string()))?; - (account_id, priv_key) - }, - None => ( - platform.account_id.clone(), - *platform.priv_key_policy.activated_key_or_err()?, - ), - }; + let is_ibc_transfer = to_address.prefix() != platform.account_prefix || req.ibc_source_channel.is_some(); + + let (account_id, maybe_pk) = platform.account_id_and_pk_for_withdraw(req.from)?; let (base_denom_balance, base_denom_balance_dec) = platform .get_balance_as_unsigned_and_decimal(&account_id, &platform.denom, token.decimals()) @@ -707,15 +531,28 @@ impl MmCoin for TendermintToken { BigDecimal::default() }; - let msg_send = MsgSend { - from_address: account_id.clone(), - to_address, - amount: vec![Coin { + let msg_payload = if is_ibc_transfer { + let channel_id = match req.ibc_source_channel { + Some(channel_id) => channel_id, + None => platform.detect_channel_id_for_ibc_transfer(&to_address).await?, + }; + + MsgTransfer::new_with_default_timeout(channel_id, account_id.clone(), to_address.clone(), Coin { denom: token.denom.clone(), amount: amount_denom.into(), - }], + }) + .to_any() + } else { + MsgSend { + from_address: account_id.clone(), + to_address: to_address.clone(), + amount: vec![Coin { + denom: token.denom.clone(), + amount: amount_denom.into(), + }], + } + .to_any() } - .to_any() .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; let memo = req.memo.unwrap_or_else(|| TX_DEFAULT_MEMO.into()); @@ -727,13 +564,17 @@ impl MmCoin for TendermintToken { let timeout_height = current_block + TIMEOUT_HEIGHT_DELTA; - let (_, gas_limit) = platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT); + let (_, gas_limit) = if is_ibc_transfer { + platform.gas_info_for_withdraw(&req.fee, IBC_GAS_LIMIT_DEFAULT) + } else { + platform.gas_info_for_withdraw(&req.fee, GAS_LIMIT_DEFAULT) + }; let fee_amount_u64 = platform .calculate_account_fee_amount_as_u64( &account_id, - &priv_key, - msg_send.clone(), + maybe_pk, + msg_payload.clone(), timeout_height, memo.clone(), req.fee, @@ -758,18 +599,18 @@ impl MmCoin for TendermintToken { let fee = Fee::from_amount_and_gas(fee_amount, gas_limit); let account_info = platform.account_info(&account_id).await?; - let tx_raw = platform - .any_to_signed_raw_tx(&priv_key, account_info, msg_send, fee, timeout_height, memo.clone()) - .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let tx_bytes = tx_raw - .to_bytes() + let tx = platform + .any_to_transaction_data(maybe_pk, msg_payload, account_info, fee, timeout_height, memo.clone()) .map_to_mm(|e| WithdrawError::InternalError(e.to_string()))?; - let hash = sha256(&tx_bytes); + let internal_id = { + let hex_vec = tx.tx_hex().cloned().unwrap_or_default().to_vec(); + sha256(&hex_vec).to_vec().into() + }; + Ok(TransactionDetails { - tx_hash: hex::encode_upper(hash.as_slice()), - tx_hex: tx_bytes.into(), + tx, from: vec![account_id.to_string()], to: vec![req.to], my_balance_change: &received_by_me - &total_amount, @@ -785,9 +626,13 @@ impl MmCoin for TendermintToken { gas_limit, })), coin: token.ticker.clone(), - internal_id: hash.to_vec().into(), + internal_id, kmd_rewards: None, - transaction_type: TransactionType::default(), + transaction_type: if is_ibc_transfer { + TransactionType::TendermintIBCTransfer + } else { + TransactionType::StandardTransfer + }, memo: Some(memo), }) }; @@ -823,6 +668,7 @@ impl MmCoin for TendermintToken { &self, value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { let amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => decimal, diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 86a2b40ab4..1c8adf8de3 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -5,13 +5,15 @@ use crate::tendermint::htlc::CustomTendermintMsgType; use crate::tendermint::TendermintFeeDetails; use crate::tx_history_storage::{GetTxHistoryFilters, WalletId}; use crate::utxo::utxo_common::big_decimal_from_sat_unsigned; -use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionDetails, TransactionType, TxFeeDetails}; +use crate::{HistorySyncState, MarketCoinOps, MmCoin, TransactionData, TransactionDetails, TransactionType, + TxFeeDetails}; use async_trait::async_trait; +use base64::Engine; use bitcrypto::sha256; use common::executor::Timer; use common::log; -use cosmrs::tendermint::abci::Code as TxCode; use cosmrs::tendermint::abci::Event; +use cosmrs::tendermint::abci::{Code as TxCode, EventAttribute}; use cosmrs::tx::Fee; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmResult; @@ -23,6 +25,26 @@ use rpc::v1::types::Bytes as BytesJson; use std::cmp; use std::convert::Infallible; +const TX_PAGE_SIZE: u8 = 50; + +const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; +const CREATE_HTLC_EVENT: &str = "create_htlc"; +const CLAIM_HTLC_EVENT: &str = "claim_htlc"; +const TRANSFER_EVENT: &str = "transfer"; +const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; + +const RECEIVER_TAG_KEY: &str = "receiver"; +const RECEIVER_TAG_KEY_BASE64: &str = "cmVjZWl2ZXI="; + +const RECIPIENT_TAG_KEY: &str = "recipient"; +const RECIPIENT_TAG_KEY_BASE64: &str = "cmVjaXBpZW50"; + +const SENDER_TAG_KEY: &str = "sender"; +const SENDER_TAG_KEY_BASE64: &str = "c2VuZGVy"; + +const AMOUNT_TAG_KEY: &str = "amount"; +const AMOUNT_TAG_KEY_BASE64: &str = "YW1vdW50"; + macro_rules! try_or_return_stopped_as_err { ($exp:expr, $reason: expr, $fmt:literal) => { match $exp { @@ -293,18 +315,6 @@ where self: Box, ctx: &mut TendermintTxHistoryStateMachine, ) -> StateResult> { - const TX_PAGE_SIZE: u8 = 50; - - const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; - const CREATE_HTLC_EVENT: &str = "create_htlc"; - const CLAIM_HTLC_EVENT: &str = "claim_htlc"; - const TRANSFER_EVENT: &str = "transfer"; - const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; - const RECIPIENT_TAG_KEY: &str = "recipient"; - const SENDER_TAG_KEY: &str = "sender"; - const RECEIVER_TAG_KEY: &str = "receiver"; - const AMOUNT_TAG_KEY: &str = "amount"; - struct TxAmounts { total: BigDecimal, spent_by_me: BigDecimal, @@ -392,23 +402,26 @@ where fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { match msg_event.kind.as_str() { CREATE_HTLC_EVENT => { - transfer_details.from = - some_or_return!(msg_event.attributes.iter().find(|tag| tag.key == SENDER_TAG_KEY)) - .value - .to_string(); - - transfer_details.to = - some_or_return!(msg_event.attributes.iter().find(|tag| tag.key == RECEIVER_TAG_KEY)) - .value - .to_string(); + transfer_details.from = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); + + transfer_details.to = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + RECEIVER_TAG_KEY, + RECEIVER_TAG_KEY_BASE64, + )); transfer_details.transfer_event_type = TransferEventType::CreateHtlc; }, CLAIM_HTLC_EVENT => { - transfer_details.from = - some_or_return!(msg_event.attributes.iter().find(|tag| tag.key == SENDER_TAG_KEY)) - .value - .to_string(); + transfer_details.from = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; }, @@ -421,10 +434,12 @@ where for (index, event) in tx_events.iter().enumerate() { if event.kind.as_str() == TRANSFER_EVENT { - let amount_with_denoms = - some_or_continue!(event.attributes.iter().find(|tag| tag.key == AMOUNT_TAG_KEY)) - .value - .to_string(); + let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + AMOUNT_TAG_KEY, + AMOUNT_TAG_KEY_BASE64 + )); + let amount_with_denoms = amount_with_denoms.split(','); for amount_with_denom in amount_with_denoms { @@ -433,13 +448,17 @@ where let denom = &amount_with_denom[extracted_amount.len()..]; let amount = some_or_continue!(extracted_amount.parse().ok()); - let from = some_or_continue!(event.attributes.iter().find(|tag| tag.key == SENDER_TAG_KEY)) - .value - .to_string(); + let from = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); - let to = some_or_continue!(event.attributes.iter().find(|tag| tag.key == RECIPIENT_TAG_KEY)) - .value - .to_string(); + let to = some_or_continue!(get_value_from_event_attributes( + &event.attributes, + RECIPIENT_TAG_KEY, + RECIPIENT_TAG_KEY_BASE64, + )); let mut tx_details = TransferDetails { from, @@ -492,12 +511,8 @@ where // Retain fee related events events.retain(|event| { if event.kind == TRANSFER_EVENT { - let amount_with_denom = event - .attributes - .iter() - .find(|tag| tag.key == AMOUNT_TAG_KEY) - .map(|t| t.value.to_string()); - + let amount_with_denom = + get_value_from_event_attributes(&event.attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); amount_with_denom != Some(fee_amount_with_denom.clone()) } else { true @@ -705,8 +720,7 @@ where received_by_me: tx_amounts.received_by_me, // This can be 0 since it gets remapped in `coins::my_tx_history_v2` my_balance_change: BigDecimal::default(), - tx_hash: tx_hash.to_string(), - tx_hex: msg.into(), + tx: TransactionData::new_signed(msg.into(), tx_hash.to_string()), fee_details: Some(TxFeeDetails::Tendermint(fee_details.clone())), block_height: tx.height.into(), coin: transfer_details.denom.clone(), @@ -879,6 +893,24 @@ where } } +/// Find, decode (if needed) and return the event attribute value. +/// +/// If the attribute doesn't exist, or decoding fails, `None` will be returned. +fn get_value_from_event_attributes(events: &[EventAttribute], tag: &str, base64_encoded_tag: &str) -> Option { + let event_attribute = events + .iter() + .find(|attribute| attribute.key == tag || attribute.key == base64_encoded_tag)?; + + if event_attribute.key == base64_encoded_tag { + let decoded_bytes = base64::engine::general_purpose::STANDARD + .decode(event_attribute.value.clone()) + .ok()?; + String::from_utf8(decoded_bytes).ok() + } else { + Some(event_attribute.value.clone()) + } +} + pub async fn tendermint_history_loop( coin: TendermintCoin, storage: impl TxHistoryStorage, @@ -906,3 +938,101 @@ pub async fn tendermint_history_loop( .await .expect("The error of this machine is Infallible"); } + +#[cfg(any(test, target_arch = "wasm32"))] +mod tests { + use super::*; + use common::cross_test; + + common::cfg_wasm32! { + use wasm_bindgen_test::*; + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + } + + cross_test!(test_get_value_from_event_attributes, { + let attributes = vec![ + EventAttribute { + key: "recipient".to_owned(), + value: "nuc1erfnkjsmalkwtvj44qnfr2drfzdt4n9ledw63y".to_owned(), + index: false, + }, + EventAttribute { + key: "sender".to_owned(), + value: "nuc1a7xynj4ceft8kgdjr6kcq0s07y3ccya60rqwwn".to_owned(), + index: false, + }, + EventAttribute { + key: "amount".to_owned(), + value: "8000ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C".to_owned(), + index: false, + }, + ]; + + let value = get_value_from_event_attributes(&attributes, "invalid", ""); + assert_eq!(value, None); + let value = get_value_from_event_attributes(&attributes, RECIPIENT_TAG_KEY, RECIPIENT_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc1erfnkjsmalkwtvj44qnfr2drfzdt4n9ledw63y"); + let value = get_value_from_event_attributes(&attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc1a7xynj4ceft8kgdjr6kcq0s07y3ccya60rqwwn"); + let value = get_value_from_event_attributes(&attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64).unwrap(); + assert_eq!( + value, + "8000ibc/F7F28FF3C09024A0225EDBBDB207E5872D2B4EF2FB874FE47B05EF9C9A7D211C" + ); + + let encoded_attributes = vec![ + EventAttribute { + key: "cmVjaXBpZW50".to_owned(), + value: "bnVjMTd4cGZ2YWttMmFtZzk2MnlsczZmODR6M2tlbGw4YzVsM3B6YTJ5".to_owned(), + index: true, + }, + EventAttribute { + key: "c2VuZGVy".to_owned(), + value: "bnVjMWE3eHluajRjZWZ0OGtnZGpyNmtjcTBzMDd5M2NjeWE2MHJxd3du".to_owned(), + index: true, + }, + EventAttribute { + key: "YW1vdW50".to_owned(), + value: "MjcxNjJ1bnVjbA==".to_owned(), + index: true, + }, + ]; + + let value = get_value_from_event_attributes(&encoded_attributes, "invalid", ""); + assert_eq!(value, None); + let value = + get_value_from_event_attributes(&encoded_attributes, RECIPIENT_TAG_KEY, RECIPIENT_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc17xpfvakm2amg962yls6f84z3kell8c5l3pza2y"); + let value = + get_value_from_event_attributes(&encoded_attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "nuc1a7xynj4ceft8kgdjr6kcq0s07y3ccya60rqwwn"); + let value = + get_value_from_event_attributes(&encoded_attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64).unwrap(); + assert_eq!(value, "27162unucl"); + + let invalid_attributes = vec![ + EventAttribute { + key: String::default(), + value: String::default(), + index: true, + }, + EventAttribute { + key: "invalid-key".to_owned(), + value: String::default(), + index: true, + }, + EventAttribute { + key: "dummy-key".to_owned(), + value: String::default(), + index: true, + }, + ]; + + let value = get_value_from_event_attributes(&invalid_attributes, RECIPIENT_TAG_KEY, RECIPIENT_TAG_KEY_BASE64); + assert_eq!(value, None); + let value = get_value_from_event_attributes(&invalid_attributes, SENDER_TAG_KEY, SENDER_TAG_KEY_BASE64); + assert_eq!(value, None); + let value = get_value_from_event_attributes(&invalid_attributes, AMOUNT_TAG_KEY, AMOUNT_TAG_KEY_BASE64); + assert_eq!(value, None); + }); +} diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index 684edc6a29..c077bf60bd 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -114,7 +114,9 @@ impl MarketCoinOps for TestCoin { #[async_trait] #[mockable] impl SwapOps for TestCoin { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8]) -> TransactionFut { unimplemented!() } + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, uuid: &[u8], _expire_at: u64) -> TransactionFut { + unimplemented!() + } fn send_maker_payment(&self, _maker_payment_args: SendPaymentArgs) -> TransactionFut { unimplemented!() } @@ -362,6 +364,7 @@ impl MmCoin for TestCoin { &self, _value: TradePreimageValue, _stage: FeeApproxStage, + _include_refund_fee: bool, ) -> TradePreimageResult { unimplemented!() } @@ -419,7 +422,7 @@ pub struct TestTx {} impl Transaction for TestTx { fn tx_hex(&self) -> Vec { todo!() } - fn tx_hash(&self) -> BytesJson { todo!() } + fn tx_hash_as_bytes(&self) -> BytesJson { todo!() } } pub struct TestPreimage {} diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index 49993e4c6a..cf0575f973 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -447,24 +447,28 @@ impl TxHistoryStorage for SqliteTxHistoryStorage { let sql_transaction = conn.transaction()?; for tx in transactions { - let tx_hash = tx.tx_hash.clone(); + let Some(tx_hash) = tx.tx.tx_hash() else { continue }; + let Some(tx_hex) = tx.tx.tx_hex().cloned() else { + continue; + }; + let tx_hex = format!("{:02x}", tx_hex); + let internal_id = format!("{:02x}", tx.internal_id); let confirmation_status = ConfirmationStatus::from_block_height(tx.block_height); let token_id = token_id_from_tx_type(&tx.transaction_type); let tx_json = json::to_string(&tx).expect("serialization should not fail"); - let tx_hex = format!("{:02x}", tx.tx_hex); - let tx_cache_params = [&tx_hash, &tx_hex]; + let tx_cache_params = [tx_hash, &tx_hex]; sql_transaction.execute(&insert_tx_in_cache_sql(&wallet_id)?, tx_cache_params)?; let params = [ tx_hash, - internal_id.clone(), - tx.block_height.to_string(), - confirmation_status.to_sql_param_str(), - token_id, - tx_json, + &internal_id, + &tx.block_height.to_string(), + &confirmation_status.to_sql_param_str(), + &token_id, + &tx_json, ]; sql_transaction.execute(&insert_tx_in_history_sql(&wallet_id)?, params)?; diff --git a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs index ab4a2a7e85..2ffcc9760d 100644 --- a/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs +++ b/mm2src/coins/tx_history_storage/tx_history_v2_tests.rs @@ -363,24 +363,24 @@ async fn test_add_and_get_tx_from_cache_impl() { let tx = get_bch_tx_details("6686ee013620d31ba645b27d581fed85437ce00f46b595a576718afac4dd5b69"); storage - .add_tx_to_cache(&wallet_id_1, &tx.tx_hash, &tx.tx_hex) + .add_tx_to_cache(&wallet_id_1, tx.tx.tx_hash().unwrap(), tx.tx.tx_hex().unwrap()) .await .unwrap(); let tx_hex_from_1 = storage - .tx_bytes_from_cache(&wallet_id_1, &tx.tx_hash) + .tx_bytes_from_cache(&wallet_id_1, tx.tx.tx_hash().unwrap()) .await .unwrap() .unwrap(); - assert_eq!(tx_hex_from_1, tx.tx_hex); + assert_eq!(&tx_hex_from_1, tx.tx.tx_hex().unwrap()); // Since `wallet_id_1` and `wallet_id_2` wallets have the same `ticker`, the wallets must have one transaction cache. let tx_hex_from_2 = storage - .tx_bytes_from_cache(&wallet_id_2, &tx.tx_hash) + .tx_bytes_from_cache(&wallet_id_2, tx.tx.tx_hash().unwrap()) .await .unwrap() .unwrap(); - assert_eq!(tx_hex_from_2, tx.tx_hex); + assert_eq!(&tx_hex_from_2, tx.tx.tx_hex().unwrap()); } async fn test_get_raw_tx_bytes_on_add_transactions_impl() { @@ -401,7 +401,7 @@ async fn test_get_raw_tx_bytes_on_add_transactions_impl() { let mut tx2 = tx1.clone(); tx2.internal_id = BytesJson(vec![1; 32]); - let expected_tx_hex = tx1.tx_hex.clone(); + let expected_tx_hex = tx1.tx.tx_hex().unwrap().clone(); let transactions = [tx1, tx2]; storage diff --git a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs index b55b04ad86..acc6d338e2 100644 --- a/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/wasm/tx_history_storage_v2.rs @@ -59,13 +59,15 @@ impl TxHistoryStorage for IndexedDbTxHistoryStorage { let cache_table = db_transaction.table::().await?; for tx in transactions { + let Some(tx_hash) = tx.tx.tx_hash() else { continue }; + let history_item = TxHistoryTableV2::from_tx_details(wallet_id.clone(), &tx)?; history_table.add_item(&history_item).await?; - let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx); + let cache_item = TxCacheTableV2::from_tx_details(wallet_id.clone(), &tx)?; let index_keys = MultiIndex::new(TxCacheTableV2::COIN_TX_HASH_INDEX) .with_value(&wallet_id.ticker)? - .with_value(&tx.tx_hash)?; + .with_value(tx_hash)?; // `TxHistoryTableV2::tx_hash` is not a unique field, but `TxCacheTableV2::tx_hash` is unique. // So we use `DbTable::add_item_or_ignore_by_unique_multi_index` instead of `DbTable::add_item` // since `transactions` may contain txs with same `tx_hash` but different `internal_id`. @@ -396,12 +398,17 @@ impl TxHistoryTableV2 { const WALLET_ID_TOKEN_ID_INDEX: &'static str = "wallet_id_token_id"; fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + let tx_hash = tx + .tx + .tx_hash() + .ok_or_else(|| WasmTxHistoryError::NotSupported("Unsupported type of TransactionDetails".to_string()))?; + let details_json = json::to_value(tx).map_to_mm(|e| WasmTxHistoryError::ErrorSerializing(e.to_string()))?; let hd_wallet_rmd160 = wallet_id.hd_wallet_rmd160_or_exclude(); Ok(TxHistoryTableV2 { coin: wallet_id.ticker, hd_wallet_rmd160, - tx_hash: tx.tx_hash.clone(), + tx_hash: tx_hash.to_string(), internal_id: tx.internal_id.clone(), block_height: BeBigUint::from(tx.block_height), confirmation_status: ConfirmationStatus::from_block_height(tx.block_height), @@ -458,12 +465,18 @@ impl TxCacheTableV2 { /// * tx_hash - transaction hash const COIN_TX_HASH_INDEX: &'static str = "coin_tx_hash"; - fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> TxCacheTableV2 { - TxCacheTableV2 { - coin: wallet_id.ticker, - tx_hash: tx.tx_hash.clone(), - tx_hex: tx.tx_hex.clone(), + fn from_tx_details(wallet_id: WalletId, tx: &TransactionDetails) -> WasmTxHistoryResult { + if let (Some(tx_hash), Some(tx_hex)) = (tx.tx.tx_hash(), tx.tx.tx_hex()) { + return Ok(TxCacheTableV2 { + coin: wallet_id.ticker, + tx_hash: tx_hash.to_string(), + tx_hex: tx_hex.clone(), + }); } + + MmError::err(WasmTxHistoryError::NotSupported( + "Unsupported type of TransactionDetails".to_string(), + )) } } diff --git a/mm2src/coins/utxo.rs b/mm2src/coins/utxo.rs index 6cf25fcf7f..ebd7f72f29 100644 --- a/mm2src/coins/utxo.rs +++ b/mm2src/coins/utxo.rs @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright © 2023 Pampex LTD and TillyHK LTD * + * Copyright © 2023 Pampex LTD and TillyHK LTD * * * * See the CONTRIBUTOR-LICENSE-AGREEMENT, COPYING, LICENSE-COPYRIGHT-NOTICE * * and DEVELOPER-CERTIFICATE-OF-ORIGIN files in the LEGAL directory in * @@ -183,7 +183,7 @@ impl Transaction for UtxoTx { } } - fn tx_hash(&self) -> BytesJson { self.hash().reversed().to_vec().into() } + fn tx_hash_as_bytes(&self) -> BytesJson { self.hash().reversed().to_vec().into() } } impl From for BalanceError { @@ -309,21 +309,20 @@ pub struct CachedUnspentInfo { pub value: u64, } -impl From for CachedUnspentInfo { - fn from(unspent: UnspentInfo) -> CachedUnspentInfo { +impl CachedUnspentInfo { + fn from_unspent_info(unspent: &UnspentInfo) -> CachedUnspentInfo { CachedUnspentInfo { outpoint: unspent.outpoint, value: unspent.value, } } -} -impl From for UnspentInfo { - fn from(cached: CachedUnspentInfo) -> UnspentInfo { + fn to_unspent_info(&self, script: Script) -> UnspentInfo { UnspentInfo { - outpoint: cached.outpoint, - value: cached.value, + outpoint: self.outpoint, + value: self.value, height: None, + script, } } } @@ -350,22 +349,17 @@ impl RecentlySpentOutPoints { } pub fn add_spent(&mut self, inputs: Vec, spend_tx_hash: H256, outputs: Vec) { - let inputs: HashSet<_> = inputs.into_iter().map(From::from).collect(); + let inputs: HashSet<_> = inputs.iter().map(CachedUnspentInfo::from_unspent_info).collect(); let to_replace: HashSet<_> = outputs - .iter() + .into_iter() .enumerate() - .filter_map(|(index, output)| { - if output.script_pubkey == self.for_script_pubkey { - Some(CachedUnspentInfo { - outpoint: OutPoint { - hash: spend_tx_hash, - index: index as u32, - }, - value: output.value, - }) - } else { - None - } + .filter(|(_, output)| output.script_pubkey == self.for_script_pubkey) + .map(|(index, output)| CachedUnspentInfo { + outpoint: OutPoint { + hash: spend_tx_hash, + index: index as u32, + }, + value: output.value, }) .collect(); @@ -400,13 +394,14 @@ impl RecentlySpentOutPoints { pub fn replace_spent_outputs_with_cache(&self, mut outputs: HashSet) -> HashSet { let mut replacement_unspents = HashSet::new(); outputs.retain(|unspent| { - let outs = self.input_to_output_map.get(&unspent.clone().into()); + let outs = self + .input_to_output_map + .get(&CachedUnspentInfo::from_unspent_info(unspent)); + match outs { Some(outs) => { - for out in outs.iter() { - if !replacement_unspents.contains(out) { - replacement_unspents.insert(out.clone()); - } + for out in outs { + replacement_unspents.insert(out.clone()); } false }, @@ -416,7 +411,11 @@ impl RecentlySpentOutPoints { if replacement_unspents.is_empty() { return outputs; } - outputs.extend(replacement_unspents.into_iter().map(From::from)); + outputs.extend( + replacement_unspents + .iter() + .map(|cached| cached.to_unspent_info(self.for_script_pubkey.clone().into())), + ); self.replace_spent_outputs_with_cache(outputs) } } @@ -1795,6 +1794,7 @@ where outpoint: input.previous_output, value: input.amount, height: None, + script: input.prev_script.clone(), }) .collect(); @@ -1803,12 +1803,9 @@ where _ => coin.as_ref().conf.signature_version, }; - let prev_script = utxo_common::output_script_checked(coin.as_ref(), &my_address) - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let signed = try_tx_s!(sign_tx( unsigned, key_pair, - prev_script, signature_version, coin.as_ref().conf.fork_id )); @@ -1830,6 +1827,9 @@ pub fn output_script(address: &Address) -> Result { } } +/// Builds transaction output script for a legacy P2PK address +pub fn output_script_p2pk(pubkey: &Public) -> Script { Builder::build_p2pk(pubkey) } + pub fn address_by_conf_and_pubkey_str( coin: &str, conf: &Json, @@ -1854,16 +1854,15 @@ pub fn address_by_conf_and_pubkey_str( let conf_builder = UtxoConfBuilder::new(conf, ¶ms, coin); let utxo_conf = try_s!(conf_builder.build()); let pubkey_bytes = try_s!(hex::decode(pubkey)); - let hash = dhash160(&pubkey_bytes); + let pubkey = try_s!(Public::from_slice(&pubkey_bytes)); let address = AddressBuilder::new( addr_format, - hash.into(), utxo_conf.checksum_type, utxo_conf.address_prefixes, utxo_conf.bech32_hrp, ) - .as_pkh() + .as_pkh_from_pk(pubkey) .build()?; address.display_address() } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index c832d1a75d..2b94135933 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -870,7 +870,7 @@ impl UtxoCommonOps for BchCoin { #[async_trait] impl SwapOps for BchCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } @@ -1302,6 +1302,7 @@ impl MmCoin for BchCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { utxo_common::get_sender_trade_fee(self, value, stage).await } diff --git a/mm2src/coins/utxo/bchd_grpc.rs b/mm2src/coins/utxo/bchd_grpc.rs index 6017f3b9c0..da240508c6 100644 --- a/mm2src/coins/utxo/bchd_grpc.rs +++ b/mm2src/coins/utxo/bchd_grpc.rs @@ -260,6 +260,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -271,6 +272,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -294,6 +296,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -305,6 +308,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -316,6 +320,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, @@ -341,6 +346,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 999, }; @@ -353,6 +359,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }]; @@ -386,6 +393,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 1000, }, @@ -397,6 +405,7 @@ mod bchd_grpc_tests { }, value: 0, height: None, + script: Vec::new().into(), }, slp_amount: 8999, }, diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index 9d1b16723d..8b8a60d246 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -146,12 +146,11 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { let utxo = self.as_ref(); AddressBuilder::new( self.addr_format().clone(), - AddressHashEnum::AddressHash(address.0.into()), utxo.conf.checksum_type, utxo.conf.address_prefixes.clone(), utxo.conf.bech32_hrp.clone(), ) - .as_pkh() + .as_pkh(AddressHashEnum::AddressHash(address.0.into())) .build() .expect("valid address props") } @@ -161,20 +160,6 @@ pub trait QtumBasedCoin: UtxoCommonOps + MarketCoinOps { contract_addr_from_utxo_addr(my_address).mm_err(Qrc20AddressError::from) } - fn utxo_address_from_contract_addr(&self, address: H160) -> Address { - let utxo = self.as_ref(); - AddressBuilder::new( - self.addr_format().clone(), - AddressHashEnum::AddressHash(address.0.into()), - utxo.conf.checksum_type, - utxo.conf.address_prefixes.clone(), - utxo.conf.bech32_hrp.clone(), - ) - .as_pkh() - .build() - .expect("valid address props") - } - fn contract_address_from_raw_pubkey(&self, pubkey: &[u8]) -> Result { let utxo = self.as_ref(); let qtum_address = try_s!(utxo_common::address_from_raw_pubkey( @@ -524,7 +509,7 @@ impl UtxoStandardOps for QtumCoin { #[async_trait] impl SwapOps for QtumCoin { #[inline] - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { utxo_common::send_taker_fee(self.clone(), fee_addr, dex_fee) } @@ -941,6 +926,7 @@ impl MmCoin for QtumCoin { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { utxo_common::get_sender_trade_fee(self, value, stage).await } diff --git a/mm2src/coins/utxo/qtum_delegation.rs b/mm2src/coins/utxo/qtum_delegation.rs index 4a62adcd26..ad9aec86e1 100644 --- a/mm2src/coins/utxo/qtum_delegation.rs +++ b/mm2src/coins/utxo/qtum_delegation.rs @@ -8,7 +8,7 @@ use crate::utxo::utxo_common::{big_decimal_from_sat_unsigned, UtxoTxBuilder}; use crate::utxo::{qtum, utxo_common, Address, GetUtxoListOps, UtxoCommonOps}; use crate::utxo::{PrivKeyPolicyNotAllowed, UTXO_LOCK}; use crate::{DelegationError, DelegationFut, DelegationResult, MarketCoinOps, StakingInfos, StakingInfosError, - StakingInfosFut, StakingInfosResult, TransactionDetails, TransactionType}; + StakingInfosFut, StakingInfosResult, TransactionData, TransactionDetails, TransactionType}; use bitcrypto::dhash256; use common::now_sec; use derive_more::Display; @@ -186,7 +186,7 @@ impl QtumCoin { .map(|padded_staker_address_hex| padded_staker_address_hex.trim_start_matches('0')) }) { let hash = H160::from_str(raw).map_to_mm(|e| StakingInfosError::Internal(e.to_string()))?; - let address = self.utxo_address_from_contract_addr(hash); + let address = self.utxo_addr_from_contract_addr(hash); Ok(Some(address.to_string())) } else { Ok(None) @@ -290,16 +290,7 @@ impl QtumCoin { DelegationError::from_generate_tx_error(gen_tx_error, self.ticker().to_string(), utxo.decimals) })?; - let prev_script = self - .script_for_address(&my_address) - .map_err(|e| DelegationError::InternalError(e.to_string()))?; - let signed = sign_tx( - unsigned, - key_pair, - prev_script, - utxo.conf.signature_version, - utxo.conf.fork_id, - )?; + let signed = sign_tx(unsigned, key_pair, utxo.conf.signature_version, utxo.conf.fork_id)?; let miner_fee = data.fee_amount + data.unused_change; let generated_tx = GenerateQrc20TxResult { @@ -324,8 +315,10 @@ impl QtumCoin { let my_balance_change = &received_by_me - &spent_by_me; Ok(TransactionDetails { - tx_hex: serialize(&generated_tx.signed).into(), - tx_hash: generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + tx: TransactionData::new_signed( + serialize(&generated_tx.signed).into(), + generated_tx.signed.hash().reversed().to_vec().to_tx_hash(), + ), from: vec![my_address_string], to: vec![to_address], total_amount: qtum_amount, diff --git a/mm2src/coins/utxo/rpc_clients.rs b/mm2src/coins/utxo/rpc_clients.rs index 954b88c52f..e8f86bc8e0 100644 --- a/mm2src/coins/utxo/rpc_clients.rs +++ b/mm2src/coins/utxo/rpc_clients.rs @@ -2,15 +2,15 @@ #![cfg_attr(target_arch = "wasm32", allow(dead_code))] use crate::utxo::utxo_block_header_storage::BlockHeaderStorage; -use crate::utxo::{output_script, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, GetTxError, - GetTxHeightError, ScripthashNotification}; +use crate::utxo::{output_script, output_script_p2pk, sat_from_big_decimal, GetBlockHeaderError, GetConfirmedTxError, + GetTxError, GetTxHeightError, NumConversResult, ScripthashNotification}; use crate::{big_decimal_from_sat_unsigned, MyAddressError, NumConversError, RpcTransportEventHandler, RpcTransportEventHandlerShared}; use async_trait::async_trait; use chain::{BlockHeader, BlockHeaderBits, BlockHeaderNonce, OutPoint, Transaction as UtxoTx, TransactionInput, TxHashAlgo}; use common::custom_futures::{select_ok_sequential, timeout::FutureTimerExt}; -use common::custom_iter::{CollectInto, TryIntoGroupMap}; +use common::custom_iter::TryIntoGroupMap; use common::executor::{abortable_queue, abortable_queue::AbortableQueue, AbortableSystem, SpawnFuture, Timer}; use common::jsonrpc_client::{JsonRpcBatchClient, JsonRpcBatchResponse, JsonRpcClient, JsonRpcError, JsonRpcErrorType, JsonRpcId, JsonRpcMultiClient, JsonRpcRemoteAddr, JsonRpcRequest, JsonRpcRequestEnum, @@ -37,6 +37,7 @@ use mm2_number::{BigDecimal, BigInt, MmNumber}; use mm2_rpc::data::legacy::ElectrumProtocol; #[cfg(test)] use mocktopus::macros::*; use rpc::v1::types::{Bytes as BytesJson, Transaction as RpcTransaction, H256 as H256Json}; +use script::Script; use serde_json::{self as json, Value as Json}; use serialization::{deserialize, serialize, serialize_with_flags, CoinVariant, CompactInteger, Reader, SERIALIZE_TRANSACTION_WITNESS}; @@ -256,19 +257,34 @@ pub struct UnspentInfo { /// The block height transaction mined in. /// Note None if the transaction is not mined yet. pub height: Option, + /// The script pubkey of the UTXO + pub script: Script, } -impl From for UnspentInfo { - fn from(electrum: ElectrumUnspent) -> UnspentInfo { +impl UnspentInfo { + fn from_electrum(unspent: ElectrumUnspent, script: Script) -> UnspentInfo { UnspentInfo { outpoint: OutPoint { - hash: electrum.tx_hash.reversed().into(), - index: electrum.tx_pos, + hash: unspent.tx_hash.reversed().into(), + index: unspent.tx_pos, }, - value: electrum.value, - height: electrum.height, + value: unspent.value, + height: unspent.height, + script, } } + + fn from_native(unspent: NativeUnspent, decimals: u8, height: Option) -> NumConversResult { + Ok(UnspentInfo { + outpoint: OutPoint { + hash: unspent.txid.reversed().into(), + index: unspent.vout, + }, + value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, + height, + script: unspent.script_pub_key.0.into(), + }) + } } #[derive(Debug, PartialEq)] @@ -758,20 +774,10 @@ impl UtxoRpcClientOps for NativeClient { .list_unspent_impl(0, std::i32::MAX, vec![address.to_string()]) .map_to_mm_fut(UtxoRpcError::from) .and_then(move |unspents| { - let unspents: UtxoRpcResult> = unspents - .into_iter() - .map(|unspent| { - Ok(UnspentInfo { - outpoint: OutPoint { - hash: unspent.txid.reversed().into(), - index: unspent.vout, - }, - value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, - height: None, - }) - }) - .collect(); unspents + .into_iter() + .map(|unspent| Ok(UnspentInfo::from_native(unspent, decimals, None)?)) + .collect::>() }); Box::new(fut) } @@ -799,14 +805,7 @@ impl UtxoRpcClientOps for NativeClient { UtxoRpcError::InvalidResponse(format!("Unexpected address '{}'", unspent.address)) })? .clone(); - let unspent_info = UnspentInfo { - outpoint: OutPoint { - hash: unspent.txid.reversed().into(), - index: unspent.vout, - }, - value: sat_from_big_decimal(&unspent.amount.to_decimal(), decimals)?, - height: None, - }; + let unspent_info = UnspentInfo::from_native(unspent, decimals, None)?; Ok((orig_address, unspent_info)) }) // Collect `(Address, UnspentInfo)` items into `HashMap>` grouped by the addresses. @@ -2172,7 +2171,7 @@ impl ElectrumClient { Ok(headers) => headers, Err(e) => return MmError::err(UtxoRpcError::InvalidResponse(format!("{:?}", e))), }; - let mut block_registry: HashMap = HashMap::new(); + let mut block_registry: HashMap = HashMap::with_capacity(block_headers.len()); let mut starting_height = from_height; for block_header in &block_headers { block_registry.insert(starting_height, block_header.clone()); @@ -2237,48 +2236,67 @@ impl ElectrumClient { #[cfg_attr(test, mockable)] impl UtxoRpcClientOps for ElectrumClient { fn list_unspent(&self, address: &Address, _decimals: u8) -> UtxoRpcFut> { - let script = try_f!(output_script(address)); - let script_hash = electrum_script_hash(&script); - Box::new( - self.scripthash_list_unspent(&hex::encode(script_hash)) - .map_to_mm_fut(UtxoRpcError::from) - .map(move |unspents| { + let mut output_scripts = vec![try_f!(output_script(address))]; + + // If the plain pubkey is available, fetch the UTXOs found in P2PK outputs as well (if any). + if let Some(pubkey) = address.pubkey() { + let p2pk_output_script = output_script_p2pk(pubkey); + output_scripts.push(p2pk_output_script); + } + + let this = self.clone(); + let fut = async move { + let hashes = output_scripts + .iter() + .map(|s| hex::encode(electrum_script_hash(s))) + .collect(); + let unspents = this.scripthash_list_unspent_batch(hashes).compat().await?; + + let unspents = unspents + .into_iter() + .zip(output_scripts) + .flat_map(|(unspents, output_script)| { unspents - .iter() - .map(|unspent| UnspentInfo { - outpoint: OutPoint { - hash: unspent.tx_hash.reversed().into(), - index: unspent.tx_pos, - }, - value: unspent.value, - height: unspent.height, - }) - .collect() - }), - ) + .into_iter() + .map(move |unspent| UnspentInfo::from_electrum(unspent, output_script.clone())) + }) + .collect(); + Ok(unspents) + }; + + Box::new(fut.boxed().compat()) } fn list_unspent_group(&self, addresses: Vec
, _decimals: u8) -> UtxoRpcFut { - let script_hashes = try_f!(addresses + let output_scripts = try_f!(addresses .iter() - .map(|addr| { - let script = output_script(addr)?; - let script_hash = electrum_script_hash(&script); - Ok(hex::encode(script_hash)) - }) + .map(output_script) .collect::, keys::Error>>()); let this = self.clone(); let fut = async move { - let unspents = this.scripthash_list_unspent_batch(script_hashes).compat().await?; + let hashes = output_scripts + .iter() + .map(|s| hex::encode(electrum_script_hash(s))) + .collect(); + let unspents = this.scripthash_list_unspent_batch(hashes).compat().await?; + + let unspents: Vec> = unspents + .into_iter() + .zip(output_scripts) + .map(|(unspents, output_script)| { + unspents + .into_iter() + .map(|unspent| UnspentInfo::from_electrum(unspent, output_script.clone())) + .collect() + }) + .collect(); let unspent_map = addresses .into_iter() // `scripthash_list_unspent_batch` returns `ScriptHashUnspents` elements in the same order in which they were requested. // So we can zip `addresses` and `unspents` into one iterator. .zip(unspents) - // Map `(Address, Vec)` pairs into `(Address, Vec)`. - .map(|(address, electrum_unspents)| (address, electrum_unspents.collect_into())) .collect(); Ok(unspent_map) }; @@ -2346,12 +2364,26 @@ impl UtxoRpcClientOps for ElectrumClient { rpc_req!(self, "blockchain.scripthash.get_balance").into(), JsonRpcErrorType::Internal(err.to_string()) ))); - let hash = electrum_script_hash(&output_script); - let hash_str = hex::encode(hash); - Box::new( - self.scripthash_get_balance(&hash_str) - .map(move |electrum_balance| electrum_balance.to_big_decimal(decimals)), - ) + let mut hashes = vec![hex::encode(electrum_script_hash(&output_script))]; + + // If the plain pubkey is available, fetch the balance found in P2PK output as well (if any). + if let Some(pubkey) = address.pubkey() { + let p2pk_output_script = output_script_p2pk(pubkey); + hashes.push(hex::encode(electrum_script_hash(&p2pk_output_script))); + } + + let this = self.clone(); + let fut = async move { + Ok(this + .scripthash_get_balances(hashes) + .compat() + .await? + .into_iter() + .fold(BigDecimal::from(0), |sum, electrum_balance| { + sum + electrum_balance.to_big_decimal(decimals) + })) + }; + Box::new(fut.boxed().compat()) } fn display_balances(&self, addresses: Vec
, decimals: u8) -> UtxoRpcFut> { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 4138d3b7a8..c559449c22 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -20,8 +20,8 @@ use crate::{BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, CoinFutSpawner, C RawTransactionResult, RefundError, RefundPaymentArgs, RefundResult, SearchForSwapTxSpendInput, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, SwapTxTypeWithSecretHash, TakerSwapMakerCoin, TradeFee, TradePreimageError, - TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionDetails, TransactionEnum, - TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, + TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, + TransactionEnum, TransactionErr, TransactionFut, TransactionResult, TxFeeDetails, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateInstructionsErr, ValidateOtherPubKeyErr, ValidatePaymentInput, ValidateWatcherSpendInput, VerificationError, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WatcherReward, WatcherRewardError, @@ -455,10 +455,11 @@ impl SlpToken { bch_unspent: UnspentInfo { outpoint: OutPoint { hash: tx.hash(), - index: 1, + index: SLP_SWAP_VOUT as u32, }, value: 0, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount: slp_satoshis, }; @@ -555,8 +556,9 @@ impl SlpToken { hash: tx.hash(), index: SLP_SWAP_VOUT as u32, }, - value: tx.outputs[1].value, + value: tx.outputs[SLP_SWAP_VOUT].value, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount, }; @@ -606,8 +608,9 @@ impl SlpToken { hash: tx.hash(), index: SLP_SWAP_VOUT as u32, }, - value: tx.outputs[1].value, + value: tx.outputs[SLP_SWAP_VOUT].value, height: None, + script: tx.outputs[SLP_SWAP_VOUT].script_pubkey.clone().into(), }, slp_amount, }; @@ -677,7 +680,6 @@ impl SlpToken { &unsigned, i, my_key_pair, - my_script_pubkey.clone(), self.platform_coin.as_ref().conf.signature_version, self.platform_coin.as_ref().conf.fork_id, ) @@ -1214,7 +1216,7 @@ impl MarketCoinOps for SlpToken { #[async_trait] impl SwapOps for SlpToken { - fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8]) -> TransactionFut { + fn send_taker_fee(&self, fee_addr: &[u8], dex_fee: DexFee, _uuid: &[u8], _expire_at: u64) -> TransactionFut { let coin = self.clone(); let fee_pubkey = try_tx_fus!(Public::from_slice(fee_addr)); let script_pubkey = ScriptBuilder::build_p2pkh(&fee_pubkey.address_hash().into()).into(); @@ -1622,12 +1624,6 @@ impl MmCoin for SlpToken { )); } - let my_address = coin - .platform_coin - .as_ref() - .derivation_method - .single_addr_or_err() - .await?; let key_pair = coin.platform_coin.as_ref().priv_key_policy.activated_key_or_err()?; let address = CashAddress::decode(&req.to).map_to_mm(WithdrawError::InvalidAddress)?; @@ -1694,14 +1690,9 @@ impl MmCoin for SlpToken { WithdrawError::from_generate_tx_error(gen_tx_error, coin.platform_ticker().into(), platform_decimals) })?; - let prev_script = coin - .platform_coin - .script_for_address(&my_address) - .map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; let signed = sign_tx( unsigned, key_pair, - prev_script, coin.platform_conf().signature_version, coin.platform_conf().fork_id, )?; @@ -1722,9 +1713,8 @@ impl MmCoin for SlpToken { let tx_hash: BytesJson = signed.hash().reversed().take().to_vec().into(); let details = TransactionDetails { - tx_hex: serialize(&signed).into(), internal_id: tx_hash.clone(), - tx_hash: tx_hash.to_tx_hash(), + tx: TransactionData::new_signed(serialize(&signed).into(), tx_hash.to_tx_hash()), from: vec![my_address_string], to: vec![to_address], total_amount, @@ -1795,6 +1785,7 @@ impl MmCoin for SlpToken { &self, value: TradePreimageValue, stage: FeeApproxStage, + _include_refund_fee: bool, // refund fee is taken from swap output ) -> TradePreimageResult { let slp_amount = match value { TradePreimageValue::Exact(decimal) | TradePreimageValue::UpperBound(decimal) => { diff --git a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs index edf34ebb65..0b07d1596c 100644 --- a/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs +++ b/mm2src/coins/utxo/utxo_builder/utxo_coin_builder.rs @@ -171,12 +171,11 @@ pub trait UtxoFieldsWithIguanaSecretBuilder: UtxoCoinBuilderCommonOps { let addr_format = self.address_format()?; let my_address = AddressBuilder::new( addr_format, - AddressHashEnum::AddressHash(key_pair.public().address_hash()), conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh() + .as_pkh_from_pk(*key_pair.public()) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; let derivation_method = DerivationMethod::SingleAddress(my_address); @@ -276,12 +275,11 @@ where let addr_format = builder.address_format()?; let my_address = AddressBuilder::new( addr_format, - AddressHashEnum::AddressHash(key_pair.public().address_hash()), conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), ) - .as_pkh() + .as_pkh_from_pk(*key_pair.public()) .build() .map_to_mm(UtxoCoinBuildError::Internal)?; diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 6e5ee35510..be54d53fe1 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -19,15 +19,15 @@ use crate::{scan_for_new_addresses_impl, CanRefundHtlc, CoinBalance, CoinWithDer SearchForSwapTxSpendInput, SendMakerPaymentArgs, SendMakerPaymentSpendPreimageInput, SendPaymentArgs, SendTakerFundingArgs, SignRawTransactionEnum, SignRawTransactionRequest, SignUtxoTransactionParams, SignatureError, SignatureResult, SpendMakerPaymentArgs, SpendPaymentArgs, SwapOps, - SwapTxTypeWithSecretHash, TradePreimageValue, TransactionFut, TransactionResult, TxFeeDetails, TxGenError, - TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, ValidateOtherPubKeyErr, ValidatePaymentFut, - ValidatePaymentInput, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs, - ValidateTakerFundingSpendPreimageError, ValidateTakerFundingSpendPreimageResult, - ValidateTakerPaymentSpendPreimageError, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationError, VerificationResult, WatcherSearchForSwapTxSpendInput, - WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawResult, WithdrawSenderAddress, - EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, - INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; + SwapTxTypeWithSecretHash, TradePreimageValue, TransactionData, TransactionFut, TransactionResult, + TxFeeDetails, TxGenError, TxMarshalingErr, TxPreimageWithSig, ValidateAddressResult, + ValidateOtherPubKeyErr, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxError, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageError, + ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageError, + ValidateTakerPaymentSpendPreimageResult, ValidateWatcherSpendInput, VerificationError, VerificationResult, + WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, + WithdrawResult, WithdrawSenderAddress, EARLY_CONFIRMATION_ERR_LOG, INVALID_RECEIVER_ERR_LOG, + INVALID_REFUND_TX_ERR_LOG, INVALID_SCRIPT_ERR_LOG, INVALID_SENDER_ERR_LOG, OLD_TRANSACTION_ERR_LOG}; use crate::{MmCoinEnum, WatcherReward, WatcherRewardError}; use base64::engine::general_purpose::STANDARD; use base64::Engine; @@ -310,19 +310,18 @@ pub fn addresses_from_script(coin: &T, script: &Script) -> Res let (addr_format, build_option) = match dst.kind { AddressScriptType::P2PKH => ( coin.addr_format_for_standard_scripts(), - AddressBuilderOption::BuildAsPubkeyHash, + AddressBuilderOption::PubkeyHash(dst.hash), ), AddressScriptType::P2SH => ( coin.addr_format_for_standard_scripts(), - AddressBuilderOption::BuildAsScriptHash, + AddressBuilderOption::ScriptHash(dst.hash), ), - AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::BuildAsPubkeyHash), - AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::BuildAsScriptHash), + AddressScriptType::P2WPKH => (UtxoAddressFormat::Segwit, AddressBuilderOption::PubkeyHash(dst.hash)), + AddressScriptType::P2WSH => (UtxoAddressFormat::Segwit, AddressBuilderOption::ScriptHash(dst.hash)), }; AddressBuilder::new( addr_format, - dst.hash, conf.checksum_type, conf.address_prefixes.clone(), conf.bech32_hrp.clone(), @@ -514,9 +513,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .inputs .extend(inputs.into_iter().map(|input| UnsignedTransactionInput { previous_output: input.outpoint, + prev_script: input.script, sequence: SEQUENCE_FINAL, amount: input.value, - witness: Vec::new(), })); self } @@ -589,9 +588,9 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { } if let Some(min_relay) = self.min_relay_fee { if self.tx_fee < min_relay { - outputs_plus_fee -= self.tx_fee; - outputs_plus_fee += min_relay; - self.tx_fee = min_relay; + let fee_diff = min_relay - self.tx_fee; + outputs_plus_fee += fee_diff; + self.tx_fee += fee_diff; } } self.sum_inputs >= outputs_plus_fee @@ -680,17 +679,23 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { None }; - for utxo in self.available_inputs.clone() { - self.tx.inputs.push(UnsignedTransactionInput { - previous_output: utxo.outpoint, - sequence: SEQUENCE_FINAL, - amount: utxo.value, - witness: vec![], - }); - self.sum_inputs += utxo.value; + // The function `update_fee_and_check_completeness` checks if the total value of the current inputs + // (added using add_required_inputs or directly) is enough to cover the transaction outputs and fees. + // If it returns `true`, it indicates that no additional inputs are needed from the available inputs, + // and we can skip the loop that adds these additional inputs. + if !self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { + for utxo in self.available_inputs.clone() { + self.tx.inputs.push(UnsignedTransactionInput { + previous_output: utxo.outpoint, + prev_script: utxo.script, + sequence: SEQUENCE_FINAL, + amount: utxo.value, + }); + self.sum_inputs += utxo.value; - if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { - break; + if self.update_fee_and_check_completeness(from.addr_format(), &actual_tx_fee) { + break; + } } } @@ -769,12 +774,13 @@ impl<'a, T: AsRef + UtxoTxGenerationOps> UtxoTxBuilder<'a, T> { .find(|input| input.previous_output == utxo.outpoint) { input.amount = utxo.value; + input.prev_script = utxo.script; } else { self.tx.inputs.push(UnsignedTransactionInput { previous_output: utxo.outpoint, + prev_script: utxo.script, sequence: SEQUENCE_FINAL, amount: utxo.value, - witness: vec![], }); } } @@ -913,8 +919,8 @@ async fn p2sh_spending_tx_preimage( hash: prev_tx.hash(), index: DEFAULT_SWAP_VOUT as u32, }, + prev_script: Vec::new().into(), amount, - witness: Vec::new(), }], outputs, expiry_height: 0, @@ -1212,12 +1218,11 @@ pub async fn sign_and_send_taker_funding_spend( ); let payment_address = AddressBuilder::new( UtxoAddressFormat::Standard, - AddressHashEnum::AddressHash(dhash160(&payment_redeem_script)), coin.as_ref().conf.checksum_type, coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) - .as_sh() + .as_sh(dhash160(&payment_redeem_script).into()) .build() .map_err(TransactionErr::Plain)?; let payment_address_str = payment_address.to_string(); @@ -2443,12 +2448,11 @@ pub fn check_if_my_payment_sent( UtxoRpcClientEnum::Native(client) => { let target_addr = AddressBuilder::new( coin.addr_format_for_standard_scripts(), - hash.into(), coin.as_ref().conf.checksum_type, coin.as_ref().conf.address_prefixes.clone(), coin.as_ref().conf.bech32_hrp.clone(), ) - .as_sh() + .as_sh(hash.into()) .build()?; let target_addr = target_addr.to_string(); let is_imported = try_s!(client.is_address_imported(&target_addr).await); @@ -2687,26 +2691,24 @@ pub fn send_raw_tx_bytes( } /// Helper to load unspent outputs from cache or rpc -/// also returns first previous scriptpubkey async fn get_unspents_for_inputs( coin: &UtxoCoinFields, inputs: &Vec, -) -> Result<(Option