From 14e695dd012fe13e8728951d50cfbcb1e7fb24c6 Mon Sep 17 00:00:00 2001 From: dimxy Date: Mon, 16 Sep 2024 21:15:15 +0500 Subject: [PATCH 01/24] change wei to big decimal macro to fn (for easier using in other crates) --- mm2src/coins/eth.rs | 26 ++++------ mm2src/coins/eth/eip1559_gas_fee.rs | 52 +++++++++---------- .../coins/rpc_command/get_estimated_fees.rs | 18 +++---- 3 files changed, 44 insertions(+), 52 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index a6cd170627..f0c08ec9ac 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -310,20 +310,6 @@ pub type Web3RpcFut = Box> pub type Web3RpcResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; -#[macro_export] -macro_rules! wei_from_gwei_decimal { - ($big_decimal: expr) => { - $crate::eth::wei_from_big_decimal($big_decimal, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - -#[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, @@ -368,11 +354,11 @@ impl TryFrom for PayForGasOption { fn try_from(param: PayForGasParams) -> Result { match param { PayForGasParams::Legacy(legacy) => Ok(Self::Legacy(LegacyGasPrice { - gas_price: wei_from_gwei_decimal!(&legacy.gas_price)?, + 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)?, + 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)?, })), } } @@ -5963,6 +5949,12 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu U256::from_dec_str(&amount).map_to_mm(|e| NumConversError::new(format!("{:?}", e))) } +pub fn wei_from_gwei_decimal(bigdec: &BigDecimal) -> NumConversResult { + wei_from_big_decimal(bigdec, ETH_GWEI_DECIMALS) +} + +pub fn wei_to_gwei_decimal(wei: U256) -> NumConversResult { u256_to_big_decimal(wei, ETH_GWEI_DECIMALS) } + impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs index 4d33781f39..32ac186169 100644 --- a/mm2src/coins/eth/eip1559_gas_fee.rs +++ b/mm2src/coins/eth/eip1559_gas_fee.rs @@ -1,8 +1,8 @@ //! 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 super::{wei_from_gwei_decimal, wei_to_gwei_decimal, Web3RpcError, Web3RpcResult}; +use crate::{EthCoin, NumConversError}; use ethereum_types::U256; use mm2_err_handle::mm_error::MmError; use mm2_err_handle::or_mm_error::OrMmError; @@ -104,24 +104,24 @@ impl TryFrom for FeePerGasEstimated { fn try_from(infura_fees: InfuraFeePerGas) -> Result { Ok(Self { - base_fee: wei_from_gwei_decimal!(&infura_fees.estimated_base_fee)?, + 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)?, + 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 + 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)?, + 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), }, @@ -143,33 +143,33 @@ impl TryFrom for FeePerGasEstimated { return Ok(FeePerGasEstimated::default()); } Ok(Self { - base_fee: wei_from_gwei_decimal!(&block_prices.block_prices[0].base_fee_per_gas)?, + 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_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 + 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_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 + 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_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 + 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, @@ -260,7 +260,7 @@ impl FeePerGasSimpleEstimator { 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)); + 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 = @@ -273,7 +273,7 @@ impl FeePerGasSimpleEstimator { Ok(FeePerGasLevel { max_priority_fee_per_gas, - max_fee_per_gas: wei_from_gwei_decimal!(&max_fee_per_gas_dec)?, + 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, @@ -290,7 +290,7 @@ impl FeePerGasSimpleEstimator { .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)); + 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) diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs index b62e572756..811ecb448e 100644 --- a/mm2src/coins/rpc_command/get_estimated_fees.rs +++ b/mm2src/coins/rpc_command/get_estimated_fees.rs @@ -1,7 +1,7 @@ //! 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 crate::eth::{wei_to_gwei_decimal, EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; +use crate::{lp_coinfind_or_err, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; use common::executor::{spawn_abortable, Timer}; use common::log::debug; use common::{HttpStatusCode, StatusCode}; @@ -66,22 +66,22 @@ impl TryFrom for FeePerGasEstimatedExt { fn try_from(fees: FeePerGasEstimated) -> Result { Ok(Self { - base_fee: wei_to_gwei_decimal!(fees.base_fee)?, + 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)?, + 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)?, + 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)?, + 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, }, From aad3739b9f5c85742b652eb7c09bcd60b53e1003 Mon Sep 17 00:00:00 2001 From: dimxy Date: Mon, 16 Sep 2024 21:17:01 +0500 Subject: [PATCH 02/24] add chain_id from conf fn --- mm2src/coins/eth.rs | 2 ++ mm2src/coins/eth/eth_rpc.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index f0c08ec9ac..b5842bc66f 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -852,6 +852,8 @@ impl EthCoinImpl { let guard = self.erc20_tokens_infos.lock().unwrap(); (*guard).clone() } + + pub fn chain_id(&self) -> u64 { self.chain_id } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 922e219fbd..3cd4010809 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -242,7 +242,7 @@ impl EthCoin { } /// Get chain id - pub(crate) async fn chain_id(&self) -> Result { + pub(crate) async fn network_chain_id(&self) -> Result { self.try_rpc_send("eth_chainId", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) From 5df753b3b0844d7403f3ee72fd3bcafea5d72d43 Mon Sep 17 00:00:00 2001 From: dimxy Date: Mon, 16 Sep 2024 22:38:35 +0500 Subject: [PATCH 03/24] add code to connect 1inch api, add rpcs to use classic swap api --- Cargo.lock | 20 + Cargo.toml | 1 + mm2src/coins/eth.rs | 4 +- mm2src/coins/lp_coins.rs | 90 +++- mm2src/common/common.rs | 7 + mm2src/mm2_main/Cargo.toml | 2 + mm2src/mm2_main/src/ext_api.rs | 4 + mm2src/mm2_main/src/ext_api/one_inch.rs | 4 + .../mm2_main/src/ext_api/one_inch/errors.rs | 83 ++++ mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 148 +++++++ mm2src/mm2_main/src/ext_api/one_inch/types.rs | 178 ++++++++ mm2src/mm2_main/src/mm2.rs | 1 + .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 13 +- .../tests/docker_tests/docker_tests_inner.rs | 81 ++++ mm2src/trading_api/Cargo.toml | 25 ++ mm2src/trading_api/src/lib.rs | 1 + mm2src/trading_api/src/one_inch_api.rs | 4 + mm2src/trading_api/src/one_inch_api/client.rs | 150 +++++++ mm2src/trading_api/src/one_inch_api/errors.rs | 154 +++++++ mm2src/trading_api/src/one_inch_api/types.rs | 416 ++++++++++++++++++ 20 files changed, 1377 insertions(+), 9 deletions(-) create mode 100644 mm2src/mm2_main/src/ext_api.rs create mode 100644 mm2src/mm2_main/src/ext_api/one_inch.rs create mode 100644 mm2src/mm2_main/src/ext_api/one_inch/errors.rs create mode 100644 mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs create mode 100644 mm2src/mm2_main/src/ext_api/one_inch/types.rs create mode 100644 mm2src/trading_api/Cargo.toml create mode 100644 mm2src/trading_api/src/lib.rs create mode 100644 mm2src/trading_api/src/one_inch_api.rs create mode 100644 mm2src/trading_api/src/one_inch_api/client.rs create mode 100644 mm2src/trading_api/src/one_inch_api/errors.rs create mode 100644 mm2src/trading_api/src/one_inch_api/types.rs diff --git a/Cargo.lock b/Cargo.lock index 431959e5c5..ec7389f255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4600,6 +4600,7 @@ dependencies = [ "num-traits", "parity-util-mem", "parking_lot 0.12.0", + "primitive-types", "primitives", "prost", "prost-build", @@ -4629,6 +4630,7 @@ dependencies = [ "spv_validation", "testcontainers", "tokio", + "trading_api", "trie-db", "trie-root 0.16.0", "url", @@ -8693,6 +8695,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trading_api" +version = "0.1.0" +dependencies = [ + "common", + "enum_derives", + "ethereum-types", + "lazy_static", + "mm2_core", + "mm2_err_handle", + "mm2_net", + "mm2_number", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "trezor" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index deec6b843f..32d349dcb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "mm2src/proxy_signature", "mm2src/rpc_task", "mm2src/trezor", + "mm2src/trading_api", ] exclude = [ diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index b5842bc66f..72bd1ce1b4 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -4527,7 +4527,7 @@ impl EthCoin { self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } - fn allowance(&self, spender: Address) -> Web3RpcFut { + pub(crate) fn allowance(&self, spender: Address) -> Web3RpcFut { let coin = self.clone(); let fut = async move { match coin.coin_type { @@ -4592,7 +4592,7 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - fn approve(&self, spender: Address, amount: U256) -> EthTxFut { + pub(crate) fn approve(&self, spender: Address, amount: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 1af6027e30..ac8426b73f 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -41,6 +41,7 @@ #[macro_use] extern crate serde_json; #[macro_use] extern crate ser_error_derive; +use crate::eth::Web3RpcError; use async_trait::async_trait; use base58::FromBase58Error; use bip32::ExtendedPrivateKey; @@ -241,10 +242,9 @@ pub mod coins_tests; pub mod eth; use eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; -use eth::GetValidEthWithdrawAddError; -use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, - GetEthAddressError, SignedEthTx}; -use ethereum_types::U256; +use eth::{eth_coin_from_conf_and_request, get_eth_address, u256_to_big_decimal, wei_from_big_decimal, EthCoin, + EthGasDetailsErr, EthTxFeeDetails, GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; +use ethereum_types::{Address as EthAddress, U256}; pub mod hd_wallet; use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, @@ -713,6 +713,10 @@ impl TransactionErr { } } +impl std::fmt::Display for TransactionErr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.get_plain_text_format()) } +} + #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { Spent(TransactionEnum), @@ -5751,3 +5755,81 @@ pub mod for_tests { } } } + +#[derive(Debug, Deserialize)] +pub struct Erc20ApproveRequest { + coin: String, + spender: EthAddress, + amount: BigDecimal, +} + +#[derive(Debug, Deserialize)] +pub struct Erc20AllowanceRequest { + coin: String, + spender: EthAddress, +} + +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum Erc20CallError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Coin not supported {}", coin)] + CoinNotSupported { coin: String }, + #[from_stringify("NumConversError")] + #[display(fmt = "Invalid param: {}", _0)] + InvalidParam(String), + #[from_stringify("TransactionErr")] + #[display(fmt = "Transaction error {}", _0)] + TransactionError(String), + #[from_stringify("Web3RpcError")] + #[display(fmt = "Web3 RPC error {}", _0)] + Web3RpcError(String), +} + +impl HttpStatusCode for Erc20CallError { + fn status_code(&self) -> StatusCode { + match self { + Erc20CallError::NoSuchCoin { .. } + | Erc20CallError::CoinNotSupported { .. } + | Erc20CallError::InvalidParam(_) + | Erc20CallError::TransactionError(_) + | Erc20CallError::Web3RpcError(_) => StatusCode::BAD_REQUEST, + } + } +} + +type Erc20AllowanceResult = MmResult; +type Erc20ApproveResult = MmResult; + +/// Call allowance for ERC20 tokens +/// Returns BigDecimal value +pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> Erc20AllowanceResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin) + .await + .mm_err(|_| Erc20CallError::NoSuchCoin { coin: req.coin.clone() })?; + match coin { + MmCoinEnum::EthCoin(eth_coin) => { + let wei = eth_coin.allowance(req.spender).compat().await?; + let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; + Ok(amount) + }, + _ => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), + } +} + +/// Call approve for ERC20 coins +/// Returns signed transaction to send to the chain +pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> Erc20ApproveResult { + let coin = lp_coinfind_or_err(&ctx, &req.coin) + .await + .mm_err(|_| Erc20CallError::NoSuchCoin { coin: req.coin.clone() })?; + match coin { + MmCoinEnum::EthCoin(eth_coin) => { + let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; + let tx = eth_coin.approve(req.spender, amount).compat().await?; + Ok(tx.tx_hash_as_bytes()) + }, + _ => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), + } +} diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index c98fe610d2..0588c01385 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -1128,6 +1128,13 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +#[macro_export] +macro_rules! str_strip_0x { + ($s: expr) => { + $s.strip_prefix("0x").unwrap_or($s) + }; +} + #[test] fn test_http_uri_to_ws_address() { let uri = "https://cosmos-rpc.polkachu.com".parse::().unwrap(); diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 60c3e9aa62..ace4369c6e 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -70,10 +70,12 @@ mm2_net = { path = "../mm2_net", features = ["event-stream", "p2p"] } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} mm2_state_machine = { path = "../mm2_state_machine" } +trading_api = { path = "../trading_api" } num-traits = "0.2" parity-util-mem = "0.11" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } +primitive-types = "0.11.1" prost = "0.11" rand = { version = "0.7", features = ["std", "small_rng"] } rand6 = { version = "0.6", package = "rand" } diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs new file mode 100644 index 0000000000..fbeed01e06 --- /dev/null +++ b/mm2src/mm2_main/src/ext_api.rs @@ -0,0 +1,4 @@ +/// RPCs to integrate with external third party trading API + +/// RPCs to access 1inch eth-like swap API +pub mod one_inch; diff --git a/mm2src/mm2_main/src/ext_api/one_inch.rs b/mm2src/mm2_main/src/ext_api/one_inch.rs new file mode 100644 index 0000000000..21ca1216e4 --- /dev/null +++ b/mm2src/mm2_main/src/ext_api/one_inch.rs @@ -0,0 +1,4 @@ +pub mod errors; +pub mod rpcs; +/// RPC imlementation to integrate 1inch api +pub mod types; diff --git a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs new file mode 100644 index 0000000000..23b4fa552a --- /dev/null +++ b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs @@ -0,0 +1,83 @@ +use coins::{eth::u256_to_big_decimal, NumConversError}; +use common::{HttpStatusCode, StatusCode}; +use enum_derives::EnumFromStringify; +use mm2_number::BigDecimal; +use ser_error_derive::SerializeErrorType; +use serde::Serialize; +use trading_api::one_inch_api::errors::ApiClientError; + +#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ApiIntegrationRpcError { + #[from_stringify("coins::CoinFindError")] + CoinFindError(String), + #[display(fmt = "EVM token needed")] + CoinTypeError, + #[display(fmt = "NFT not supported")] + NftNotSupported, + #[display(fmt = "Chain not supported")] + ChainNotSupported, + #[from_stringify("coins::UnexpectedDerivationMethod")] + MyAddressError(String), + InvalidParam(String), + #[display(fmt = "allowance not enough for 1inch contract, available: {allowance}, needed: {amount}")] + OneInchAllowanceNotEnough { + allowance: BigDecimal, + amount: BigDecimal, + }, + #[display(fmt = "1inch API error: {}", _0)] + OneInchError(ApiClientError), + ApiDataError(String), +} + +impl HttpStatusCode for ApiIntegrationRpcError { + fn status_code(&self) -> StatusCode { + match self { + ApiIntegrationRpcError::CoinFindError(_) + | ApiIntegrationRpcError::CoinTypeError + | ApiIntegrationRpcError::NftNotSupported + | ApiIntegrationRpcError::ChainNotSupported + | ApiIntegrationRpcError::MyAddressError(_) + | ApiIntegrationRpcError::InvalidParam(_) + | ApiIntegrationRpcError::OneInchAllowanceNotEnough { .. } => StatusCode::BAD_REQUEST, + ApiIntegrationRpcError::OneInchError(_) | ApiIntegrationRpcError::ApiDataError(_) => { + StatusCode::BAD_GATEWAY + }, + } + } +} + +impl ApiIntegrationRpcError { + pub(crate) fn from_api_error(error: ApiClientError, decimals: u8) -> Self { + match error { + ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), + ApiClientError::HttpClientError(_) + | ApiClientError::ParseBodyError(_) + | ApiClientError::GeneralApiError(_) => ApiIntegrationRpcError::OneInchError(error), + ApiClientError::AllowanceNotEnough(nested_err) => ApiIntegrationRpcError::OneInchAllowanceNotEnough { + allowance: u256_to_big_decimal(nested_err.allowance, decimals).unwrap_or_default(), + amount: u256_to_big_decimal(nested_err.amount, decimals).unwrap_or_default(), + }, + } + } +} + +/// Error aggregator for errors of conversion of api returned values +#[derive(Debug, Display, Serialize)] +pub(crate) struct FromApiValueError(String); + +impl From for FromApiValueError { + fn from(err: NumConversError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: primitive_types::Error) -> Self { Self(format!("{:?}", err)) } +} + +impl From for FromApiValueError { + fn from(err: hex::FromHexError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: ethereum_types::FromDecStrErr) -> Self { Self(err.to_string()) } +} diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs new file mode 100644 index 0000000000..c2147b814f --- /dev/null +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -0,0 +1,148 @@ +use super::errors::ApiIntegrationRpcError; +use super::types::{AggregationContractRequest, ClassicSwapCreateRequest, ClassicSwapQuoteRequest, ClassicSwapResponse}; +use coins::eth::{display_eth_address, wei_from_big_decimal, EthCoin, EthCoinType}; +use coins::{lp_coinfind_or_err, CoinWithDerivationMethod, MmCoin, MmCoinEnum}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use trading_api::one_inch_api::client::ApiClient; +use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams}; + +// Default swap optional params: +const INCLUDE_GAS: bool = true; +const INCLUDE_PROTOCOLS: bool = true; +const INCLUDE_TOKENS_INFO: bool = true; + +/// "1inch_v6_0_classic_swap_contract" rpc impl +/// used to get contract address (for e.g. to approve funds) +pub async fn one_inch_v6_0_classic_swap_contract_rpc( + _ctx: MmArc, + _req: AggregationContractRequest, +) -> MmResult { + Ok(ApiClient::classic_swap_contract().to_owned()) +} + +/// "1inch_classic_swap_quote" rpc impl +pub async fn one_inch_v6_0_classic_swap_quote_rpc( + ctx: MmArc, + req: ClassicSwapQuoteRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + api_supports_coin(&base)?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + let sell_amount = wei_from_big_decimal(&req.amount, base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let query_params = ClassicSwapQuoteParams::new(base_contract, rel_contract, sell_amount.to_string()) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(if let Some(include_tokens_info) = req.include_tokens_info { + Some(include_tokens_info) + } else { + Some(INCLUDE_TOKENS_INFO) + }) + .with_include_protocols(if let Some(include_protocols) = req.include_protocols { + Some(include_protocols) + } else { + Some(INCLUDE_PROTOCOLS) + }) + .with_include_gas(if let Some(include_gas) = req.include_gas { + Some(include_gas) + } else { + Some(INCLUDE_GAS) + }) + .with_connector_tokens(req.connector_tokens) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; + let quote = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))? + .get_classic_swap_quote(base.chain_id(), query_params) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_value(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_classic_swap_create" rpc impl +pub async fn one_inch_v6_0_classic_swap_create_rpc( + ctx: MmArc, + req: ClassicSwapCreateRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + api_supports_coin(&base)?; + let (_, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + + let sell_amount = wei_from_big_decimal(&req.amount, base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let single_address = base.derivation_method().single_addr_or_err().await?; + + let query_params = ClassicSwapCreateParams::new( + base_contract, + rel_contract, + sell_amount.to_string(), + display_eth_address(&single_address), + req.slippage, + ) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(if let Some(include_tokens_info) = req.include_tokens_info { + Some(include_tokens_info) + } else { + Some(INCLUDE_TOKENS_INFO) + }) + .with_include_protocols(if let Some(include_protocols) = req.include_protocols { + Some(include_protocols) + } else { + Some(INCLUDE_PROTOCOLS) + }) + .with_include_gas(if let Some(include_gas) = req.include_gas { + Some(include_gas) + } else { + Some(INCLUDE_GAS) + }) + .with_connector_tokens(req.connector_tokens) + .with_permit(req.permit) + .with_receiver(req.receiver) + .with_referrer(req.referrer) + .with_disable_estimate(req.disable_estimate) + .with_allow_partial_fill(req.allow_partial_fill) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; + let swap_with_tx = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))? + .get_classic_swap_tx(base.chain_id(), query_params) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_value(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, String), ApiIntegrationRpcError> { + let coin = match lp_coinfind_or_err(ctx, ticker).await? { + MmCoinEnum::EthCoin(coin) => coin, + _ => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), + }; + let contract = match coin.coin_type { + EthCoinType::Eth => ApiClient::eth_special_contract().to_owned(), + EthCoinType::Erc20 { token_addr, .. } => display_eth_address(&token_addr), + EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), + }; + Ok((coin, contract)) +} + +#[allow(clippy::result_large_err)] +fn api_supports_coin(coin: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { + if ApiClient::is_chain_supported(coin.chain_id()) { + Ok(()) + } else { + Err(MmError::new(ApiIntegrationRpcError::ChainNotSupported)) + } +} diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs new file mode 100644 index 0000000000..7ccb107965 --- /dev/null +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -0,0 +1,178 @@ +use crate::ext_api::one_inch::errors::FromApiValueError; +use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; +use ethereum_types::{Address, U256}; +use mm2_err_handle::prelude::*; +use mm2_number::BigDecimal; +use rpc::v1::types::Bytes as BytesJson; +use serde::{Deserialize, Serialize}; +use trading_api::one_inch_api; + +#[derive(Clone, Debug, Deserialize)] +pub struct AggregationContractRequest {} + +#[derive(Clone, Debug, Deserialize)] +pub struct ClassicSwapQuoteRequest { + pub base: String, + pub rel: String, + pub amount: BigDecimal, + + // Optional fields + pub fee: Option, + pub protocols: Option, + pub gas_price: Option, + pub complexity_level: Option, + pub parts: Option, + pub main_route_parts: Option, + pub gas_limit: Option, + + pub include_tokens_info: Option, + pub include_protocols: Option, + pub include_gas: Option, + pub connector_tokens: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ClassicSwapCreateRequest { + pub base: String, + pub rel: String, + pub amount: BigDecimal, + pub slippage: f32, + + // Optional fields + pub fee: Option, + pub protocols: Option, + pub gas_price: Option, + pub complexity_level: Option, + pub parts: Option, + pub main_route_parts: Option, + pub gas_limit: Option, + pub include_tokens_info: Option, + pub include_protocols: Option, + pub include_gas: Option, + pub connector_tokens: Option, + pub permit: Option, + pub receiver: Option, + pub referrer: Option, + + /// Disable gas estimation + pub disable_estimate: Option, + /// Allow the swap to be partially filled + pub allow_partial_fill: Option, +} + +#[derive(Serialize, Debug)] +pub struct ClassicSwapResponse { + pub dst_amount: BigDecimal, + #[serde(skip_serializing_if = "Option::is_none")] + pub src_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocols: Option>>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tx: Option, + pub gas: Option, +} + +impl ClassicSwapResponse { + pub(crate) fn from_api_value( + data: one_inch_api::types::ClassicSwapData, + decimals: u8, + ) -> MmResult { + Ok(Self { + dst_amount: u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?, + src_token: TokenInfo::from_api_value(data.src_token), + dst_token: TokenInfo::from_api_value(data.dst_token), + protocols: ProtocolInfo::from_api_value(data.protocols), + tx: data.tx.map(|tx| TxFields::from_api_value(tx, decimals)).transpose()?, + gas: data.gas, + }) + } +} + +#[derive(Serialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: BytesJson, + pub value: BigDecimal, + /// Estimated gas price in gwei + pub gas_price: BigDecimal, + pub gas: u128, // TODO: in eth EthTxFeeDetails rpc we use u64. Better have identical u128 everywhere +} + +impl TxFields { + pub(crate) fn from_api_value( + tx_fields: trading_api::one_inch_api::types::TxFields, + decimals: u8, + ) -> MmResult { + Ok(Self { + from: tx_fields.from, + to: tx_fields.to, + data: BytesJson::from(hex::decode(str_strip_0x!(tx_fields.data.as_str()))?), + value: u256_to_big_decimal(U256::from_dec_str(&tx_fields.value)?, decimals)?, + gas_price: wei_to_gwei_decimal(U256::from_dec_str(&tx_fields.gas_price)?)?, + gas: tx_fields.gas, + }) + } +} + +#[derive(Serialize, Debug)] +pub struct TokenInfo { + pub address: Address, + pub symbol: String, + pub name: String, + pub decimals: u32, + pub eip2612: bool, + pub is_fot: bool, + pub logo_uri: String, + pub tags: Vec, +} + +impl TokenInfo { + pub(crate) fn from_api_value(opt_info: Option) -> Option { + opt_info.map(|info| Self { + address: info.address, + symbol: info.symbol, + name: info.name, + decimals: info.decimals, + eip2612: info.eip2612, + is_fot: info.is_fot, + logo_uri: info.logo_uri, + tags: info.tags, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct ProtocolInfo { + pub name: String, + pub part: f64, + pub from_token_address: Address, + pub to_token_address: Address, +} + +impl ProtocolInfo { + pub(crate) fn from_api_value( + opt_info: Option>>>, + ) -> Option>>> { + opt_info.map(|v0| { + v0.into_iter() + .map(|v1| { + v1.into_iter() + .map(|v2| { + v2.into_iter() + .map(|info| Self { + name: info.name, + part: info.part, + from_token_address: info.from_token_address, + to_token_address: info.to_token_address, + }) + .collect::>() + }) + .collect::>() + }) + .collect::>() + }) + } +} diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 7dcc5572cb..93ce7bbbca 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -72,6 +72,7 @@ use mm2_err_handle::prelude::*; #[cfg(not(target_arch = "wasm32"))] pub mod database; +pub mod ext_api; pub mod heartbeat_event; pub mod lp_dispatcher; pub mod lp_message_service; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index b9066bf540..7ea63108ff 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,4 +1,6 @@ use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; +use crate::ext_api::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_quote_rpc}; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; #[cfg(target_arch = "wasm32")] use crate::lp_native_dex::init_metamask::{cancel_connect_metamask, connect_metamask, connect_metamask_status}; @@ -35,9 +37,9 @@ use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; -use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, get_swap_transaction_fee_policy, - nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, - verify_message, withdraw}; +use coins::{add_delegation, allowance_rpc, approve_rpc, get_my_address, get_raw_transaction, get_staking_infos, + get_swap_transaction_fee_policy, nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, + sign_raw_transaction, verify_message, withdraw}; #[cfg(all( feature = "enable-solana", not(target_os = "ios"), @@ -165,6 +167,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, active_swaps_rpc).await, "add_delegation" => handle_mmrpc(ctx, request, add_delegation).await, "add_node_to_version_stat" => handle_mmrpc(ctx, request, add_node_to_version_stat).await, + "approve" => handle_mmrpc(ctx, request, approve_rpc).await, + "allowance" => handle_mmrpc(ctx, request, allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, @@ -221,6 +225,9 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, set_swap_transaction_fee_policy).await, "send_asked_data" => handle_mmrpc(ctx, request, send_asked_data_rpc).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, + "1inch_v6_0_classic_swap_contract" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_contract_rpc).await, + "1inch_v6_0_classic_swap_quote" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_quote_rpc).await, + "1inch_v6_0_classic_swap_create" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_create_rpc).await, #[cfg(not(target_arch = "wasm32"))] native_only_methods => match native_only_methods { #[cfg(all(feature = "enable-solana", not(target_os = "ios"), not(target_os = "android")))] diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index 330dec30de..adf0f813d3 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -5520,3 +5520,84 @@ fn test_orderbook_depth() { block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } + +#[test] +fn test_approve_erc20() { + let privkey = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(privkey); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let mm = MarketMakerIt::start( + json!({ + "gui": "nogui", + "netid": 9000, + "dht": "on", // Enable DHT without delay. + "passphrase": format!("0x{}", hex::encode(privkey)), + "coins": coins, + "rpc_password": "pass", + "i_am_seed": true, + }), + "pass".to_string(), + None, + ) + .unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("Alice log path: {}", mm.log_path.display()); + + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let _eth_enable = block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + let _erc20_enable = block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"allowance", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "allowance error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert!( + BigDecimal::from_str(res["result"].as_str().unwrap()).is_ok(), + "allowance result incorrect" + ); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"approve", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + "amount": BigDecimal::from_str("11.0").unwrap(), + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "approve error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert!( + hex::decode(res["result"].as_str().unwrap()).is_ok(), + "approve result incorrect" + ); + block_on(mm.stop()).unwrap(); +} diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml new file mode 100644 index 0000000000..79601880b5 --- /dev/null +++ b/mm2src/trading_api/Cargo.toml @@ -0,0 +1,25 @@ +[package] +# integration with external trading api +name = "trading_api" +version = "0.1.0" +edition = "2018" + +[dependencies] +common = { path = "../common" } +enum_derives = { path = "../derives/enum_derives" } +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_net = { path = "../mm2_net" } +mm2_number = { path = "../mm2_number" } + +# async-trait = "0.1" +# byteorder = "1.3.2" +# derive_more = "0.99" +# futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } +# hw_common = { path = "../hw_common" } +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +url = { version = "2.2.2", features = ["serde"] } diff --git a/mm2src/trading_api/src/lib.rs b/mm2src/trading_api/src/lib.rs new file mode 100644 index 0000000000..6bcebf6beb --- /dev/null +++ b/mm2src/trading_api/src/lib.rs @@ -0,0 +1 @@ +pub mod one_inch_api; diff --git a/mm2src/trading_api/src/one_inch_api.rs b/mm2src/trading_api/src/one_inch_api.rs new file mode 100644 index 0000000000..43c417ca8a --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api.rs @@ -0,0 +1,4 @@ +// Wrapper for 1inch API +pub mod client; +pub mod errors; +pub mod types; diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs new file mode 100644 index 0000000000..25f3fb2e41 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -0,0 +1,150 @@ +use common::StatusCode; +use lazy_static::lazy_static; +use mm2_err_handle::{map_mm_error::MapMmError, + map_to_mm::MapToMmResult, + mm_error::{MmError, MmResult}}; +use serde_json::{self, Value}; +use url::Url; + +use mm2_core::mm_ctx::MmArc; +use mm2_net::transport::slurp_url_with_headers; + +use crate::one_inch_api::errors::NativeError; + +use super::errors::ApiClientError; + +const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; +const SWAP_METHOD: &str = "swap"; +const QUOTE_METHOD: &str = "quote"; + +const ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0: &str = "0x111111125421ca6dc452d289314280a0f8842a65"; +const ONE_INCH_ETH_SPECIAL_CONTRACT: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + +#[cfg(test)] +const ONE_INCH_API_TEST_URL: &str = "https://api.1inch.dev"; + +lazy_static! { + /// API key for testing + static ref ONE_INCH_API_TEST_AUTH: String = std::env::var("ONE_INCH_API_TEST_AUTH").unwrap_or_default(); +} + +pub(crate) type QueryParams<'life> = Vec<(&'life str, String)>; + +/// 1inch v6.0 supported eth-based chains +const ONE_INCH_V6_0_SUPPORTED_CHAINS: &[(&str, u64)] = &[ + ("Ethereum", 1), + ("Optimism", 10), + ("BSC", 56), + ("Gnosis", 100), + ("Polygon", 137), + ("Fantom", 250), + ("ZkSync", 324), + ("Klaytn", 8217), + ("Base", 8453), + ("Arbitrum", 42161), + ("Avalanche", 43114), + ("Aurora", 1313161554), +]; + +pub(crate) struct UrlBuilder<'a> { + base_url: Url, + endpoint: &'a str, + chain_id: u64, + method_name: String, + query_params: QueryParams<'a>, +} + +impl<'a> UrlBuilder<'a> { + pub(crate) fn new(api_client: &ApiClient, chain_id: u64, method_name: String) -> Self { + Self { + base_url: api_client.base_url.clone(), + endpoint: ApiClient::get_swap_endpoint(), + chain_id, + method_name, + query_params: vec![], + } + } + + pub(crate) fn with_query_params(mut self, mut more_params: QueryParams<'a>) -> Self { + self.query_params.append(&mut more_params); + Self { + base_url: self.base_url, + endpoint: self.endpoint, + chain_id: self.chain_id, + method_name: self.method_name, + query_params: self.query_params, + } + } + + #[allow(clippy::result_large_err)] + pub(crate) fn build(self) -> MmResult { + let url = self + .base_url + .join(self.endpoint)? + .join(&format!("{}/", self.chain_id.to_string()))? + .join(self.method_name.as_str())?; + Ok(Url::parse_with_params( + url.as_str(), + self.query_params + .iter() + .map(|v| (v.0, v.1.as_str())) + .collect::>(), + )?) + } +} + +/// 1-inch API caller +pub struct ApiClient { + base_url: Url, +} + +impl ApiClient { + #[allow(unused_variables)] + #[allow(clippy::result_large_err)] + pub fn new(ctx: MmArc) -> MmResult { + #[cfg(not(test))] + let url_cfg = ctx.conf["1inch_api"] + .as_str() + .ok_or(ApiClientError::InvalidParam("No API config param".to_owned()))?; + + #[cfg(test)] + let url_cfg = ONE_INCH_API_TEST_URL; + + Ok(Self { + base_url: Url::parse(url_cfg)?, + }) + } + + pub fn eth_special_contract() -> &'static str { ONE_INCH_ETH_SPECIAL_CONTRACT } + + pub fn classic_swap_contract() -> &'static str { ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0 } + + pub fn is_chain_supported(chain_id: u64) -> bool { + ONE_INCH_V6_0_SUPPORTED_CHAINS.iter().any(|(_name, id)| *id == chain_id) + } + + fn get_headers() -> Vec<(&'static str, &'static str)> { + vec![ + ("Authorization", ONE_INCH_API_TEST_AUTH.as_str()), + ("accept", "application/json"), + ] + } + + fn get_swap_endpoint() -> &'static str { ONE_INCH_API_ENDPOINT_V6_0 } + + pub(crate) fn get_swap_method() -> &'static str { SWAP_METHOD } + + pub(crate) fn get_quote_method() -> &'static str { QUOTE_METHOD } + + pub(crate) async fn call_api(api_url: Url) -> MmResult { + let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) + .await + .mm_err(ApiClientError::HttpClientError)?; + let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError(err.to_string()))?; + if status_code != StatusCode::OK { + let error = NativeError::new(status_code, body); + return Err(MmError::new(ApiClientError::from_native_error(error))); + } + Ok(body) + } +} diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs new file mode 100644 index 0000000000..21c3650f69 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -0,0 +1,154 @@ +use common::StatusCode; +use enum_derives::EnumFromStringify; +use ethereum_types::U256; +use mm2_net::transport::SlurpError; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize)] +pub struct GeneralApiError { + pub error: String, + pub description: Option, + pub status_code: u16, +} + +#[derive(Debug, Serialize)] +pub struct AllowanceNotEnoughError { + pub error: String, + pub description: Option, + pub status_code: u16, + /// Amount to approve for the API contract + pub amount: U256, + /// Existing allowance for the API contract + pub allowance: U256, +} + +#[derive(Debug, Serialize, EnumFromStringify)] +pub enum ApiClientError { + #[from_stringify("url::ParseError")] + InvalidParam(String), + HttpClientError(SlurpError), + ParseBodyError(String), + GeneralApiError(GeneralApiError), + AllowanceNotEnough(AllowanceNotEnoughError), +} + +impl std::fmt::Display for ApiClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ApiClientError::InvalidParam(s) => { + write!(f, "error description: {}", s) + }, + ApiClientError::GeneralApiError(GeneralApiError { description, .. }) => { + write!( + f, + "error description: {}", + description.as_ref().unwrap_or(&"".to_owned()) + ) + }, + ApiClientError::AllowanceNotEnough(AllowanceNotEnoughError { description, .. }) => { + write!( + f, + "error description: {}", + description.as_ref().unwrap_or(&"".to_owned()) + ) + }, + ApiClientError::HttpClientError(err) => write!(f, "{}", err.to_string()), + ApiClientError::ParseBodyError(err) => write!(f, "{}", err.to_string()), + } + } +} + +// API error meta 'type' field known values +const META_TYPE_ALLOWANCE: &str = "allowance"; +const META_TYPE_AMOUNT: &str = "amount"; + +#[derive(Debug, Deserialize)] +pub(crate) struct Error400 { + pub error: String, + pub description: Option, + #[serde(rename = "statusCode")] + pub status_code: u16, + pub meta: Option>, + #[allow(dead_code)] + #[serde(rename = "requestId")] + pub request_id: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct Meta { + #[serde(rename = "type")] + pub meta_type: String, + #[serde(rename = "value")] + pub meta_value: String, +} + +#[derive(Debug)] +pub(crate) struct OtherError { + pub error: String, + pub status_code: u16, +} + +#[derive(Debug)] +pub(crate) enum NativeError { + Error400(Error400), + OtherError(OtherError), + ParseError(String), +} + +impl NativeError { + pub(crate) fn new(status_code: StatusCode, body: Value) -> Self { + if status_code == StatusCode::BAD_REQUEST { + match serde_json::from_value(body) { + Ok(err) => Self::Error400(err), + Err(err) => Self::ParseError(err.to_string()), + } + } else { + Self::OtherError(OtherError { + error: body["error"].as_str().unwrap_or_default().to_owned(), + status_code: status_code.into(), + }) + } + } +} + +impl ApiClientError { + /// Convert from native API errors to lib errors + /// Look for known API errors. If none found return as general API error + pub(crate) fn from_native_error(api_error: NativeError) -> ApiClientError { + match api_error { + NativeError::Error400(error_400) => { + if let Some(meta) = error_400.meta { + // Try if it's "Not enough allowance" error 'meta' data: + if let Some(meta_allowance) = meta.iter().find(|m| m.meta_type == META_TYPE_ALLOWANCE) { + // try find 'amount' value + let amount = if let Some(meta_amount) = meta.iter().find(|m| m.meta_type == META_TYPE_AMOUNT) { + U256::from_dec_str(&meta_amount.meta_value).unwrap_or_default() + } else { + Default::default() + }; + let allowance = U256::from_dec_str(&meta_allowance.meta_value).unwrap_or_default(); + return ApiClientError::AllowanceNotEnough(AllowanceNotEnoughError { + error: error_400.error, + status_code: error_400.status_code, + description: error_400.description, + amount, + allowance, + }); + } + } + ApiClientError::GeneralApiError(GeneralApiError { + error: error_400.error, + status_code: error_400.status_code, + description: error_400.description, + }) + }, + NativeError::OtherError(other_error) => ApiClientError::GeneralApiError(GeneralApiError { + error: other_error.error, + status_code: other_error.status_code, + description: None, + }), + NativeError::ParseError(err_str) => ApiClientError::ParseBodyError(err_str), + } + } +} diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs new file mode 100644 index 0000000000..fff70bc1b5 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -0,0 +1,416 @@ +#![allow(clippy::result_large_err)] + +use super::client::{ApiClient, QueryParams, UrlBuilder}; +use super::errors::ApiClientError; +use ethereum_types::Address; +use mm2_err_handle::mm_error::MmResult; +use serde::Deserialize; + +const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; +const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; +const ONE_INCH_MAX_GAS: u128 = 11500000; +const ONE_INCH_MAX_PARTS: u32 = 100; +const ONE_INCH_MAX_MAIN_ROUTE_PARTS: u32 = 50; +const ONE_INCH_MAX_COMPLEXITY_LEVEL: u32 = 3; + +macro_rules! push_if_some { + ($arr: expr, $k: expr, $v: expr) => { + if let Some(v) = $v { + $arr.push(($k, v.to_string())) + } + }; +} + +/// API params builder for swap quote +#[derive(Default)] +pub struct ClassicSwapQuoteParams { + src: String, + dst: String, + amount: String, + + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, +} + +impl ClassicSwapQuoteParams { + pub fn new(src: String, dst: String, amount: String) -> Self { + Self { + src, + dst, + amount, + ..Default::default() + } + } + + pub fn with_fee(mut self, fee: Option) -> Self { + self.fee = fee; + self + } + pub fn with_protocols(mut self, protocols: Option) -> Self { + self.protocols = protocols; + self + } + pub fn with_gas_price(mut self, gas_price: Option) -> Self { + self.gas_price = gas_price; + self + } + pub fn with_complexity_level(mut self, complexity_level: Option) -> Self { + self.complexity_level = complexity_level; + self + } + pub fn with_parts(mut self, parts: Option) -> Self { + self.parts = parts; + self + } + pub fn with_main_route_parts(mut self, main_route_parts: Option) -> Self { + self.main_route_parts = main_route_parts; + self + } + pub fn with_gas_limit(mut self, gas_limit: Option) -> Self { + self.gas_limit = gas_limit; + self + } + pub fn with_include_tokens_info(mut self, include_tokens_info: Option) -> Self { + self.include_tokens_info = include_tokens_info; + self + } + pub fn with_include_protocols(mut self, include_protocols: Option) -> Self { + self.include_protocols = include_protocols; + self + } + pub fn with_include_gas(mut self, include_gas: Option) -> Self { + self.include_gas = include_gas; + self + } + pub fn with_connector_tokens(mut self, connector_tokens: Option) -> Self { + self.connector_tokens = connector_tokens; + self + } + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +/// API params builder to create a tx for swap +#[derive(Default)] +pub struct ClassicSwapCreateParams { + src: String, + dst: String, + amount: String, + from: String, + slippage: f32, + + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, + permit: Option, + receiver: Option, + referrer: Option, + /// Disable gas estimation + disable_estimate: Option, + /// Allow the swap to be partially filled + allow_partial_fill: Option, +} + +impl ClassicSwapCreateParams { + pub fn new(src: String, dst: String, amount: String, from: String, slippage: f32) -> Self { + Self { + src, + dst, + amount, + from, + slippage, + ..Default::default() + } + } + + pub fn with_fee(mut self, fee: Option) -> Self { + self.fee = fee; + self + } + pub fn with_protocols(mut self, protocols: Option) -> Self { + self.protocols = protocols; + self + } + pub fn with_gas_price(mut self, gas_price: Option) -> Self { + self.gas_price = gas_price; + self + } + pub fn with_complexity_level(mut self, complexity_level: Option) -> Self { + self.complexity_level = complexity_level; + self + } + pub fn with_parts(mut self, parts: Option) -> Self { + self.parts = parts; + self + } + pub fn with_main_route_parts(mut self, main_route_parts: Option) -> Self { + self.main_route_parts = main_route_parts; + self + } + pub fn with_gas_limit(mut self, gas_limit: Option) -> Self { + self.gas_limit = gas_limit; + self + } + pub fn with_include_tokens_info(mut self, include_tokens_info: Option) -> Self { + self.include_tokens_info = include_tokens_info; + self + } + pub fn with_include_protocols(mut self, include_protocols: Option) -> Self { + self.include_protocols = include_protocols; + self + } + pub fn with_include_gas(mut self, include_gas: Option) -> Self { + self.include_gas = include_gas; + self + } + pub fn with_connector_tokens(mut self, connector_tokens: Option) -> Self { + self.connector_tokens = connector_tokens; + self + } + pub fn with_permit(mut self, permit: Option) -> Self { + self.permit = permit; + self + } + pub fn with_receiver(mut self, receiver: Option) -> Self { + self.receiver = receiver; + self + } + pub fn with_referrer(mut self, referrer: Option) -> Self { + self.referrer = referrer; + self + } + pub fn with_disable_estimate(mut self, disable_estimate: Option) -> Self { + self.disable_estimate = disable_estimate; + self + } + pub fn with_allow_partial_fill(mut self, allow_partial_fill: Option) -> Self { + self.allow_partial_fill = allow_partial_fill; + self + } + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ("from", self.from.clone()), + ("slippage", self.slippage.to_string()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + push_if_some!(params, "permit", &self.permit); + push_if_some!(params, "receiver", &self.receiver); + push_if_some!(params, "referrer", &self.referrer); + push_if_some!(params, "disableEstimate", self.disable_estimate); + push_if_some!(params, "allowPartialFill", self.allow_partial_fill); + + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_slippage(self.slippage)?; + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +#[derive(Deserialize, Debug)] +pub struct TokenInfo { + pub address: Address, + pub symbol: String, + pub name: String, + pub decimals: u32, + pub eip2612: bool, + #[serde(rename = "isFoT")] + pub is_fot: bool, + #[serde(rename = "logoURI")] + pub logo_uri: String, + pub tags: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct ProtocolInfo { + pub name: String, + pub part: f64, + + #[serde(rename = "fromTokenAddress")] + pub from_token_address: Address, + + #[serde(rename = "toTokenAddress")] + pub to_token_address: Address, +} + +#[derive(Deserialize, Debug)] +pub struct ClassicSwapData { + /// dst token amount to receive, in api is a decimal number as string + #[serde(rename = "dstAmount")] + pub dst_amount: String, + #[serde(rename = "srcToken")] + pub src_token: Option, + #[serde(rename = "dstToken")] + pub dst_token: Option, + pub protocols: Option>>>, + pub tx: Option, + pub gas: Option, +} + +#[derive(Deserialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: String, + /// tx value, in api is a decimal number as string + pub value: String, + /// gas price, in api is a decimal number as string + #[serde(rename = "gasPrice")] + pub gas_price: String, + /// gas limit, in api is a decimal number + pub gas: u128, +} + +impl ApiClient { + pub async fn get_classic_swap_quote( + &self, + chain_id: u64, + params: QueryParams<'_>, + ) -> MmResult { + let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_quote_method().to_owned()) + .with_query_params(params) + .build()?; + let value = Self::call_api(api_url).await?; + serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) + } + + pub async fn get_classic_swap_tx( + &self, + chain_id: u64, + params: QueryParams<'_>, + ) -> MmResult { + let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_swap_method().to_owned()) + .with_query_params(params) + .build()?; + + let value = Self::call_api(api_url).await?; + serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) + } +} + +fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { + if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { + return Err(ApiClientError::InvalidParam("invalid slippage param".to_owned()).into()); + } + Ok(()) +} + +fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { + if let Some(fee) = fee { + if !(0.0..=ONE_INCH_MAX_FEE_SHARE).contains(fee) { + return Err(ApiClientError::InvalidParam("invalid fee param".to_owned()).into()); + } + } + Ok(()) +} + +fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> { + if let Some(gas_limit) = gas_limit { + if gas_limit > &ONE_INCH_MAX_GAS { + return Err(ApiClientError::InvalidParam("invalid gas param".to_owned()).into()); + } + } + Ok(()) +} + +fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(parts) = parts { + if parts > &ONE_INCH_MAX_PARTS { + return Err(ApiClientError::InvalidParam("invalid max parts param".to_owned()).into()); + } + } + Ok(()) +} + +fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(parts) = main_route_parts { + if parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { + return Err(ApiClientError::InvalidParam("invalid max main route parts param".to_owned()).into()); + } + } + Ok(()) +} + +fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), ApiClientError> { + if let Some(complexity_level) = complexity_level { + if complexity_level > &ONE_INCH_MAX_COMPLEXITY_LEVEL { + return Err(ApiClientError::InvalidParam("invalid max complexity level param".to_owned()).into()); + } + } + Ok(()) +} From 57192ca82a7f8c31be06edbbe59d249828d0685d Mon Sep 17 00:00:00 2001 From: dimxy Date: Fri, 20 Sep 2024 17:14:47 +0500 Subject: [PATCH 04/24] refactor on review notes --- Cargo.lock | 1 + mm2src/mm2_main/src/ext_api.rs | 3 +- mm2src/mm2_main/src/ext_api/one_inch.rs | 3 +- .../mm2_main/src/ext_api/one_inch/errors.rs | 6 +- mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 46 ++--- mm2src/mm2_main/src/ext_api/one_inch/types.rs | 85 +------- mm2src/trading_api/Cargo.toml | 1 + mm2src/trading_api/src/lib.rs | 2 + mm2src/trading_api/src/one_inch_api.rs | 3 +- mm2src/trading_api/src/one_inch_api/client.rs | 35 +++- mm2src/trading_api/src/one_inch_api/types.rs | 188 ++++-------------- 11 files changed, 109 insertions(+), 264 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec7389f255..b20151bda6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8707,6 +8707,7 @@ dependencies = [ "mm2_err_handle", "mm2_net", "mm2_number", + "paste", "serde", "serde_derive", "serde_json", diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs index fbeed01e06..f1b92c145f 100644 --- a/mm2src/mm2_main/src/ext_api.rs +++ b/mm2src/mm2_main/src/ext_api.rs @@ -1,4 +1,3 @@ -/// RPCs to integrate with external third party trading API +//! RPCs for integration with external third party trading APIs. -/// RPCs to access 1inch eth-like swap API pub mod one_inch; diff --git a/mm2src/mm2_main/src/ext_api/one_inch.rs b/mm2src/mm2_main/src/ext_api/one_inch.rs index 21ca1216e4..3d47853294 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch.rs @@ -1,4 +1,5 @@ +//! RPC implementation for integration with 1inch swap API provider. + pub mod errors; pub mod rpcs; -/// RPC imlementation to integrate 1inch api pub mod types; diff --git a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs index 23b4fa552a..1e1eebc890 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs @@ -10,7 +10,7 @@ use trading_api::one_inch_api::errors::ApiClientError; #[serde(tag = "error_type", content = "error_data")] pub enum ApiIntegrationRpcError { #[from_stringify("coins::CoinFindError")] - CoinFindError(String), + NoSuchCoin(String), #[display(fmt = "EVM token needed")] CoinTypeError, #[display(fmt = "NFT not supported")] @@ -33,8 +33,8 @@ pub enum ApiIntegrationRpcError { impl HttpStatusCode for ApiIntegrationRpcError { fn status_code(&self) -> StatusCode { match self { - ApiIntegrationRpcError::CoinFindError(_) - | ApiIntegrationRpcError::CoinTypeError + ApiIntegrationRpcError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + ApiIntegrationRpcError::CoinTypeError | ApiIntegrationRpcError::NftNotSupported | ApiIntegrationRpcError::ChainNotSupported | ApiIntegrationRpcError::MyAddressError(_) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs index c2147b814f..bc7059a9da 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -29,7 +29,7 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; api_supports_coin(&base)?; let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; - let sell_amount = wei_from_big_decimal(&req.amount, base.decimals()) + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; let query_params = ClassicSwapQuoteParams::new(base_contract, rel_contract, sell_amount.to_string()) .with_fee(req.fee) @@ -39,21 +39,9 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( .with_parts(req.parts) .with_main_route_parts(req.main_route_parts) .with_gas_limit(req.gas_limit) - .with_include_tokens_info(if let Some(include_tokens_info) = req.include_tokens_info { - Some(include_tokens_info) - } else { - Some(INCLUDE_TOKENS_INFO) - }) - .with_include_protocols(if let Some(include_protocols) = req.include_protocols { - Some(include_protocols) - } else { - Some(INCLUDE_PROTOCOLS) - }) - .with_include_gas(if let Some(include_gas) = req.include_gas { - Some(include_gas) - } else { - Some(INCLUDE_GAS) - }) + .with_include_tokens_info(Some(req.include_tokens_info.unwrap_or(INCLUDE_TOKENS_INFO))) + .with_include_protocols(Some(req.include_protocols.unwrap_or(INCLUDE_PROTOCOLS))) + .with_include_gas(Some(req.include_gas.unwrap_or(INCLUDE_GAS))) .with_connector_tokens(req.connector_tokens) .build_query_params() .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; @@ -66,7 +54,9 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) } -/// "1inch_classic_swap_create" rpc impl +/// "1inch_classic_swap_create" rpc implementation +/// This rpc actually returns a transaction to call the 1inch swap aggregation contract. GUI should sign it and send to the chain. +/// We don't verify the transaction in any way and trust the 1inch api. pub async fn one_inch_v6_0_classic_swap_create_rpc( ctx: MmArc, req: ClassicSwapCreateRequest, @@ -75,7 +65,7 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( api_supports_coin(&base)?; let (_, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; - let sell_amount = wei_from_big_decimal(&req.amount, base.decimals()) + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; let single_address = base.derivation_method().single_addr_or_err().await?; @@ -93,21 +83,9 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( .with_parts(req.parts) .with_main_route_parts(req.main_route_parts) .with_gas_limit(req.gas_limit) - .with_include_tokens_info(if let Some(include_tokens_info) = req.include_tokens_info { - Some(include_tokens_info) - } else { - Some(INCLUDE_TOKENS_INFO) - }) - .with_include_protocols(if let Some(include_protocols) = req.include_protocols { - Some(include_protocols) - } else { - Some(INCLUDE_PROTOCOLS) - }) - .with_include_gas(if let Some(include_gas) = req.include_gas { - Some(include_gas) - } else { - Some(INCLUDE_GAS) - }) + .with_include_tokens_info(Some(req.include_tokens_info.unwrap_or(INCLUDE_TOKENS_INFO))) + .with_include_protocols(Some(req.include_protocols.unwrap_or(INCLUDE_PROTOCOLS))) + .with_include_gas(Some(req.include_gas.unwrap_or(INCLUDE_GAS))) .with_connector_tokens(req.connector_tokens) .with_permit(req.permit) .with_receiver(req.receiver) @@ -133,7 +111,7 @@ async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, let contract = match coin.coin_type { EthCoinType::Eth => ApiClient::eth_special_contract().to_owned(), EthCoinType::Erc20 { token_addr, .. } => display_eth_address(&token_addr), - EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), + EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::NftNotSupported)), }; Ok((coin, contract)) } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index 7ccb107965..198fbbdd34 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -2,10 +2,11 @@ use crate::ext_api::one_inch::errors::FromApiValueError; use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; use ethereum_types::{Address, U256}; use mm2_err_handle::prelude::*; -use mm2_number::BigDecimal; +use mm2_number::{BigDecimal, MmNumber}; use rpc::v1::types::Bytes as BytesJson; use serde::{Deserialize, Serialize}; -use trading_api::one_inch_api; +use trading_api::one_inch_api::{self, + types::{ProtocolInfo, TokenInfo}}; #[derive(Clone, Debug, Deserialize)] pub struct AggregationContractRequest {} @@ -14,8 +15,7 @@ pub struct AggregationContractRequest {} pub struct ClassicSwapQuoteRequest { pub base: String, pub rel: String, - pub amount: BigDecimal, - + pub amount: MmNumber, // Optional fields pub fee: Option, pub protocols: Option, @@ -24,7 +24,6 @@ pub struct ClassicSwapQuoteRequest { pub parts: Option, pub main_route_parts: Option, pub gas_limit: Option, - pub include_tokens_info: Option, pub include_protocols: Option, pub include_gas: Option, @@ -35,9 +34,8 @@ pub struct ClassicSwapQuoteRequest { pub struct ClassicSwapCreateRequest { pub base: String, pub rel: String, - pub amount: BigDecimal, + pub amount: MmNumber, pub slippage: f32, - // Optional fields pub fee: Option, pub protocols: Option, @@ -53,7 +51,6 @@ pub struct ClassicSwapCreateRequest { pub permit: Option, pub receiver: Option, pub referrer: Option, - /// Disable gas estimation pub disable_estimate: Option, /// Allow the swap to be partially filled @@ -62,7 +59,7 @@ pub struct ClassicSwapCreateRequest { #[derive(Serialize, Debug)] pub struct ClassicSwapResponse { - pub dst_amount: BigDecimal, + pub dst_amount: MmNumber, #[serde(skip_serializing_if = "Option::is_none")] pub src_token: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -80,10 +77,10 @@ impl ClassicSwapResponse { decimals: u8, ) -> MmResult { Ok(Self { - dst_amount: u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?, - src_token: TokenInfo::from_api_value(data.src_token), - dst_token: TokenInfo::from_api_value(data.dst_token), - protocols: ProtocolInfo::from_api_value(data.protocols), + dst_amount: MmNumber::from(u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?), + src_token: data.src_token, + dst_token: data.dst_token, + protocols: data.protocols, tx: data.tx.map(|tx| TxFields::from_api_value(tx, decimals)).transpose()?, gas: data.gas, }) @@ -103,7 +100,7 @@ pub struct TxFields { impl TxFields { pub(crate) fn from_api_value( - tx_fields: trading_api::one_inch_api::types::TxFields, + tx_fields: one_inch_api::types::TxFields, decimals: u8, ) -> MmResult { Ok(Self { @@ -116,63 +113,3 @@ impl TxFields { }) } } - -#[derive(Serialize, Debug)] -pub struct TokenInfo { - pub address: Address, - pub symbol: String, - pub name: String, - pub decimals: u32, - pub eip2612: bool, - pub is_fot: bool, - pub logo_uri: String, - pub tags: Vec, -} - -impl TokenInfo { - pub(crate) fn from_api_value(opt_info: Option) -> Option { - opt_info.map(|info| Self { - address: info.address, - symbol: info.symbol, - name: info.name, - decimals: info.decimals, - eip2612: info.eip2612, - is_fot: info.is_fot, - logo_uri: info.logo_uri, - tags: info.tags, - }) - } -} - -#[derive(Debug, Serialize)] -pub struct ProtocolInfo { - pub name: String, - pub part: f64, - pub from_token_address: Address, - pub to_token_address: Address, -} - -impl ProtocolInfo { - pub(crate) fn from_api_value( - opt_info: Option>>>, - ) -> Option>>> { - opt_info.map(|v0| { - v0.into_iter() - .map(|v1| { - v1.into_iter() - .map(|v2| { - v2.into_iter() - .map(|info| Self { - name: info.name, - part: info.part, - from_token_address: info.from_token_address, - to_token_address: info.to_token_address, - }) - .collect::>() - }) - .collect::>() - }) - .collect::>() - }) - } -} diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 79601880b5..3da6026d9a 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -19,6 +19,7 @@ mm2_number = { path = "../mm2_number" } # hw_common = { path = "../hw_common" } ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } lazy_static = "1.4" +paste = "1.0" serde = "1.0" serde_derive = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/trading_api/src/lib.rs b/mm2src/trading_api/src/lib.rs index 6bcebf6beb..183e6d9bcd 100644 --- a/mm2src/trading_api/src/lib.rs +++ b/mm2src/trading_api/src/lib.rs @@ -1 +1,3 @@ +//! This module is for indirect connection to third-party trading APIs, processing their results and errors + pub mod one_inch_api; diff --git a/mm2src/trading_api/src/one_inch_api.rs b/mm2src/trading_api/src/one_inch_api.rs index 43c417ca8a..9b0af1625e 100644 --- a/mm2src/trading_api/src/one_inch_api.rs +++ b/mm2src/trading_api/src/one_inch_api.rs @@ -1,4 +1,5 @@ -// Wrapper for 1inch API +//! Wrapper for 1inch API. + pub mod client; pub mod errors; pub mod types; diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index 25f3fb2e41..a220c30339 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -11,7 +11,7 @@ use mm2_net::transport::slurp_url_with_headers; use crate::one_inch_api::errors::NativeError; -use super::errors::ApiClientError; +use super::{errors::ApiClientError, types::ClassicSwapData}; const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; const SWAP_METHOD: &str = "swap"; @@ -115,9 +115,9 @@ impl ApiClient { }) } - pub fn eth_special_contract() -> &'static str { ONE_INCH_ETH_SPECIAL_CONTRACT } + pub const fn eth_special_contract() -> &'static str { ONE_INCH_ETH_SPECIAL_CONTRACT } - pub fn classic_swap_contract() -> &'static str { ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0 } + pub const fn classic_swap_contract() -> &'static str { ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0 } pub fn is_chain_supported(chain_id: u64) -> bool { ONE_INCH_V6_0_SUPPORTED_CHAINS.iter().any(|(_name, id)| *id == chain_id) @@ -132,9 +132,9 @@ impl ApiClient { fn get_swap_endpoint() -> &'static str { ONE_INCH_API_ENDPOINT_V6_0 } - pub(crate) fn get_swap_method() -> &'static str { SWAP_METHOD } + pub(crate) const fn get_swap_method() -> &'static str { SWAP_METHOD } - pub(crate) fn get_quote_method() -> &'static str { QUOTE_METHOD } + pub(crate) const fn get_quote_method() -> &'static str { QUOTE_METHOD } pub(crate) async fn call_api(api_url: Url) -> MmResult { let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) @@ -147,4 +147,29 @@ impl ApiClient { } Ok(body) } + + pub async fn get_classic_swap_quote( + &self, + chain_id: u64, + params: QueryParams<'_>, + ) -> MmResult { + let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_quote_method().to_owned()) + .with_query_params(params) + .build()?; + let value = Self::call_api(api_url).await?; + serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) + } + + pub async fn get_classic_swap_tx( + &self, + chain_id: u64, + params: QueryParams<'_>, + ) -> MmResult { + let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_swap_method().to_owned()) + .with_query_params(params) + .build()?; + + let value = Self::call_api(api_url).await?; + serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) + } } diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index fff70bc1b5..a62b39cb94 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -1,10 +1,11 @@ #![allow(clippy::result_large_err)] -use super::client::{ApiClient, QueryParams, UrlBuilder}; +use super::client::QueryParams; use super::errors::ApiClientError; use ethereum_types::Address; use mm2_err_handle::mm_error::MmResult; -use serde::Deserialize; +use paste::paste; +use serde::{Deserialize, Serialize}; const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; @@ -21,13 +22,23 @@ macro_rules! push_if_some { }; } +macro_rules! def_with_opt_param { + ($var: ident, $var_type: ty) => { + paste! { + pub fn [](mut self, $var: Option<$var_type>) -> Self { + self.$var = $var; + self + } + } + }; +} + /// API params builder for swap quote #[derive(Default)] pub struct ClassicSwapQuoteParams { src: String, dst: String, amount: String, - // Optional fields fee: Option, protocols: Option, @@ -36,7 +47,6 @@ pub struct ClassicSwapQuoteParams { parts: Option, main_route_parts: Option, gas_limit: Option, - include_tokens_info: Option, include_protocols: Option, include_gas: Option, @@ -53,50 +63,17 @@ impl ClassicSwapQuoteParams { } } - pub fn with_fee(mut self, fee: Option) -> Self { - self.fee = fee; - self - } - pub fn with_protocols(mut self, protocols: Option) -> Self { - self.protocols = protocols; - self - } - pub fn with_gas_price(mut self, gas_price: Option) -> Self { - self.gas_price = gas_price; - self - } - pub fn with_complexity_level(mut self, complexity_level: Option) -> Self { - self.complexity_level = complexity_level; - self - } - pub fn with_parts(mut self, parts: Option) -> Self { - self.parts = parts; - self - } - pub fn with_main_route_parts(mut self, main_route_parts: Option) -> Self { - self.main_route_parts = main_route_parts; - self - } - pub fn with_gas_limit(mut self, gas_limit: Option) -> Self { - self.gas_limit = gas_limit; - self - } - pub fn with_include_tokens_info(mut self, include_tokens_info: Option) -> Self { - self.include_tokens_info = include_tokens_info; - self - } - pub fn with_include_protocols(mut self, include_protocols: Option) -> Self { - self.include_protocols = include_protocols; - self - } - pub fn with_include_gas(mut self, include_gas: Option) -> Self { - self.include_gas = include_gas; - self - } - pub fn with_connector_tokens(mut self, connector_tokens: Option) -> Self { - self.connector_tokens = connector_tokens; - self - } + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); pub fn build_query_params(&self) -> MmResult, ApiClientError> { self.validate_params()?; @@ -140,7 +117,6 @@ pub struct ClassicSwapCreateParams { amount: String, from: String, slippage: f32, - // Optional fields fee: Option, protocols: Option, @@ -154,6 +130,7 @@ pub struct ClassicSwapCreateParams { include_gas: Option, connector_tokens: Option, permit: Option, + /// Funds receiver receiver: Option, referrer: Option, /// Disable gas estimation @@ -174,70 +151,22 @@ impl ClassicSwapCreateParams { } } - pub fn with_fee(mut self, fee: Option) -> Self { - self.fee = fee; - self - } - pub fn with_protocols(mut self, protocols: Option) -> Self { - self.protocols = protocols; - self - } - pub fn with_gas_price(mut self, gas_price: Option) -> Self { - self.gas_price = gas_price; - self - } - pub fn with_complexity_level(mut self, complexity_level: Option) -> Self { - self.complexity_level = complexity_level; - self - } - pub fn with_parts(mut self, parts: Option) -> Self { - self.parts = parts; - self - } - pub fn with_main_route_parts(mut self, main_route_parts: Option) -> Self { - self.main_route_parts = main_route_parts; - self - } - pub fn with_gas_limit(mut self, gas_limit: Option) -> Self { - self.gas_limit = gas_limit; - self - } - pub fn with_include_tokens_info(mut self, include_tokens_info: Option) -> Self { - self.include_tokens_info = include_tokens_info; - self - } - pub fn with_include_protocols(mut self, include_protocols: Option) -> Self { - self.include_protocols = include_protocols; - self - } - pub fn with_include_gas(mut self, include_gas: Option) -> Self { - self.include_gas = include_gas; - self - } - pub fn with_connector_tokens(mut self, connector_tokens: Option) -> Self { - self.connector_tokens = connector_tokens; - self - } - pub fn with_permit(mut self, permit: Option) -> Self { - self.permit = permit; - self - } - pub fn with_receiver(mut self, receiver: Option) -> Self { - self.receiver = receiver; - self - } - pub fn with_referrer(mut self, referrer: Option) -> Self { - self.referrer = referrer; - self - } - pub fn with_disable_estimate(mut self, disable_estimate: Option) -> Self { - self.disable_estimate = disable_estimate; - self - } - pub fn with_allow_partial_fill(mut self, allow_partial_fill: Option) -> Self { - self.allow_partial_fill = allow_partial_fill; - self - } + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + def_with_opt_param!(permit, String); + def_with_opt_param!(receiver, String); + def_with_opt_param!(referrer, String); + def_with_opt_param!(disable_estimate, bool); + def_with_opt_param!(allow_partial_fill, bool); pub fn build_query_params(&self) -> MmResult, ApiClientError> { self.validate_params()?; @@ -282,7 +211,7 @@ impl ClassicSwapCreateParams { } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Serialize)] pub struct TokenInfo { pub address: Address, pub symbol: String, @@ -296,14 +225,12 @@ pub struct TokenInfo { pub tags: Vec, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ProtocolInfo { pub name: String, pub part: f64, - #[serde(rename = "fromTokenAddress")] pub from_token_address: Address, - #[serde(rename = "toTokenAddress")] pub to_token_address: Address, } @@ -336,33 +263,6 @@ pub struct TxFields { pub gas: u128, } -impl ApiClient { - pub async fn get_classic_swap_quote( - &self, - chain_id: u64, - params: QueryParams<'_>, - ) -> MmResult { - let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_quote_method().to_owned()) - .with_query_params(params) - .build()?; - let value = Self::call_api(api_url).await?; - serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) - } - - pub async fn get_classic_swap_tx( - &self, - chain_id: u64, - params: QueryParams<'_>, - ) -> MmResult { - let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_swap_method().to_owned()) - .with_query_params(params) - .build()?; - - let value = Self::call_api(api_url).await?; - serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) - } -} - fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { return Err(ApiClientError::InvalidParam("invalid slippage param".to_owned()).into()); From 1c31a7a73e717dffcd8c85b9cef397957a4e25e4 Mon Sep 17 00:00:00 2001 From: dimxy Date: Fri, 20 Sep 2024 17:27:51 +0500 Subject: [PATCH 05/24] added doc comment for swap params --- mm2src/trading_api/src/one_inch_api/types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index a62b39cb94..e64dbdc9b5 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -36,7 +36,9 @@ macro_rules! def_with_opt_param { /// API params builder for swap quote #[derive(Default)] pub struct ClassicSwapQuoteParams { + /// Source token address src: String, + /// Destination token address dst: String, amount: String, // Optional fields From 3610147ad88ce67926cb1ae32bc2ea71eb01c7ac Mon Sep 17 00:00:00 2001 From: dimxy Date: Fri, 20 Sep 2024 19:04:24 +0500 Subject: [PATCH 06/24] refactor on review notes (more) --- mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 21 +++++++------------ mm2src/mm2_main/src/ext_api/one_inch/types.rs | 19 +++++++++++------ mm2src/trading_api/src/one_inch_api/client.rs | 21 +++++-------------- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs index bc7059a9da..b860a86d84 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -7,11 +7,6 @@ use mm2_err_handle::prelude::*; use trading_api::one_inch_api::client::ApiClient; use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams}; -// Default swap optional params: -const INCLUDE_GAS: bool = true; -const INCLUDE_PROTOCOLS: bool = true; -const INCLUDE_TOKENS_INFO: bool = true; - /// "1inch_v6_0_classic_swap_contract" rpc impl /// used to get contract address (for e.g. to approve funds) pub async fn one_inch_v6_0_classic_swap_contract_rpc( @@ -39,15 +34,15 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( .with_parts(req.parts) .with_main_route_parts(req.main_route_parts) .with_gas_limit(req.gas_limit) - .with_include_tokens_info(Some(req.include_tokens_info.unwrap_or(INCLUDE_TOKENS_INFO))) - .with_include_protocols(Some(req.include_protocols.unwrap_or(INCLUDE_PROTOCOLS))) - .with_include_gas(Some(req.include_gas.unwrap_or(INCLUDE_GAS))) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) .with_connector_tokens(req.connector_tokens) .build_query_params() .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; let quote = ApiClient::new(ctx) .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))? - .get_classic_swap_quote(base.chain_id(), query_params) + .call_swap_api(base.chain_id(), ApiClient::get_quote_method().to_owned(), query_params) .await .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin ClassicSwapResponse::from_api_value(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin @@ -83,9 +78,9 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( .with_parts(req.parts) .with_main_route_parts(req.main_route_parts) .with_gas_limit(req.gas_limit) - .with_include_tokens_info(Some(req.include_tokens_info.unwrap_or(INCLUDE_TOKENS_INFO))) - .with_include_protocols(Some(req.include_protocols.unwrap_or(INCLUDE_PROTOCOLS))) - .with_include_gas(Some(req.include_gas.unwrap_or(INCLUDE_GAS))) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) .with_connector_tokens(req.connector_tokens) .with_permit(req.permit) .with_receiver(req.receiver) @@ -96,7 +91,7 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; let swap_with_tx = ApiClient::new(ctx) .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))? - .get_classic_swap_tx(base.chain_id(), query_params) + .call_swap_api(base.chain_id(), ApiClient::get_swap_method().to_owned(), query_params) .await .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin ClassicSwapResponse::from_api_value(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index 198fbbdd34..7bf5d61f1d 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -1,5 +1,6 @@ use crate::ext_api::one_inch::errors::FromApiValueError; use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; +use common::true_f; use ethereum_types::{Address, U256}; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; @@ -24,9 +25,12 @@ pub struct ClassicSwapQuoteRequest { pub parts: Option, pub main_route_parts: Option, pub gas_limit: Option, - pub include_tokens_info: Option, - pub include_protocols: Option, - pub include_gas: Option, + #[serde(default = "true_f")] + pub include_tokens_info: bool, + #[serde(default = "true_f")] + pub include_protocols: bool, + #[serde(default = "true_f")] + pub include_gas: bool, pub connector_tokens: Option, } @@ -44,9 +48,12 @@ pub struct ClassicSwapCreateRequest { pub parts: Option, pub main_route_parts: Option, pub gas_limit: Option, - pub include_tokens_info: Option, - pub include_protocols: Option, - pub include_gas: Option, + #[serde(default = "true_f")] + pub include_tokens_info: bool, + #[serde(default = "true_f")] + pub include_protocols: bool, + #[serde(default = "true_f")] + pub include_gas: bool, pub connector_tokens: Option, pub permit: Option, pub receiver: Option, diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index a220c30339..d7b901913a 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -132,9 +132,9 @@ impl ApiClient { fn get_swap_endpoint() -> &'static str { ONE_INCH_API_ENDPOINT_V6_0 } - pub(crate) const fn get_swap_method() -> &'static str { SWAP_METHOD } + pub const fn get_swap_method() -> &'static str { SWAP_METHOD } - pub(crate) const fn get_quote_method() -> &'static str { QUOTE_METHOD } + pub const fn get_quote_method() -> &'static str { QUOTE_METHOD } pub(crate) async fn call_api(api_url: Url) -> MmResult { let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) @@ -148,24 +148,13 @@ impl ApiClient { Ok(body) } - pub async fn get_classic_swap_quote( + pub async fn call_swap_api( &self, chain_id: u64, + method: String, params: QueryParams<'_>, ) -> MmResult { - let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_quote_method().to_owned()) - .with_query_params(params) - .build()?; - let value = Self::call_api(api_url).await?; - serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) - } - - pub async fn get_classic_swap_tx( - &self, - chain_id: u64, - params: QueryParams<'_>, - ) -> MmResult { - let api_url = UrlBuilder::new(self, chain_id, ApiClient::get_swap_method().to_owned()) + let api_url = UrlBuilder::new(self, chain_id, method) .with_query_params(params) .build()?; From 23d8f1d6e387b45d6f91ad37f411bb08e976c368 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sun, 22 Sep 2024 22:14:49 +0500 Subject: [PATCH 07/24] refactor on review notes (more) --- Cargo.lock | 2 +- mm2src/common/Cargo.toml | 1 + mm2src/common/common.rs | 22 +++++++++++++++++++ mm2src/trading_api/Cargo.toml | 1 - mm2src/trading_api/src/one_inch_api/client.rs | 21 ++++++------------ mm2src/trading_api/src/one_inch_api/types.rs | 21 +----------------- 6 files changed, 32 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b20151bda6..429dc8cf7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,7 @@ dependencies = [ "log", "parking_lot 0.12.0", "parking_lot_core 0.6.2", + "paste", "primitive-types", "rand 0.7.3", "regex", @@ -8707,7 +8708,6 @@ dependencies = [ "mm2_err_handle", "mm2_net", "mm2_number", - "paste", "serde", "serde_derive", "serde_json", diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 6a5395b360..b775741cf8 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -34,6 +34,7 @@ lazy_static = "1.4" log = "0.4.17" parking_lot = { version = "0.12.0", features = ["nightly"] } parking_lot_core = { version = "0.6", features = ["nightly"] } +paste = "1.0" primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng"] } rustc-hash = "1.1.0" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 0588c01385..4992772164 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -172,6 +172,7 @@ use std::ptr::read_volatile; use std::sync::atomic::Ordering; use std::time::{Duration, SystemTime, SystemTimeError}; use uuid::Uuid; +pub use paste::paste; pub use http::StatusCode; pub use serde; @@ -1135,6 +1136,27 @@ macro_rules! str_strip_0x { }; } +#[macro_export] +macro_rules! push_if_some { + ($arr: expr, $k: expr, $v: expr) => { + if let Some(v) = $v { + $arr.push(($k, v.to_string())) + } + }; +} + +#[macro_export] +macro_rules! def_with_opt_param { + ($var: ident, $var_type: ty) => { + $crate::paste! { + pub fn [](&mut self, $var: Option<$var_type>) -> &mut Self { + self.$var = $var; + self + } + } + }; +} + #[test] fn test_http_uri_to_ws_address() { let uri = "https://cosmos-rpc.polkachu.com".parse::().unwrap(); diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 3da6026d9a..79601880b5 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -19,7 +19,6 @@ mm2_number = { path = "../mm2_number" } # hw_common = { path = "../hw_common" } ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } lazy_static = "1.4" -paste = "1.0" serde = "1.0" serde_derive = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index d7b901913a..b33b6d9535 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -3,7 +3,7 @@ use lazy_static::lazy_static; use mm2_err_handle::{map_mm_error::MapMmError, map_to_mm::MapToMmResult, mm_error::{MmError, MmResult}}; -use serde_json::{self, Value}; +use serde::de::DeserializeOwned; use url::Url; use mm2_core::mm_ctx::MmArc; @@ -65,19 +65,13 @@ impl<'a> UrlBuilder<'a> { } } - pub(crate) fn with_query_params(mut self, mut more_params: QueryParams<'a>) -> Self { + pub(crate) fn with_query_params(&mut self, mut more_params: QueryParams<'a>) -> &mut Self { self.query_params.append(&mut more_params); - Self { - base_url: self.base_url, - endpoint: self.endpoint, - chain_id: self.chain_id, - method_name: self.method_name, - query_params: self.query_params, - } + self } #[allow(clippy::result_large_err)] - pub(crate) fn build(self) -> MmResult { + pub(crate) fn build(&self) -> MmResult { let url = self .base_url .join(self.endpoint)? @@ -136,7 +130,7 @@ impl ApiClient { pub const fn get_quote_method() -> &'static str { QUOTE_METHOD } - pub(crate) async fn call_api(api_url: Url) -> MmResult { + pub(crate) async fn call_api(api_url: &Url) -> MmResult { let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) .await .mm_err(ApiClientError::HttpClientError)?; @@ -145,7 +139,7 @@ impl ApiClient { let error = NativeError::new(status_code, body); return Err(MmError::new(ApiClientError::from_native_error(error))); } - Ok(body) + serde_json::from_value(body).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) } pub async fn call_swap_api( @@ -158,7 +152,6 @@ impl ApiClient { .with_query_params(params) .build()?; - let value = Self::call_api(api_url).await?; - serde_json::from_value(value).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) + Self::call_api(&api_url).await } } diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index e64dbdc9b5..6cf75434ec 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -3,8 +3,8 @@ use super::client::QueryParams; use super::errors::ApiClientError; use ethereum_types::Address; +use common::{push_if_some, def_with_opt_param}; use mm2_err_handle::mm_error::MmResult; -use paste::paste; use serde::{Deserialize, Serialize}; const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; @@ -14,25 +14,6 @@ const ONE_INCH_MAX_PARTS: u32 = 100; const ONE_INCH_MAX_MAIN_ROUTE_PARTS: u32 = 50; const ONE_INCH_MAX_COMPLEXITY_LEVEL: u32 = 3; -macro_rules! push_if_some { - ($arr: expr, $k: expr, $v: expr) => { - if let Some(v) = $v { - $arr.push(($k, v.to_string())) - } - }; -} - -macro_rules! def_with_opt_param { - ($var: ident, $var_type: ty) => { - paste! { - pub fn [](mut self, $var: Option<$var_type>) -> Self { - self.$var = $var; - self - } - } - }; -} - /// API params builder for swap quote #[derive(Default)] pub struct ClassicSwapQuoteParams { From 18466fc42af61d725186e54ad4b8f05e68f36d79 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sun, 22 Sep 2024 22:22:19 +0500 Subject: [PATCH 08/24] fix fmt --- mm2src/common/common.rs | 2 +- mm2src/trading_api/src/one_inch_api/types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 4992772164..cf4fd6391d 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -151,6 +151,7 @@ use futures01::{future, Future}; use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; +pub use paste::paste; use rand::RngCore; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; @@ -172,7 +173,6 @@ use std::ptr::read_volatile; use std::sync::atomic::Ordering; use std::time::{Duration, SystemTime, SystemTimeError}; use uuid::Uuid; -pub use paste::paste; pub use http::StatusCode; pub use serde; diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index 6cf75434ec..5e963ba0ba 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -2,8 +2,8 @@ use super::client::QueryParams; use super::errors::ApiClientError; +use common::{def_with_opt_param, push_if_some}; use ethereum_types::Address; -use common::{push_if_some, def_with_opt_param}; use mm2_err_handle::mm_error::MmResult; use serde::{Deserialize, Serialize}; From 5ec045f64fd1548c3c0f29fa289474ba33386561 Mon Sep 17 00:00:00 2001 From: dimxy Date: Wed, 16 Oct 2024 23:02:38 +0500 Subject: [PATCH 09/24] fix review notes --- Cargo.lock | 1 + mm2src/coins/eth.rs | 1 + mm2src/coins/eth/eth_rpc.rs | 2 +- mm2src/coins/lp_coins.rs | 35 ++++------ .../mm2_main/src/ext_api/one_inch/errors.rs | 4 ++ mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 3 + mm2src/mm2_main/src/ext_api/one_inch/types.rs | 70 +++++++++++++++++-- mm2src/trading_api/Cargo.toml | 6 +- mm2src/trading_api/src/one_inch_api/errors.rs | 51 +++++++------- mm2src/trading_api/src/one_inch_api/types.rs | 58 ++++++++++++--- 10 files changed, 162 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd6f1b4518..6c5fd0f61e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7227,6 +7227,7 @@ name = "trading_api" version = "0.1.0" dependencies = [ "common", + "derive_more", "enum_derives", "ethereum-types", "lazy_static", diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 72bd1ce1b4..2422010ff9 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -853,6 +853,7 @@ impl EthCoinImpl { (*guard).clone() } + #[inline(always)] pub fn chain_id(&self) -> u64 { self.chain_id } } diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 3cd4010809..3dc6711126 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -241,7 +241,7 @@ impl EthCoin { .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } - /// Get chain id + /// Get chain id from network pub(crate) async fn network_chain_id(&self) -> Result { self.try_rpc_send("eth_chainId", vec![]) .await diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index ef1f7d0f53..83ff1ce1b1 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -5647,37 +5647,30 @@ impl HttpStatusCode for Erc20CallError { } } -type Erc20AllowanceResult = MmResult; -type Erc20ApproveResult = MmResult; - -/// Call allowance for ERC20 tokens -/// Returns BigDecimal value -pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> Erc20AllowanceResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin) - .await - .mm_err(|_| Erc20CallError::NoSuchCoin { coin: req.coin.clone() })?; - match coin { - MmCoinEnum::EthCoin(eth_coin) => { +/// Call allowance for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). +/// Returns BigDecimal value. +pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { + match lp_coinfind_or_err(&ctx, &req.coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => { let wei = eth_coin.allowance(req.spender).compat().await?; let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; Ok(amount) }, - _ => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: req.coin.clone() })), } } -/// Call approve for ERC20 coins -/// Returns signed transaction to send to the chain -pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> Erc20ApproveResult { - let coin = lp_coinfind_or_err(&ctx, &req.coin) - .await - .mm_err(|_| Erc20CallError::NoSuchCoin { coin: req.coin.clone() })?; - match coin { - MmCoinEnum::EthCoin(eth_coin) => { +/// Call approve for ERC20 coins (see https://eips.ethereum.org/EIPS/eip-20#allowance). +/// Returns signed transaction to send to the chain. +pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { + match lp_coinfind_or_err(&ctx, &req.coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => { let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; let tx = eth_coin.approve(req.spender, amount).compat().await?; Ok(tx.tx_hash_as_bytes()) }, - _ => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: req.coin.clone() })), } } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs index 1e1eebc890..ad8a79bf61 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs @@ -20,6 +20,8 @@ pub enum ApiIntegrationRpcError { #[from_stringify("coins::UnexpectedDerivationMethod")] MyAddressError(String), InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { param: String, value: String, min: String, max: String }, #[display(fmt = "allowance not enough for 1inch contract, available: {allowance}, needed: {amount}")] OneInchAllowanceNotEnough { allowance: BigDecimal, @@ -39,6 +41,7 @@ impl HttpStatusCode for ApiIntegrationRpcError { | ApiIntegrationRpcError::ChainNotSupported | ApiIntegrationRpcError::MyAddressError(_) | ApiIntegrationRpcError::InvalidParam(_) + | ApiIntegrationRpcError::OutOfBounds { .. } | ApiIntegrationRpcError::OneInchAllowanceNotEnough { .. } => StatusCode::BAD_REQUEST, ApiIntegrationRpcError::OneInchError(_) | ApiIntegrationRpcError::ApiDataError(_) => { StatusCode::BAD_GATEWAY @@ -51,6 +54,7 @@ impl ApiIntegrationRpcError { pub(crate) fn from_api_error(error: ApiClientError, decimals: u8) -> Self { match error { ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), + ApiClientError::OutOfBounds { param, value, min, max } => ApiIntegrationRpcError::OutOfBounds { param, value, min, max }, ApiClientError::HttpClientError(_) | ApiClientError::ParseBodyError(_) | ApiClientError::GeneralApiError(_) => ApiIntegrationRpcError::OneInchError(error), diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs index b860a86d84..c8a7afb605 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -82,11 +82,14 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( .with_include_protocols(Some(req.include_protocols)) .with_include_gas(Some(req.include_gas)) .with_connector_tokens(req.connector_tokens) + .with_excluded_protocols(req.excluded_protocols) .with_permit(req.permit) + .with_compatibility(req.compatibility) .with_receiver(req.receiver) .with_referrer(req.referrer) .with_disable_estimate(req.disable_estimate) .with_allow_partial_fill(req.allow_partial_fill) + .with_use_permit2(req.use_permit2) .build_query_params() .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; let swap_with_tx = ApiClient::new(ctx) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index 7bf5d61f1d..416d00aa94 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -12,69 +12,131 @@ use trading_api::one_inch_api::{self, #[derive(Clone, Debug, Deserialize)] pub struct AggregationContractRequest {} +/// Request to get quote for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/quote_params #[derive(Clone, Debug, Deserialize)] pub struct ClassicSwapQuoteRequest { + /// Base coin ticker pub base: String, + /// Rel coin ticker pub rel: String, + /// Swap amount in coins (with fraction) pub amount: MmNumber, - // Optional fields + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) #[serde(default = "true_f")] pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) #[serde(default = "true_f")] pub include_protocols: bool, + /// Include estimated gas in return value (default is true) #[serde(default = "true_f")] pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used pub connector_tokens: Option, } +/// Request to create transaction for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/swap_params #[derive(Clone, Debug, Deserialize)] pub struct ClassicSwapCreateRequest { + /// Base coin ticker pub base: String, + /// Rel coin ticker pub rel: String, + /// Swap amount in coins (with fraction) pub amount: MmNumber, + /// Allowed slippage, min: 0; max: 50 pub slippage: f32, - // Optional fields + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) #[serde(default = "true_f")] pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) #[serde(default = "true_f")] pub include_protocols: bool, + /// Include estimated gas in response (default is true) #[serde(default = "true_f")] pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used pub connector_tokens: Option, + /// Excluded supported liquidity sources. Should be the same for a quote and swap, max: 5 + pub excluded_protocols: Option, + /// Used according https://eips.ethereum.org/EIPS/eip-2612 pub permit: Option, + /// Exclude the Unoswap method + pub compatibility: Option, + /// This address will receive funds after the swap. By default same address as 'my address' pub receiver: Option, + /// Address to receive partner fee pub referrer: Option, - /// Disable gas estimation + /// if true, disable most of the checks, default: false pub disable_estimate: Option, - /// Allow the swap to be partially filled + /// if true, the algorithm can cancel part of the route, if the rate has become less attractive. + /// Unswapped tokens will return to 'my address'. Default: true pub allow_partial_fill: Option, + /// Enable this flag in case you did an approval to permit2 smart contract (default false) + pub use_permit2: Option, } +/// Response for both classic swap quote or create swap calls #[derive(Serialize, Debug)] pub struct ClassicSwapResponse { + /// Destination token amount, in coins (with fraction) pub dst_amount: MmNumber, + /// Source (base) token info #[serde(skip_serializing_if = "Option::is_none")] pub src_token: Option, + /// Destination (rel) token info #[serde(skip_serializing_if = "Option::is_none")] pub dst_token: Option, + /// Used liquidity sources #[serde(skip_serializing_if = "Option::is_none")] pub protocols: Option>>>, + /// Swap tx fields (returned only for create swap rpc) #[serde(skip_serializing_if = "Option::is_none")] pub tx: Option, + /// Estimated (returned only for quote rpc) pub gas: Option, } diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 79601880b5..46785d0316 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -12,11 +12,7 @@ mm2_err_handle = { path = "../mm2_err_handle" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } -# async-trait = "0.1" -# byteorder = "1.3.2" -# derive_more = "0.99" -# futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] } -# hw_common = { path = "../hw_common" } +derive_more = "0.99" ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } lazy_static = "1.4" serde = "1.0" diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs index 21c3650f69..0d1fae6a68 100644 --- a/mm2src/trading_api/src/one_inch_api/errors.rs +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -1,4 +1,5 @@ use common::StatusCode; +use derive_more::Display; use enum_derives::EnumFromStringify; use ethereum_types::U256; use mm2_net::transport::SlurpError; @@ -12,6 +13,16 @@ pub struct GeneralApiError { pub status_code: u16, } +impl std::fmt::Display for GeneralApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "error description: {}", + self.description.as_ref().unwrap_or(&"".to_owned()) + ) + } +} + #[derive(Debug, Serialize)] pub struct AllowanceNotEnoughError { pub error: String, @@ -23,42 +34,28 @@ pub struct AllowanceNotEnoughError { pub allowance: U256, } -#[derive(Debug, Serialize, EnumFromStringify)] +impl std::fmt::Display for AllowanceNotEnoughError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "error description: {}", + self.description.as_ref().unwrap_or(&"".to_owned()) + ) + } +} + +#[derive(Debug, Display, Serialize, EnumFromStringify)] pub enum ApiClientError { #[from_stringify("url::ParseError")] InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { param: String, value: String, min: String, max: String }, HttpClientError(SlurpError), ParseBodyError(String), GeneralApiError(GeneralApiError), AllowanceNotEnough(AllowanceNotEnoughError), } -impl std::fmt::Display for ApiClientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ApiClientError::InvalidParam(s) => { - write!(f, "error description: {}", s) - }, - ApiClientError::GeneralApiError(GeneralApiError { description, .. }) => { - write!( - f, - "error description: {}", - description.as_ref().unwrap_or(&"".to_owned()) - ) - }, - ApiClientError::AllowanceNotEnough(AllowanceNotEnoughError { description, .. }) => { - write!( - f, - "error description: {}", - description.as_ref().unwrap_or(&"".to_owned()) - ) - }, - ApiClientError::HttpClientError(err) => write!(f, "{}", err.to_string()), - ApiClientError::ParseBodyError(err) => write!(f, "{}", err.to_string()), - } - } -} - // API error meta 'type' field known values const META_TYPE_ALLOWANCE: &str = "allowance"; const META_TYPE_AMOUNT: &str = "amount"; diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index 5e963ba0ba..1c4ad11f67 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -112,14 +112,14 @@ pub struct ClassicSwapCreateParams { include_protocols: Option, include_gas: Option, connector_tokens: Option, + excluded_protocols: Option, permit: Option, - /// Funds receiver + compatibility: Option, receiver: Option, referrer: Option, - /// Disable gas estimation disable_estimate: Option, - /// Allow the swap to be partially filled allow_partial_fill: Option, + use_permit2: Option, } impl ClassicSwapCreateParams { @@ -145,11 +145,14 @@ impl ClassicSwapCreateParams { def_with_opt_param!(include_protocols, bool); def_with_opt_param!(include_gas, bool); def_with_opt_param!(connector_tokens, String); + def_with_opt_param!(excluded_protocols, String); def_with_opt_param!(permit, String); + def_with_opt_param!(compatibility, bool); def_with_opt_param!(receiver, String); def_with_opt_param!(referrer, String); def_with_opt_param!(disable_estimate, bool); def_with_opt_param!(allow_partial_fill, bool); + def_with_opt_param!(use_permit2, bool); pub fn build_query_params(&self) -> MmResult, ApiClientError> { self.validate_params()?; @@ -173,11 +176,14 @@ impl ClassicSwapCreateParams { push_if_some!(params, "includeProtocols", self.include_protocols); push_if_some!(params, "includeGas", self.include_gas); push_if_some!(params, "connectorTokens", &self.connector_tokens); + push_if_some!(params, "excludedProtocols", &self.excluded_protocols); push_if_some!(params, "permit", &self.permit); + push_if_some!(params, "compatibility", &self.compatibility); push_if_some!(params, "receiver", &self.receiver); push_if_some!(params, "referrer", &self.referrer); push_if_some!(params, "disableEstimate", self.disable_estimate); push_if_some!(params, "allowPartialFill", self.allow_partial_fill); + push_if_some!(params, "usePermit2", self.use_permit2); Ok(params) } @@ -248,7 +254,12 @@ pub struct TxFields { fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { - return Err(ApiClientError::InvalidParam("invalid slippage param".to_owned()).into()); + return Err(ApiClientError::OutOfBounds { + param: "slippage".to_owned(), + value: slippage.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_SLIPPAGE.to_string() + }.into()); } Ok(()) } @@ -256,7 +267,12 @@ fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { if let Some(fee) = fee { if !(0.0..=ONE_INCH_MAX_FEE_SHARE).contains(fee) { - return Err(ApiClientError::InvalidParam("invalid fee param".to_owned()).into()); + return Err(ApiClientError::OutOfBounds { + param: "fee".to_owned(), + value: fee.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_FEE_SHARE.to_string() + }.into()); } } Ok(()) @@ -265,7 +281,12 @@ fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> { if let Some(gas_limit) = gas_limit { if gas_limit > &ONE_INCH_MAX_GAS { - return Err(ApiClientError::InvalidParam("invalid gas param".to_owned()).into()); + return Err(ApiClientError::OutOfBounds { + param: "gas_limit".to_owned(), + value: gas_limit.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_GAS.to_string() + }.into()); } } Ok(()) @@ -274,16 +295,26 @@ fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { if let Some(parts) = parts { if parts > &ONE_INCH_MAX_PARTS { - return Err(ApiClientError::InvalidParam("invalid max parts param".to_owned()).into()); + return Err(ApiClientError::OutOfBounds { + param: "parts".to_owned(), + value: parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_PARTS.to_string() + }.into()); } } Ok(()) } fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), ApiClientError> { - if let Some(parts) = main_route_parts { - if parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { - return Err(ApiClientError::InvalidParam("invalid max main route parts param".to_owned()).into()); + if let Some(main_route_parts) = main_route_parts { + if main_route_parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "main route parts".to_owned(), + value: main_route_parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_MAIN_ROUTE_PARTS.to_string() + }.into()); } } Ok(()) @@ -292,7 +323,12 @@ fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), Api fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), ApiClientError> { if let Some(complexity_level) = complexity_level { if complexity_level > &ONE_INCH_MAX_COMPLEXITY_LEVEL { - return Err(ApiClientError::InvalidParam("invalid max complexity level param".to_owned()).into()); + return Err(ApiClientError::OutOfBounds { + param: "complexity level".to_owned(), + value: complexity_level.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_COMPLEXITY_LEVEL.to_string() + }.into()); } } Ok(()) From 7a1282de0aa30e26543d89483d4459403c0b338b Mon Sep 17 00:00:00 2001 From: dimxy Date: Thu, 17 Oct 2024 14:57:26 +0500 Subject: [PATCH 10/24] add 'deny unknown fields' for 1inch rpcs --- mm2src/mm2_main/src/ext_api/one_inch/types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index 416d00aa94..6771431c10 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -15,6 +15,7 @@ pub struct AggregationContractRequest {} /// Request to get quote for 1inch classic swap. /// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/quote_params #[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ClassicSwapQuoteRequest { /// Base coin ticker pub base: String, @@ -59,6 +60,7 @@ pub struct ClassicSwapQuoteRequest { /// Request to create transaction for 1inch classic swap. /// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/swap_params #[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ClassicSwapCreateRequest { /// Base coin ticker pub base: String, From 28a645dad0b2f5fa2fdaa0cd442a61b12645439d Mon Sep 17 00:00:00 2001 From: dimxy Date: Thu, 17 Oct 2024 18:00:28 +0500 Subject: [PATCH 11/24] return api response amount as DetailedAmount --- mm2src/mm2_main/src/ext_api/one_inch/types.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index 6771431c10..fc80341b2c 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -3,12 +3,14 @@ use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; use common::true_f; use ethereum_types::{Address, U256}; use mm2_err_handle::prelude::*; -use mm2_number::{BigDecimal, MmNumber}; +use mm2_number::{construct_detailed, BigDecimal, MmNumber}; use rpc::v1::types::Bytes as BytesJson; use serde::{Deserialize, Serialize}; use trading_api::one_inch_api::{self, types::{ProtocolInfo, TokenInfo}}; +construct_detailed!(DetailedAmount, amount); + #[derive(Clone, Debug, Deserialize)] pub struct AggregationContractRequest {} @@ -125,7 +127,7 @@ pub struct ClassicSwapCreateRequest { #[derive(Serialize, Debug)] pub struct ClassicSwapResponse { /// Destination token amount, in coins (with fraction) - pub dst_amount: MmNumber, + pub dst_amount: DetailedAmount, /// Source (base) token info #[serde(skip_serializing_if = "Option::is_none")] pub src_token: Option, @@ -148,7 +150,7 @@ impl ClassicSwapResponse { decimals: u8, ) -> MmResult { Ok(Self { - dst_amount: MmNumber::from(u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?), + dst_amount: MmNumber::from(u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?).into(), src_token: data.src_token, dst_token: data.dst_token, protocols: data.protocols, From ef93d1afc86e422bcf02e99f0929cc306e08b4c9 Mon Sep 17 00:00:00 2001 From: dimxy Date: Thu, 17 Oct 2024 18:01:41 +0500 Subject: [PATCH 12/24] refactor api errors as c-like structs --- .../mm2_main/src/ext_api/one_inch/errors.rs | 25 ++-- mm2src/mm2_main/src/ext_api/one_inch/types.rs | 23 ++-- mm2src/trading_api/src/one_inch_api/client.rs | 13 +- mm2src/trading_api/src/one_inch_api/errors.rs | 123 ++++++++---------- mm2src/trading_api/src/one_inch_api/types.rs | 78 ++++++----- 5 files changed, 132 insertions(+), 130 deletions(-) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs index ad8a79bf61..fd92723b5f 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs @@ -21,7 +21,12 @@ pub enum ApiIntegrationRpcError { MyAddressError(String), InvalidParam(String), #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] - OutOfBounds { param: String, value: String, min: String, max: String }, + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, #[display(fmt = "allowance not enough for 1inch contract, available: {allowance}, needed: {amount}")] OneInchAllowanceNotEnough { allowance: BigDecimal, @@ -54,13 +59,17 @@ impl ApiIntegrationRpcError { pub(crate) fn from_api_error(error: ApiClientError, decimals: u8) -> Self { match error { ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), - ApiClientError::OutOfBounds { param, value, min, max } => ApiIntegrationRpcError::OutOfBounds { param, value, min, max }, - ApiClientError::HttpClientError(_) - | ApiClientError::ParseBodyError(_) - | ApiClientError::GeneralApiError(_) => ApiIntegrationRpcError::OneInchError(error), - ApiClientError::AllowanceNotEnough(nested_err) => ApiIntegrationRpcError::OneInchAllowanceNotEnough { - allowance: u256_to_big_decimal(nested_err.allowance, decimals).unwrap_or_default(), - amount: u256_to_big_decimal(nested_err.amount, decimals).unwrap_or_default(), + ApiClientError::OutOfBounds { param, value, min, max } => { + ApiIntegrationRpcError::OutOfBounds { param, value, min, max } + }, + ApiClientError::TransportError(_) + | ApiClientError::ParseBodyError { .. } + | ApiClientError::GeneralApiError { .. } => ApiIntegrationRpcError::OneInchError(error), + ApiClientError::AllowanceNotEnough { allowance, amount, .. } => { + ApiIntegrationRpcError::OneInchAllowanceNotEnough { + allowance: u256_to_big_decimal(allowance, decimals).unwrap_or_default(), + amount: u256_to_big_decimal(amount, decimals).unwrap_or_default(), + } }, } } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index fc80341b2c..648f296aaa 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -25,14 +25,14 @@ pub struct ClassicSwapQuoteRequest { pub rel: String, /// Swap amount in coins (with fraction) pub amount: MmNumber, - /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. /// Should be the same for quote and swap rpc. Default is 0 pub fee: Option, - /// Specify liquidity sources + /// Specify liquidity sources /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX /// (by default - all used) pub protocols: Option, - /// Network price per gas, in Gwei for this rpc. + /// Network price per gas, in Gwei for this rpc. /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. /// If not set the 'fast' network gas price will be used pub gas_price: Option, @@ -43,7 +43,7 @@ pub struct ClassicSwapQuoteRequest { pub parts: Option, /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; pub main_route_parts: Option, - /// Maximum amount of gas for a swap. + /// Maximum amount of gas for a swap. /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 pub gas_limit: Option, /// Return fromToken and toToken info in response (default is true) @@ -72,14 +72,14 @@ pub struct ClassicSwapCreateRequest { pub amount: MmNumber, /// Allowed slippage, min: 0; max: 50 pub slippage: f32, - /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. /// Should be the same for quote and swap rpc. Default is 0 pub fee: Option, - /// Specify liquidity sources + /// Specify liquidity sources /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX /// (by default - all used) pub protocols: Option, - /// Network price per gas, in Gwei for this rpc. + /// Network price per gas, in Gwei for this rpc. /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. /// If not set the 'fast' network gas price will be used pub gas_price: Option, @@ -90,7 +90,7 @@ pub struct ClassicSwapCreateRequest { pub parts: Option, /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; pub main_route_parts: Option, - /// Maximum amount of gas for a swap. + /// Maximum amount of gas for a swap. /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 pub gas_limit: Option, /// Return fromToken and toToken info in response (default is true) @@ -112,14 +112,15 @@ pub struct ClassicSwapCreateRequest { pub compatibility: Option, /// This address will receive funds after the swap. By default same address as 'my address' pub receiver: Option, - /// Address to receive partner fee + /// Address to receive the partner fee. Must be set explicitly if fee is also set pub referrer: Option, /// if true, disable most of the checks, default: false pub disable_estimate: Option, - /// if true, the algorithm can cancel part of the route, if the rate has become less attractive. + /// if true, the algorithm can cancel part of the route, if the rate has become less attractive. /// Unswapped tokens will return to 'my address'. Default: true pub allow_partial_fill: Option, - /// Enable this flag in case you did an approval to permit2 smart contract (default false) + /// Enable this flag for auto approval by Permit2 contract if you did an approval to Uniswap Permit2 smart contract for this token. + /// Default is false pub use_permit2: Option, } diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index b33b6d9535..5982bf9474 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -133,13 +133,20 @@ impl ApiClient { pub(crate) async fn call_api(api_url: &Url) -> MmResult { let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) .await - .mm_err(ApiClientError::HttpClientError)?; - let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError(err.to_string()))?; + .mm_err(ApiClientError::TransportError)?; + let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError { + error_msg: err.to_string(), + })?; if status_code != StatusCode::OK { let error = NativeError::new(status_code, body); return Err(MmError::new(ApiClientError::from_native_error(error))); } - serde_json::from_value(body).map_err(|err| ApiClientError::ParseBodyError(err.to_string()).into()) + serde_json::from_value(body).map_err(|err| { + ApiClientError::ParseBodyError { + error_msg: err.to_string(), + } + .into() + }) } pub async fn call_swap_api( diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs index 0d1fae6a68..d92f8e144b 100644 --- a/mm2src/trading_api/src/one_inch_api/errors.rs +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -6,54 +6,37 @@ use mm2_net::transport::SlurpError; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Debug, Serialize)] -pub struct GeneralApiError { - pub error: String, - pub description: Option, - pub status_code: u16, -} - -impl std::fmt::Display for GeneralApiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "error description: {}", - self.description.as_ref().unwrap_or(&"".to_owned()) - ) - } -} - -#[derive(Debug, Serialize)] -pub struct AllowanceNotEnoughError { - pub error: String, - pub description: Option, - pub status_code: u16, - /// Amount to approve for the API contract - pub amount: U256, - /// Existing allowance for the API contract - pub allowance: U256, -} - -impl std::fmt::Display for AllowanceNotEnoughError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "error description: {}", - self.description.as_ref().unwrap_or(&"".to_owned()) - ) - } -} - #[derive(Debug, Display, Serialize, EnumFromStringify)] pub enum ApiClientError { #[from_stringify("url::ParseError")] InvalidParam(String), #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] - OutOfBounds { param: String, value: String, min: String, max: String }, - HttpClientError(SlurpError), - ParseBodyError(String), - GeneralApiError(GeneralApiError), - AllowanceNotEnough(AllowanceNotEnoughError), + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + TransportError(SlurpError), + ParseBodyError { + error_msg: String, + }, + #[display(fmt = "General API error: {error_msg} description: {description}")] + GeneralApiError { + error_msg: String, + description: String, + status_code: u16, + }, + #[display(fmt = "Allowance not enough, needed: {amount} allowance: {allowance}")] + AllowanceNotEnough { + error_msg: String, + description: String, + status_code: u16, + /// Amount to approve for the API contract + amount: U256, + /// Existing allowance for the API contract + allowance: U256, + }, } // API error meta 'type' field known values @@ -80,31 +63,27 @@ pub(crate) struct Meta { pub meta_value: String, } -#[derive(Debug)] -pub(crate) struct OtherError { - pub error: String, - pub status_code: u16, -} - #[derive(Debug)] pub(crate) enum NativeError { - Error400(Error400), - OtherError(OtherError), - ParseError(String), + HttpError { error_msg: String, status_code: u16 }, + HttpError400(Error400), + ParseError { error_msg: String }, } impl NativeError { pub(crate) fn new(status_code: StatusCode, body: Value) -> Self { if status_code == StatusCode::BAD_REQUEST { match serde_json::from_value(body) { - Ok(err) => Self::Error400(err), - Err(err) => Self::ParseError(err.to_string()), + Ok(err) => Self::HttpError400(err), + Err(err) => Self::ParseError { + error_msg: format!("could not parse error response: {}", err.to_string()), + }, } } else { - Self::OtherError(OtherError { - error: body["error"].as_str().unwrap_or_default().to_owned(), + Self::HttpError { + error_msg: body["error"].as_str().unwrap_or_default().to_owned(), status_code: status_code.into(), - }) + } } } } @@ -114,7 +93,7 @@ impl ApiClientError { /// Look for known API errors. If none found return as general API error pub(crate) fn from_native_error(api_error: NativeError) -> ApiClientError { match api_error { - NativeError::Error400(error_400) => { + NativeError::HttpError400(error_400) => { if let Some(meta) = error_400.meta { // Try if it's "Not enough allowance" error 'meta' data: if let Some(meta_allowance) = meta.iter().find(|m| m.meta_type == META_TYPE_ALLOWANCE) { @@ -125,27 +104,27 @@ impl ApiClientError { Default::default() }; let allowance = U256::from_dec_str(&meta_allowance.meta_value).unwrap_or_default(); - return ApiClientError::AllowanceNotEnough(AllowanceNotEnoughError { - error: error_400.error, + return ApiClientError::AllowanceNotEnough { + error_msg: error_400.error, status_code: error_400.status_code, - description: error_400.description, + description: error_400.description.unwrap_or_default(), amount, allowance, - }); + }; } } - ApiClientError::GeneralApiError(GeneralApiError { - error: error_400.error, + ApiClientError::GeneralApiError { + error_msg: error_400.error, status_code: error_400.status_code, - description: error_400.description, - }) + description: error_400.description.unwrap_or_default(), + } + }, + NativeError::HttpError { error_msg, status_code } => ApiClientError::GeneralApiError { + error_msg, + status_code, + description: Default::default(), }, - NativeError::OtherError(other_error) => ApiClientError::GeneralApiError(GeneralApiError { - error: other_error.error, - status_code: other_error.status_code, - description: None, - }), - NativeError::ParseError(err_str) => ApiClientError::ParseBodyError(err_str), + NativeError::ParseError { error_msg } => ApiClientError::ParseBodyError { error_msg }, } } } diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index 1c4ad11f67..12bf1f91a1 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -254,12 +254,13 @@ pub struct TxFields { fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { - return Err(ApiClientError::OutOfBounds { - param: "slippage".to_owned(), - value: slippage.to_string(), - min: 0.0.to_string(), - max: ONE_INCH_MAX_SLIPPAGE.to_string() - }.into()); + return Err(ApiClientError::OutOfBounds { + param: "slippage".to_owned(), + value: slippage.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_SLIPPAGE.to_string(), + } + .into()); } Ok(()) } @@ -267,12 +268,13 @@ fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { if let Some(fee) = fee { if !(0.0..=ONE_INCH_MAX_FEE_SHARE).contains(fee) { - return Err(ApiClientError::OutOfBounds { - param: "fee".to_owned(), - value: fee.to_string(), - min: 0.0.to_string(), - max: ONE_INCH_MAX_FEE_SHARE.to_string() - }.into()); + return Err(ApiClientError::OutOfBounds { + param: "fee".to_owned(), + value: fee.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_FEE_SHARE.to_string(), + } + .into()); } } Ok(()) @@ -281,12 +283,13 @@ fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> { if let Some(gas_limit) = gas_limit { if gas_limit > &ONE_INCH_MAX_GAS { - return Err(ApiClientError::OutOfBounds { - param: "gas_limit".to_owned(), - value: gas_limit.to_string(), - min: 0.to_string(), - max: ONE_INCH_MAX_GAS.to_string() - }.into()); + return Err(ApiClientError::OutOfBounds { + param: "gas_limit".to_owned(), + value: gas_limit.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_GAS.to_string(), + } + .into()); } } Ok(()) @@ -295,12 +298,13 @@ fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { if let Some(parts) = parts { if parts > &ONE_INCH_MAX_PARTS { - return Err(ApiClientError::OutOfBounds { - param: "parts".to_owned(), - value: parts.to_string(), - min: 0.to_string(), - max: ONE_INCH_MAX_PARTS.to_string() - }.into()); + return Err(ApiClientError::OutOfBounds { + param: "parts".to_owned(), + value: parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_PARTS.to_string(), + } + .into()); } } Ok(()) @@ -309,12 +313,13 @@ fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), ApiClientError> { if let Some(main_route_parts) = main_route_parts { if main_route_parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { - return Err(ApiClientError::OutOfBounds { - param: "main route parts".to_owned(), - value: main_route_parts.to_string(), - min: 0.to_string(), - max: ONE_INCH_MAX_MAIN_ROUTE_PARTS.to_string() - }.into()); + return Err(ApiClientError::OutOfBounds { + param: "main route parts".to_owned(), + value: main_route_parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_MAIN_ROUTE_PARTS.to_string(), + } + .into()); } } Ok(()) @@ -323,12 +328,13 @@ fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), Api fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), ApiClientError> { if let Some(complexity_level) = complexity_level { if complexity_level > &ONE_INCH_MAX_COMPLEXITY_LEVEL { - return Err(ApiClientError::OutOfBounds { - param: "complexity level".to_owned(), - value: complexity_level.to_string(), - min: 0.to_string(), - max: ONE_INCH_MAX_COMPLEXITY_LEVEL.to_string() - }.into()); + return Err(ApiClientError::OutOfBounds { + param: "complexity level".to_owned(), + value: complexity_level.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_COMPLEXITY_LEVEL.to_string(), + } + .into()); } } Ok(()) From e68cfc2d06b34c3df5e8a3abee59f905451a7719 Mon Sep 17 00:00:00 2001 From: dimxy Date: Fri, 18 Oct 2024 15:33:21 +0500 Subject: [PATCH 13/24] add two informational 1inch rpcs (to return available protocols and tokens) --- .../mm2_main/src/ext_api/one_inch/errors.rs | 6 +- mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 63 ++++++++++++++++--- mm2src/mm2_main/src/ext_api/one_inch/types.rs | 23 ++++++- .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 7 ++- mm2src/trading_api/src/one_inch_api/client.rs | 22 ++++--- mm2src/trading_api/src/one_inch_api/types.rs | 21 ++++++- 6 files changed, 119 insertions(+), 23 deletions(-) diff --git a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs index fd92723b5f..421525cff7 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/errors.rs @@ -56,7 +56,7 @@ impl HttpStatusCode for ApiIntegrationRpcError { } impl ApiIntegrationRpcError { - pub(crate) fn from_api_error(error: ApiClientError, decimals: u8) -> Self { + pub(crate) fn from_api_error(error: ApiClientError, decimals: Option) -> Self { match error { ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), ApiClientError::OutOfBounds { param, value, min, max } => { @@ -67,8 +67,8 @@ impl ApiIntegrationRpcError { | ApiClientError::GeneralApiError { .. } => ApiIntegrationRpcError::OneInchError(error), ApiClientError::AllowanceNotEnough { allowance, amount, .. } => { ApiIntegrationRpcError::OneInchAllowanceNotEnough { - allowance: u256_to_big_decimal(allowance, decimals).unwrap_or_default(), - amount: u256_to_big_decimal(amount, decimals).unwrap_or_default(), + allowance: u256_to_big_decimal(allowance, decimals.unwrap_or_default()).unwrap_or_default(), + amount: u256_to_big_decimal(amount, decimals.unwrap_or_default()).unwrap_or_default(), } }, } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs index c8a7afb605..789dcbf0ea 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -1,11 +1,14 @@ use super::errors::ApiIntegrationRpcError; -use super::types::{AggregationContractRequest, ClassicSwapCreateRequest, ClassicSwapQuoteRequest, ClassicSwapResponse}; +use super::types::{AggregationContractRequest, ClassicSwapCreateRequest, ClassicSwapLiquiditySourcesRequest, + ClassicSwapLiquiditySourcesResponse, ClassicSwapQuoteRequest, ClassicSwapResponse, + ClassicSwapTokensRequest, ClassicSwapTokensResponse}; use coins::eth::{display_eth_address, wei_from_big_decimal, EthCoin, EthCoinType}; use coins::{lp_coinfind_or_err, CoinWithDerivationMethod, MmCoin, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use trading_api::one_inch_api::client::ApiClient; -use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams}; +use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams, ProtocolsResponse, + TokensResponse}; /// "1inch_v6_0_classic_swap_contract" rpc impl /// used to get contract address (for e.g. to approve funds) @@ -39,10 +42,14 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( .with_include_gas(Some(req.include_gas)) .with_connector_tokens(req.connector_tokens) .build_query_params() - .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; let quote = ApiClient::new(ctx) - .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))? - .call_swap_api(base.chain_id(), ApiClient::get_quote_method().to_owned(), query_params) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_quote_method().to_owned(), + Some(query_params), + ) .await .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin ClassicSwapResponse::from_api_value(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin @@ -91,16 +98,52 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( .with_allow_partial_fill(req.allow_partial_fill) .with_use_permit2(req.use_permit2) .build_query_params() - .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; let swap_with_tx = ApiClient::new(ctx) - .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))? - .call_swap_api(base.chain_id(), ApiClient::get_swap_method().to_owned(), query_params) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_swap_method().to_owned(), + Some(query_params), + ) .await - .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin - ClassicSwapResponse::from_api_value(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) } +/// "1inch_v6_0_classic_swap_liquidity_sources" rpc implementation. +/// Returns list of DEX available for routing with the 1inch Aggregation contract +pub async fn one_inch_v6_0_classic_swap_liquidity_sources_rpc( + ctx: MmArc, + req: ClassicSwapLiquiditySourcesRequest, +) -> MmResult { + let response: ProtocolsResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_liquidity_sources_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapLiquiditySourcesResponse { + protocols: response.protocols, + }) +} + +/// "1inch_classic_swap_tokens" rpc implementation. +/// Returns list of tokens available for 1inch classic swaps +pub async fn one_inch_v6_0_classic_swap_tokens_rpc( + ctx: MmArc, + req: ClassicSwapTokensRequest, +) -> MmResult { + let response: TokensResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_tokens_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapTokensResponse { + tokens: response.tokens, + }) +} + async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, String), ApiIntegrationRpcError> { let coin = match lp_coinfind_or_err(ctx, ticker).await? { MmCoinEnum::EthCoin(coin) => coin, diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index 648f296aaa..dee703a51a 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use crate::ext_api::one_inch::errors::FromApiValueError; use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; use common::true_f; @@ -7,7 +8,7 @@ use mm2_number::{construct_detailed, BigDecimal, MmNumber}; use rpc::v1::types::Bytes as BytesJson; use serde::{Deserialize, Serialize}; use trading_api::one_inch_api::{self, - types::{ProtocolInfo, TokenInfo}}; + types::{ProtocolImage, ProtocolInfo, TokenInfo}}; construct_detailed!(DetailedAmount, amount); @@ -187,3 +188,23 @@ impl TxFields { }) } } + +#[derive(Deserialize)] +pub struct ClassicSwapLiquiditySourcesRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapLiquiditySourcesResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct ClassicSwapTokensRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapTokensResponse { + pub tokens: HashMap, +} diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index b34541cfb5..36e3cd27f8 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,6 +1,7 @@ use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; use crate::ext_api::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, - one_inch_v6_0_classic_swap_quote_rpc}; + one_inch_v6_0_classic_swap_liquidity_sources_rpc, + one_inch_v6_0_classic_swap_quote_rpc, one_inch_v6_0_classic_swap_tokens_rpc}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; #[cfg(target_arch = "wasm32")] @@ -224,6 +225,10 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_contract_rpc).await, "1inch_v6_0_classic_swap_quote" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_quote_rpc).await, "1inch_v6_0_classic_swap_create" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_create_rpc).await, + "1inch_v6_0_classic_swap_liquidity_sources" => { + handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_liquidity_sources_rpc).await + }, + "1inch_v6_0_classic_swap_tokens" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index 5982bf9474..016caec93a 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -11,11 +11,13 @@ use mm2_net::transport::slurp_url_with_headers; use crate::one_inch_api::errors::NativeError; -use super::{errors::ApiClientError, types::ClassicSwapData}; +use super::errors::ApiClientError; const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; const SWAP_METHOD: &str = "swap"; const QUOTE_METHOD: &str = "quote"; +const LIQUIDITY_SOURCES_METHOD: &str = "liquidity-sources"; +const TOKENS_METHOD: &str = "tokens"; const ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0: &str = "0x111111125421ca6dc452d289314280a0f8842a65"; const ONE_INCH_ETH_SPECIAL_CONTRACT: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; @@ -130,6 +132,10 @@ impl ApiClient { pub const fn get_quote_method() -> &'static str { QUOTE_METHOD } + pub const fn get_liquidity_sources_method() -> &'static str { LIQUIDITY_SOURCES_METHOD } + + pub const fn get_tokens_method() -> &'static str { TOKENS_METHOD } + pub(crate) async fn call_api(api_url: &Url) -> MmResult { let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) .await @@ -149,15 +155,17 @@ impl ApiClient { }) } - pub async fn call_swap_api( + pub async fn call_swap_api( &self, chain_id: u64, method: String, - params: QueryParams<'_>, - ) -> MmResult { - let api_url = UrlBuilder::new(self, chain_id, method) - .with_query_params(params) - .build()?; + params: Option>, + ) -> MmResult { + let mut builder = UrlBuilder::new(self, chain_id, method); + if let Some(params) = params { + builder.with_query_params(params); + } + let api_url = builder.build()?; Self::call_api(&api_url).await } diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index 12bf1f91a1..7e7102a632 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -1,5 +1,6 @@ #![allow(clippy::result_large_err)] +use std::collections::HashMap; use super::client::QueryParams; use super::errors::ApiClientError; use common::{def_with_opt_param, push_if_some}; @@ -207,7 +208,7 @@ pub struct TokenInfo { pub name: String, pub decimals: u32, pub eip2612: bool, - #[serde(rename = "isFoT")] + #[serde(rename = "isFoT", default)] pub is_fot: bool, #[serde(rename = "logoURI")] pub logo_uri: String, @@ -252,6 +253,24 @@ pub struct TxFields { pub gas: u128, } +#[derive(Deserialize, Serialize)] +pub struct ProtocolImage { + pub id: String, + pub title: String, + pub img: String, + pub img_color: String, +} + +#[derive(Deserialize)] +pub struct ProtocolsResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct TokensResponse { + pub tokens: HashMap, +} + fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { return Err(ApiClientError::OutOfBounds { From 5ab3b19e74806ee8368208dd1a0f8e5f0a363c40 Mon Sep 17 00:00:00 2001 From: dimxy Date: Fri, 18 Oct 2024 16:00:27 +0500 Subject: [PATCH 14/24] more review notes --- mm2src/coins/lp_coins.rs | 42 +++++++++---------- mm2src/common/common.rs | 2 + mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 4 +- mm2src/mm2_main/src/ext_api/one_inch/types.rs | 11 +++-- mm2src/trading_api/src/one_inch_api/types.rs | 2 +- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 83ff1ce1b1..b017c1d65c 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -5647,30 +5647,30 @@ impl HttpStatusCode for Erc20CallError { } } -/// Call allowance for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). -/// Returns BigDecimal value. +/// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). +/// Returns BigDecimal allowance value. pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { - match lp_coinfind_or_err(&ctx, &req.coin).await { - Ok(MmCoinEnum::EthCoin(eth_coin)) => { - let wei = eth_coin.allowance(req.spender).compat().await?; - let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; - Ok(amount) - }, - Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), - Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: req.coin.clone() })), - } + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let wei = eth_coin.allowance(req.spender).compat().await?; + let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; + Ok(amount) } -/// Call approve for ERC20 coins (see https://eips.ethereum.org/EIPS/eip-20#allowance). -/// Returns signed transaction to send to the chain. +/// Call approve method for ERC20 coins (see https://eips.ethereum.org/EIPS/eip-20#allowance). +/// Returns approval transaction hash. pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { - match lp_coinfind_or_err(&ctx, &req.coin).await { - Ok(MmCoinEnum::EthCoin(eth_coin)) => { - let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; - let tx = eth_coin.approve(req.spender, amount).compat().await?; - Ok(tx.tx_hash_as_bytes()) - }, - Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { coin: req.coin })), - Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: req.coin.clone() })), + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; + let tx = eth_coin.approve(req.spender, amount).compat().await?; + Ok(tx.tx_hash_as_bytes()) +} + +async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { + match lp_coinfind_or_err(ctx, coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { + coin: coin.to_string(), + })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: coin.to_string() })), } } diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 019e06274c..431cff3205 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -1136,6 +1136,7 @@ macro_rules! str_strip_0x { }; } +/// If value is 'some' push key and value (as string) into an array containing (key, value) elements #[macro_export] macro_rules! push_if_some { ($arr: expr, $k: expr, $v: expr) => { @@ -1145,6 +1146,7 @@ macro_rules! push_if_some { }; } +/// Define 'with_...' method to set a parameter with an optional value in a builder #[macro_export] macro_rules! def_with_opt_param { ($var: ident, $var_type: ty) => { diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs index 789dcbf0ea..9fad7cbb20 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -51,8 +51,8 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( Some(query_params), ) .await - .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, base.decimals()))?; // use 'base' as amount in errors is in the src coin - ClassicSwapResponse::from_api_value(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/ext_api/one_inch/types.rs index dee703a51a..f14f513328 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/types.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use crate::ext_api::one_inch::errors::FromApiValueError; use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; use common::true_f; @@ -7,6 +6,7 @@ use mm2_err_handle::prelude::*; use mm2_number::{construct_detailed, BigDecimal, MmNumber}; use rpc::v1::types::Bytes as BytesJson; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use trading_api::one_inch_api::{self, types::{ProtocolImage, ProtocolInfo, TokenInfo}}; @@ -147,7 +147,7 @@ pub struct ClassicSwapResponse { } impl ClassicSwapResponse { - pub(crate) fn from_api_value( + pub(crate) fn from_api_classic_swap_data( data: one_inch_api::types::ClassicSwapData, decimals: u8, ) -> MmResult { @@ -156,7 +156,10 @@ impl ClassicSwapResponse { src_token: data.src_token, dst_token: data.dst_token, protocols: data.protocols, - tx: data.tx.map(|tx| TxFields::from_api_value(tx, decimals)).transpose()?, + tx: data + .tx + .map(|tx| TxFields::from_api_tx_fields(tx, decimals)) + .transpose()?, gas: data.gas, }) } @@ -174,7 +177,7 @@ pub struct TxFields { } impl TxFields { - pub(crate) fn from_api_value( + pub(crate) fn from_api_tx_fields( tx_fields: one_inch_api::types::TxFields, decimals: u8, ) -> MmResult { diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index 7e7102a632..429a09482f 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -1,12 +1,12 @@ #![allow(clippy::result_large_err)] -use std::collections::HashMap; use super::client::QueryParams; use super::errors::ApiClientError; use common::{def_with_opt_param, push_if_some}; use ethereum_types::Address; use mm2_err_handle::mm_error::MmResult; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; From e040bcaf7c14ff7521c9ecc8e14a735d255d74cd Mon Sep 17 00:00:00 2001 From: dimxy Date: Fri, 18 Oct 2024 16:03:30 +0500 Subject: [PATCH 15/24] add doc for strip 0x macro --- mm2src/common/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 431cff3205..763e0f365c 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -1129,6 +1129,7 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// If 0x prefix exists in an str strip it or return the str as-is #[macro_export] macro_rules! str_strip_0x { ($s: expr) => { From a19063aa65e7d43f2068a9c98c82d77439716f4d Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 19 Oct 2024 17:13:10 +0500 Subject: [PATCH 16/24] add test for 1inch json deserialisation --- Cargo.lock | 1 + mm2src/mm2_main/Cargo.toml | 1 + mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs | 270 ++++++++++++++++++ mm2src/trading_api/Cargo.toml | 4 + mm2src/trading_api/src/one_inch_api/client.rs | 22 +- 5 files changed, 288 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c5fd0f61e..e1fbc7f77d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7235,6 +7235,7 @@ dependencies = [ "mm2_err_handle", "mm2_net", "mm2_number", + "mocktopus", "serde", "serde_derive", "serde_json", diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 6eff9e9923..a47bc0b433 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -123,6 +123,7 @@ winapi = "0.3" coins = { path = "../coins", features = ["for-tests"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } +trading_api = { path = "../trading_api", features = ["mocktopus"] } mocktopus = "0.8.0" testcontainers = "0.15.0" web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs index 9fad7cbb20..9b323b38c7 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs @@ -165,3 +165,273 @@ fn api_supports_coin(coin: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { Err(MmError::new(ApiIntegrationRpcError::ChainNotSupported)) } } + +#[cfg(test)] +mod tests { + use crate::ext_api::one_inch::{rpcs::{one_inch_v6_0_classic_swap_create_rpc, one_inch_v6_0_classic_swap_quote_rpc}, + types::{ClassicSwapCreateRequest, ClassicSwapQuoteRequest}}; + use coins::eth::EthCoin; + use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; + use common::block_on; + use crypto::CryptoCtx; + use mm2_core::mm_ctx::MmCtxBuilder; + use mm2_number::{BigDecimal, MmNumber}; + use mocktopus::mocking::{MockResult, Mockable}; + use std::str::FromStr; + use trading_api::one_inch_api::{client::ApiClient, types::ClassicSwapData}; + + #[test] + fn test_classic_swap_response_conversion() { + let ticker_coin = "ETH".to_owned(); + let ticker_token = "JST".to_owned(); + let eth_conf = json!({ + "coin": ticker_coin, + "name": "ethereum", + "derivation_path": "m/44'/1'", + "chain_id": 1, + "protocol": { + "type": "ETH" + }, + "trezor_coin": "Ethereum" + }); + let jst_conf = json!({ + "coin": ticker_token, + "name": "jst", + "chain_id": 1, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19" + } + }, + }); + + let conf = json!({ + "coins": [eth_conf, jst_conf], + "1inch_api": "https://api.1inch.dev" + }); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123").unwrap(); + + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + serde_json::from_value(json!({ + "ticker": ticker_coin, + "rpc_mode": "Default", + "nodes": [ + {"url": "https://rpc2.sepolia.org"}, + {"url": "https://rpc.sepolia.org/"} + ], + "swap_contract_address": "0xeA6D65434A15377081495a9E7C5893543E7c32cB", + "erc20_tokens_requests": [{"ticker": ticker_token}], + "priv_key_policy": "ContextPrivKey" + })) + .unwrap(), + )) + .unwrap(); + + let response_quote_raw = json!({ + "dstAmount": "13", + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Test just token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://example.org/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:JSTT", + "PEG:JST", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "SUSHI", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9" + } + ], + [ + { + "name": "ONE_INCH_LIMIT_ORDER_V3", + "part": 100, + "fromTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ], + "gas": 452704 + }); + + let response_create_raw = json!({ + "dstAmount": "13", + "tx": { + "from": "0x590559f6fb7720f24ff3e2fccf6015b466e9c92c", + "to": "0x111111125421ca6dc452d289314280a0f8842a65", + "data": "0x07ed23790000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000590559f6fb7720f24ff3e2fccf6015b466e9c92c0000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000648e8755f7ac30b5e4fa3f9c00e2cb6667501797b8bc01a7a367a4b2889ca6a05d9c31a31a781c12a4c3bdfc2ef1e02942e388b6565989ebe860bd67925bda74fbe0000000000000000000000000000000000000000000000000005ea0005bc00a007e5c0d200000000000000000000000000000000059800057e00018500009500001a4041c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0e30db00c20c02aaa39b223fe8d0a0e5c4f27ead9083c756cc27b73644935b8e68019ac6356c40661e1bc3158606ae4071118002dc6c07b73644935b8e68019ac6356c40661e1bc3158600000000000000000000000000000000000000000000000000294932ccadc9c58c02aaa39b223fe8d0a0e5c4f27ead9083c756cc251204dff5675ecff96b565ba3804dd4a63799ccba406761d38e5ddf6ccf6cf7c55759d5210750b5d60f30044e331d039000000000000000000000000761d38e5ddf6ccf6cf7c55759d5210750b5d60f3000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002f8a744a79be00000000000000000000000042f527f50f16a103b6ccab48bccca214500c10210000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec00a0860a32ec00000000000000000000000000000000000000000000000000003005635d54300003d05120ead050515e10fdb3540ccd6f8236c46790508a76111111111117dc0aa78b770fa6a738034120c30200c4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000022b1a53ac4be63cdc1f47c99572290eff1edd8020000000000000000000000006a32cc044dd6359c27bb66e7b02dce6dd0fda2470000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003005635d5430000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000067138e8c00000000000000000000000000000000000000000000000000030fb9b1525d8185f8d63fbcbe42e5999263c349cb5d81000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae8046400000000000000000000000060cba82ddbf4b5ddcd4398cdd05354c6a790c309000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041d26038ef66344af785ff342b86db3da06c4cc6a62f0ca80ffd78affc0a95ccad44e814acebb1deda729bbfe3050bec14a47af487cc1cadc75f43db2d073016c31c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a66cd52a747c5f60b9db637ffe30d0e413ec87858101832b4c5c1ae154bf247f3717c8ed4133e276ddf68d43a827f280863c91d6c42bc6ad1ec7083b2315b6fd1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020d6bdbf78dac17f958d2ee523a2206206994597c13d831ec780a06c4eca27dac17f958d2ee523a2206206994597c13d831ec7111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000c095c0a2", + "value": "10000000", + "gas": 721429, + "gasPrice": "9525172167" + }, + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Just Token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:USDT", + "PEG:USD", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "UNISWAP_V2", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3" + } + ], + [ + { + "name": "ONE_INCH_LP_1_1", + "part": 100, + "fromTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3", + "toTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302" + } + ], + [ + { + "name": "PMM11", + "part": 100, + "fromTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ] + }); + + let quote_req = ClassicSwapQuoteRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + }; + + let create_req = ClassicSwapCreateRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + slippage: 0.0, + excluded_protocols: None, + permit: None, + compatibility: None, + receiver: None, + referrer: None, + disable_estimate: None, + allow_partial_fill: None, + use_permit2: None, + }; + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_quote_raw = response_quote_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_quote_raw).unwrap()) + })) + }); + + let quote_response = block_on(one_inch_v6_0_classic_swap_quote_rpc(ctx.clone(), quote_req)).unwrap(); + assert_eq!( + quote_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(quote_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(quote_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(quote_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(quote_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(quote_response.gas.unwrap(), 452704_u128); + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_create_raw = response_create_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_create_raw).unwrap()) + })) + }); + let create_response = block_on(one_inch_v6_0_classic_swap_create_rpc(ctx, create_req)).unwrap(); + assert_eq!( + create_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(create_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(create_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(create_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(create_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(create_response.tx.as_ref().unwrap().data.len(), 1960); + } +} diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 46785d0316..5dfde994da 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -11,6 +11,7 @@ mm2_core = { path = "../mm2_core" } mm2_err_handle = { path = "../mm2_err_handle" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } +mocktopus = { version = "0.8.0", optional = true } derive_more = "0.99" ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } @@ -19,3 +20,6 @@ serde = "1.0" serde_derive = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } url = { version = "2.2.2", features = ["serde"] } + +[dev-dependencies] +mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index 016caec93a..cd8f54a880 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -1,17 +1,17 @@ +use super::errors::ApiClientError; +use crate::one_inch_api::errors::NativeError; use common::StatusCode; use lazy_static::lazy_static; +use mm2_core::mm_ctx::MmArc; use mm2_err_handle::{map_mm_error::MapMmError, map_to_mm::MapToMmResult, mm_error::{MmError, MmResult}}; +use mm2_net::transport::slurp_url_with_headers; use serde::de::DeserializeOwned; use url::Url; -use mm2_core::mm_ctx::MmArc; -use mm2_net::transport::slurp_url_with_headers; - -use crate::one_inch_api::errors::NativeError; - -use super::errors::ApiClientError; +#[cfg(any(test, feature = "mocktopus"))] +use mocktopus::macros::*; const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; const SWAP_METHOD: &str = "swap"; @@ -94,6 +94,8 @@ pub struct ApiClient { base_url: Url, } +#[allow(clippy::swap_ptr_to_ref)] // need for moctopus +#[cfg_attr(any(test, feature = "mocktopus"), mockable)] impl ApiClient { #[allow(unused_variables)] #[allow(clippy::result_large_err)] @@ -137,7 +139,7 @@ impl ApiClient { pub const fn get_tokens_method() -> &'static str { TOKENS_METHOD } pub(crate) async fn call_api(api_url: &Url) -> MmResult { - let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), Self::get_headers()) + let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), ApiClient::get_headers()) .await .mm_err(ApiClientError::TransportError)?; let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError { @@ -155,11 +157,11 @@ impl ApiClient { }) } - pub async fn call_swap_api( + pub async fn call_swap_api<'l, T: DeserializeOwned>( &self, chain_id: u64, method: String, - params: Option>, + params: Option>, ) -> MmResult { let mut builder = UrlBuilder::new(self, chain_id, method); if let Some(params) = params { @@ -167,6 +169,6 @@ impl ApiClient { } let api_url = builder.build()?; - Self::call_api(&api_url).await + ApiClient::call_api(&api_url).await } } From d457beeda5ebfbc9f00fdfe6a9fa0c5b1d84c3cf Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 26 Oct 2024 18:08:49 +0500 Subject: [PATCH 17/24] add 1inch response web link validation --- mm2src/trading_api/src/one_inch_api/types.rs | 55 +++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs index 429a09482f..f13e943768 100644 --- a/mm2src/trading_api/src/one_inch_api/types.rs +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -4,9 +4,10 @@ use super::client::QueryParams; use super::errors::ApiClientError; use common::{def_with_opt_param, push_if_some}; use ethereum_types::Address; -use mm2_err_handle::mm_error::MmResult; +use mm2_err_handle::mm_error::{MmError, MmResult}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use url::Url; const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; @@ -15,6 +16,9 @@ const ONE_INCH_MAX_PARTS: u32 = 100; const ONE_INCH_MAX_MAIN_ROUTE_PARTS: u32 = 50; const ONE_INCH_MAX_COMPLEXITY_LEVEL: u32 = 3; +const BAD_URL_IN_RESPONSE_ERROR: &str = "unsupported url in response"; +const ONE_INCH_DOMAIN: &str = "1inch.io"; + /// API params builder for swap quote #[derive(Default)] pub struct ClassicSwapQuoteParams { @@ -210,7 +214,7 @@ pub struct TokenInfo { pub eip2612: bool, #[serde(rename = "isFoT", default)] pub is_fot: bool, - #[serde(rename = "logoURI")] + #[serde(rename = "logoURI", with = "serde_one_inch_link")] pub logo_uri: String, pub tags: Vec, } @@ -257,7 +261,9 @@ pub struct TxFields { pub struct ProtocolImage { pub id: String, pub title: String, + #[serde(with = "serde_one_inch_link")] pub img: String, + #[serde(with = "serde_one_inch_link")] pub img_color: String, } @@ -271,6 +277,28 @@ pub struct TokensResponse { pub tokens: HashMap, } +mod serde_one_inch_link { + use super::validate_one_inch_link; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Just forward to the normal serializer + pub(super) fn serialize(s: &String, serializer: S) -> Result + where + S: Serializer, + { + s.serialize(serializer) + } + + /// Deserialise String with checking links + pub(super) fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + ::deserialize(deserializer) + .map(|value| validate_one_inch_link(&value).unwrap_or_default()) + } +} + fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { return Err(ApiClientError::OutOfBounds { @@ -358,3 +386,26 @@ fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), Api } Ok(()) } + +/// Check if url is valid and is a subdomain of 1inch domain (simple anti-phishing check) +fn validate_one_inch_link(s: &str) -> MmResult { + let url = Url::parse(s).map_err(|_err| ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + })?; + if let Some(host) = url.host() { + if host.to_string().ends_with(ONE_INCH_DOMAIN) { + return Ok(s.to_owned()); + } + } + MmError::err(ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + }) +} + +#[test] +fn test_validate_one_inch_link() { + assert!(validate_one_inch_link("https://cdn.1inch.io/liquidity-sources-logo/wmatic_color.png").is_ok()); + assert!(validate_one_inch_link("https://example.org/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("https://inch.io/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("127.0.0.1").is_err()); +} From 6a150a568f76c5d3f97c71105594849191149158 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 26 Oct 2024 19:20:01 +0500 Subject: [PATCH 18/24] move approve allowance rpcs to mm2_main crate --- mm2src/coins/eth.rs | 4 +- mm2src/coins/lp_coins.rs | 78 +------------------ .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 9 ++- .../src/rpc/lp_commands/lp_commands.rs | 2 + 4 files changed, 12 insertions(+), 81 deletions(-) diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 2422010ff9..e7540b3760 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -4528,7 +4528,7 @@ impl EthCoin { self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } - pub(crate) fn allowance(&self, spender: Address) -> Web3RpcFut { + pub fn allowance(&self, spender: Address) -> Web3RpcFut { let coin = self.clone(); let fut = async move { match coin.coin_type { @@ -4593,7 +4593,7 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - pub(crate) fn approve(&self, spender: Address, amount: U256) -> EthTxFut { + pub fn approve(&self, spender: Address, amount: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index b017c1d65c..a4e042094b 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -41,7 +41,6 @@ #[macro_use] extern crate serde_json; #[macro_use] extern crate ser_error_derive; -use crate::eth::Web3RpcError; use async_trait::async_trait; use base58::FromBase58Error; use bip32::ExtendedPrivateKey; @@ -219,9 +218,9 @@ pub mod coins_tests; pub mod eth; use eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; -use eth::{eth_coin_from_conf_and_request, get_eth_address, u256_to_big_decimal, wei_from_big_decimal, EthCoin, - EthGasDetailsErr, EthTxFeeDetails, GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; -use ethereum_types::{Address as EthAddress, U256}; +use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, + GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; +use ethereum_types::U256; pub mod hd_wallet; use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, @@ -5603,74 +5602,3 @@ pub mod for_tests { } } } - -#[derive(Debug, Deserialize)] -pub struct Erc20ApproveRequest { - coin: String, - spender: EthAddress, - amount: BigDecimal, -} - -#[derive(Debug, Deserialize)] -pub struct Erc20AllowanceRequest { - coin: String, - spender: EthAddress, -} - -#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum Erc20CallError { - #[display(fmt = "No such coin {}", coin)] - NoSuchCoin { coin: String }, - #[display(fmt = "Coin not supported {}", coin)] - CoinNotSupported { coin: String }, - #[from_stringify("NumConversError")] - #[display(fmt = "Invalid param: {}", _0)] - InvalidParam(String), - #[from_stringify("TransactionErr")] - #[display(fmt = "Transaction error {}", _0)] - TransactionError(String), - #[from_stringify("Web3RpcError")] - #[display(fmt = "Web3 RPC error {}", _0)] - Web3RpcError(String), -} - -impl HttpStatusCode for Erc20CallError { - fn status_code(&self) -> StatusCode { - match self { - Erc20CallError::NoSuchCoin { .. } - | Erc20CallError::CoinNotSupported { .. } - | Erc20CallError::InvalidParam(_) - | Erc20CallError::TransactionError(_) - | Erc20CallError::Web3RpcError(_) => StatusCode::BAD_REQUEST, - } - } -} - -/// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). -/// Returns BigDecimal allowance value. -pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { - let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; - let wei = eth_coin.allowance(req.spender).compat().await?; - let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; - Ok(amount) -} - -/// Call approve method for ERC20 coins (see https://eips.ethereum.org/EIPS/eip-20#allowance). -/// Returns approval transaction hash. -pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { - let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; - let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; - let tx = eth_coin.approve(req.spender, amount).compat().await?; - Ok(tx.tx_hash_as_bytes()) -} - -async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { - match lp_coinfind_or_err(ctx, coin).await { - Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), - Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { - coin: coin.to_string(), - })), - Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: coin.to_string() })), - } -} diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 36e3cd27f8..74f82702a6 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -14,7 +14,8 @@ use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, stop_version_stat_collection, update_version_stat_collection}, lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}, - rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; + rpc::lp_commands::{eth::{allowance_rpc, approve_rpc}, + get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; @@ -39,9 +40,9 @@ use coins::utxo::qtum::QtumCoin; use coins::utxo::slp::SlpToken; use coins::utxo::utxo_standard::UtxoStandardCoin; use coins::z_coin::ZCoin; -use coins::{add_delegation, allowance_rpc, approve_rpc, get_my_address, get_raw_transaction, get_staking_infos, - get_swap_transaction_fee_policy, nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, - sign_raw_transaction, verify_message, withdraw}; +use coins::{add_delegation, get_my_address, get_raw_transaction, get_staking_infos, get_swap_transaction_fee_policy, + nft, remove_delegation, set_swap_transaction_fee_policy, sign_message, sign_raw_transaction, + verify_message, withdraw}; use coins_activation::{cancel_init_l2, cancel_init_platform_coin_with_tokens, cancel_init_standalone_coin, cancel_init_token, enable_platform_coin_with_tokens, enable_token, init_l2, init_l2_status, init_l2_user_action, init_platform_coin_with_tokens, init_platform_coin_with_tokens_status, diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs index ae992c6d3e..7c5363638f 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs @@ -7,6 +7,8 @@ use mm2_err_handle::prelude::*; use rpc::v1::types::H160 as H160Json; use serde_json::Value as Json; +pub mod eth; + pub type GetPublicKeyRpcResult = Result>; pub type GetSharedDbIdResult = Result>; pub type GetSharedDbIdError = GetPublicKeyError; From c175191837da04ef66922ccc3285a8da87dc3549 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 26 Oct 2024 19:32:33 +0500 Subject: [PATCH 19/24] add lp_command/rpc.rs source file --- mm2src/mm2_main/src/rpc/lp_commands/eth.rs | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 mm2src/mm2_main/src/rpc/lp_commands/eth.rs diff --git a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs b/mm2src/mm2_main/src/rpc/lp_commands/eth.rs new file mode 100644 index 0000000000..070799902b --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/eth.rs @@ -0,0 +1,85 @@ +//! This source file is for RPCs specific for EVM platform + +use coins::eth::{u256_to_big_decimal, wei_from_big_decimal, EthCoin, Web3RpcError}; +use coins::{lp_coinfind_or_err, MmCoin, MmCoinEnum, NumConversError, Transaction, TransactionErr}; +use common::HttpStatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::Address as EthAddress; +use futures::compat::Future01CompatExt; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{mm_error::MmError, prelude::MmResult}; +use mm2_number::BigDecimal; +use rpc::v1::types::Bytes as BytesJson; + +#[derive(Debug, Deserialize)] +pub struct Erc20ApproveRequest { + coin: String, + spender: EthAddress, + amount: BigDecimal, +} + +#[derive(Debug, Deserialize)] +pub struct Erc20AllowanceRequest { + coin: String, + spender: EthAddress, +} + +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum Erc20CallError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Coin not supported {}", coin)] + CoinNotSupported { coin: String }, + #[from_stringify("NumConversError")] + #[display(fmt = "Invalid param: {}", _0)] + InvalidParam(String), + #[from_stringify("TransactionErr")] + #[display(fmt = "Transaction error {}", _0)] + TransactionError(String), + #[from_stringify("Web3RpcError")] + #[display(fmt = "Web3 RPC error {}", _0)] + Web3RpcError(String), +} + +impl HttpStatusCode for Erc20CallError { + fn status_code(&self) -> StatusCode { + match self { + Erc20CallError::NoSuchCoin { .. } + | Erc20CallError::CoinNotSupported { .. } + | Erc20CallError::InvalidParam(_) + | Erc20CallError::TransactionError(_) + | Erc20CallError::Web3RpcError(_) => StatusCode::BAD_REQUEST, + } + } +} + +/// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). +/// Returns BigDecimal allowance value. +pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let wei = eth_coin.allowance(req.spender).compat().await?; + let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; + Ok(amount) +} + +/// Call approve method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#allowance). +/// Returns approval transaction hash. +pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; + let tx = eth_coin.approve(req.spender, amount).compat().await?; + Ok(tx.tx_hash_as_bytes()) +} + +async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { + match lp_coinfind_or_err(ctx, coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { + coin: coin.to_string(), + })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: coin.to_string() })), + } +} From 257d4b90c90b6f17426762f790ea70ee56bd7427 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 26 Oct 2024 19:45:16 +0500 Subject: [PATCH 20/24] changed StatusCode for approve rpc errors --- mm2src/mm2_main/src/rpc/lp_commands/eth.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs b/mm2src/mm2_main/src/rpc/lp_commands/eth.rs index 070799902b..b0b428b4ee 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/eth.rs @@ -49,9 +49,9 @@ impl HttpStatusCode for Erc20CallError { match self { Erc20CallError::NoSuchCoin { .. } | Erc20CallError::CoinNotSupported { .. } - | Erc20CallError::InvalidParam(_) - | Erc20CallError::TransactionError(_) - | Erc20CallError::Web3RpcError(_) => StatusCode::BAD_REQUEST, + | Erc20CallError::InvalidParam(_) => StatusCode::BAD_REQUEST, + Erc20CallError::TransactionError(_) + | Erc20CallError::Web3RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } From ce96d878e261239a1cb649a37fb2b102713a72a4 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 26 Oct 2024 19:56:01 +0500 Subject: [PATCH 21/24] fix cargo fmt --- mm2src/mm2_main/src/rpc/lp_commands/eth.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs b/mm2src/mm2_main/src/rpc/lp_commands/eth.rs index b0b428b4ee..77a0835bdc 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/eth.rs @@ -50,8 +50,7 @@ impl HttpStatusCode for Erc20CallError { Erc20CallError::NoSuchCoin { .. } | Erc20CallError::CoinNotSupported { .. } | Erc20CallError::InvalidParam(_) => StatusCode::BAD_REQUEST, - Erc20CallError::TransactionError(_) - | Erc20CallError::Web3RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, + Erc20CallError::TransactionError(_) | Erc20CallError::Web3RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } From 65c4853fd1c34ef8c3527a9becc31e04b12dcd38 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 9 Nov 2024 22:30:38 +0500 Subject: [PATCH 22/24] fix review notes (including: rename approve to approve_token rpc, add check for same chain for 1inch swap, move 1inch to 'lp_commands/rpc') --- mm2src/coins/eth/eth_tests.rs | 7 ++++ mm2src/mm2_main/src/mm2.rs | 1 - .../mm2_main/src/rpc/dispatcher/dispatcher.rs | 17 +++++---- .../src/rpc/lp_commands/lp_commands.rs | 3 +- .../{ext_api => rpc/lp_commands}/one_inch.rs | 0 .../lp_commands}/one_inch/errors.rs | 3 ++ .../lp_commands}/one_inch/rpcs.rs | 24 ++++++------ .../lp_commands}/one_inch/types.rs | 2 +- .../src/rpc/lp_commands/{eth.rs => tokens.rs} | 33 ++++++++-------- .../tests/docker_tests/docker_tests_inner.rs | 38 ++++++++----------- mm2src/trading_api/Cargo.toml | 3 ++ mm2src/trading_api/src/one_inch_api/client.rs | 4 +- 12 files changed, 74 insertions(+), 61 deletions(-) rename mm2src/mm2_main/src/{ext_api => rpc/lp_commands}/one_inch.rs (100%) rename mm2src/mm2_main/src/{ext_api => rpc/lp_commands}/one_inch/errors.rs (97%) rename mm2src/mm2_main/src/{ext_api => rpc/lp_commands}/one_inch/rpcs.rs (96%) rename mm2src/mm2_main/src/{ext_api => rpc/lp_commands}/one_inch/types.rs (99%) rename mm2src/mm2_main/src/rpc/lp_commands/{eth.rs => tokens.rs} (91%) diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 03705ff4ca..6d8192cf70 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1048,3 +1048,10 @@ fn test_gas_limit_conf() { && eth_coin.gas_limit.eth_max_trade_gas == 150_000 ); } + +#[test] +fn test_h256_to_str() { + let h = H256::from_str("5136701f11060010841c9708c3eb26f6606a070b8ae43f4b98b6d7b10a545258").unwrap(); + let b: BytesJson = h.0.to_vec().into(); + println!("H256={}", format!("0x{:02x}", b)); +} diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index 447212a852..dd7c7bed27 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -70,7 +70,6 @@ use mm2_err_handle::prelude::*; #[cfg(not(target_arch = "wasm32"))] pub mod database; -pub mod ext_api; pub mod heartbeat_event; pub mod lp_dispatcher; pub mod lp_healthcheck; diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index 74f82702a6..aa49796dbf 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -1,7 +1,4 @@ use super::{DispatcherError, DispatcherResult, PUBLIC_METHODS}; -use crate::ext_api::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, - one_inch_v6_0_classic_swap_liquidity_sources_rpc, - one_inch_v6_0_classic_swap_quote_rpc, one_inch_v6_0_classic_swap_tokens_rpc}; use crate::lp_healthcheck::peer_connection_healthcheck_rpc; use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor_status, init_trezor_user_action}; #[cfg(target_arch = "wasm32")] @@ -10,12 +7,18 @@ use crate::lp_ordermatch::{best_orders_rpc_v2, orderbook_rpc_v2, start_simple_ma stop_simple_market_maker_bot}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, + one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_liquidity_sources_rpc, + one_inch_v6_0_classic_swap_quote_rpc, + one_inch_v6_0_classic_swap_tokens_rpc}; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, stop_version_stat_collection, update_version_stat_collection}, lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}, - rpc::lp_commands::{eth::{allowance_rpc, approve_rpc}, - get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; + rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, + tokens::{approve_token_rpc, get_token_allowance_rpc}, + trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; @@ -163,8 +166,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, active_swaps_rpc).await, "add_delegation" => handle_mmrpc(ctx, request, add_delegation).await, "add_node_to_version_stat" => handle_mmrpc(ctx, request, add_node_to_version_stat).await, - "approve" => handle_mmrpc(ctx, request, approve_rpc).await, - "allowance" => handle_mmrpc(ctx, request, allowance_rpc).await, + "approve_token" => handle_mmrpc(ctx, request, approve_token_rpc).await, + "get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs index 7c5363638f..5db3b7b758 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs @@ -7,7 +7,8 @@ use mm2_err_handle::prelude::*; use rpc::v1::types::H160 as H160Json; use serde_json::Value as Json; -pub mod eth; +pub mod one_inch; +pub mod tokens; pub type GetPublicKeyRpcResult = Result>; pub type GetSharedDbIdResult = Result>; diff --git a/mm2src/mm2_main/src/ext_api/one_inch.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs similarity index 100% rename from mm2src/mm2_main/src/ext_api/one_inch.rs rename to mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs diff --git a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs similarity index 97% rename from mm2src/mm2_main/src/ext_api/one_inch/errors.rs rename to mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs index 421525cff7..8ee65af984 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/errors.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs @@ -17,6 +17,8 @@ pub enum ApiIntegrationRpcError { NftNotSupported, #[display(fmt = "Chain not supported")] ChainNotSupported, + #[display(fmt = "Must be same chain")] + DifferentChains, #[from_stringify("coins::UnexpectedDerivationMethod")] MyAddressError(String), InvalidParam(String), @@ -44,6 +46,7 @@ impl HttpStatusCode for ApiIntegrationRpcError { ApiIntegrationRpcError::CoinTypeError | ApiIntegrationRpcError::NftNotSupported | ApiIntegrationRpcError::ChainNotSupported + | ApiIntegrationRpcError::DifferentChains | ApiIntegrationRpcError::MyAddressError(_) | ApiIntegrationRpcError::InvalidParam(_) | ApiIntegrationRpcError::OutOfBounds { .. } diff --git a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs similarity index 96% rename from mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs rename to mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs index 9b323b38c7..8dcc5f2223 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/rpcs.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -25,8 +25,8 @@ pub async fn one_inch_v6_0_classic_swap_quote_rpc( req: ClassicSwapQuoteRequest, ) -> MmResult { let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; - api_supports_coin(&base)?; let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_coin(&base, &rel)?; let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; let query_params = ClassicSwapQuoteParams::new(base_contract, rel_contract, sell_amount.to_string()) @@ -64,9 +64,8 @@ pub async fn one_inch_v6_0_classic_swap_create_rpc( req: ClassicSwapCreateRequest, ) -> MmResult { let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; - api_supports_coin(&base)?; - let (_, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; - + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_coin(&base, &rel)?; let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; let single_address = base.derivation_method().single_addr_or_err().await?; @@ -158,18 +157,21 @@ async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, } #[allow(clippy::result_large_err)] -fn api_supports_coin(coin: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { - if ApiClient::is_chain_supported(coin.chain_id()) { - Ok(()) - } else { - Err(MmError::new(ApiIntegrationRpcError::ChainNotSupported)) +fn api_supports_coin(base: &EthCoin, rel: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { + if !ApiClient::is_chain_supported(base.chain_id()) { + return MmError::err(ApiIntegrationRpcError::ChainNotSupported); + } + if base.chain_id() != rel.chain_id() { + return MmError::err(ApiIntegrationRpcError::DifferentChains); } + Ok(()) } #[cfg(test)] mod tests { - use crate::ext_api::one_inch::{rpcs::{one_inch_v6_0_classic_swap_create_rpc, one_inch_v6_0_classic_swap_quote_rpc}, - types::{ClassicSwapCreateRequest, ClassicSwapQuoteRequest}}; + use crate::rpc::lp_commands::one_inch::{rpcs::{one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_quote_rpc}, + types::{ClassicSwapCreateRequest, ClassicSwapQuoteRequest}}; use coins::eth::EthCoin; use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; use common::block_on; diff --git a/mm2src/mm2_main/src/ext_api/one_inch/types.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs similarity index 99% rename from mm2src/mm2_main/src/ext_api/one_inch/types.rs rename to mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs index f14f513328..202eb0dcf2 100644 --- a/mm2src/mm2_main/src/ext_api/one_inch/types.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs @@ -1,4 +1,4 @@ -use crate::ext_api::one_inch::errors::FromApiValueError; +use crate::rpc::lp_commands::one_inch::errors::FromApiValueError; use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; use common::true_f; use ethereum_types::{Address, U256}; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs similarity index 91% rename from mm2src/mm2_main/src/rpc/lp_commands/eth.rs rename to mm2src/mm2_main/src/rpc/lp_commands/tokens.rs index 77a0835bdc..4228ffab8d 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/eth.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs @@ -11,20 +11,6 @@ use http::StatusCode; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::{mm_error::MmError, prelude::MmResult}; use mm2_number::BigDecimal; -use rpc::v1::types::Bytes as BytesJson; - -#[derive(Debug, Deserialize)] -pub struct Erc20ApproveRequest { - coin: String, - spender: EthAddress, - amount: BigDecimal, -} - -#[derive(Debug, Deserialize)] -pub struct Erc20AllowanceRequest { - coin: String, - spender: EthAddress, -} #[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] @@ -55,22 +41,35 @@ impl HttpStatusCode for Erc20CallError { } } +#[derive(Debug, Deserialize)] +pub struct Erc20AllowanceRequest { + coin: String, + spender: EthAddress, +} + /// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). /// Returns BigDecimal allowance value. -pub async fn allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { +pub async fn get_token_allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; let wei = eth_coin.allowance(req.spender).compat().await?; let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; Ok(amount) } +#[derive(Debug, Deserialize)] +pub struct Erc20ApproveRequest { + coin: String, + spender: EthAddress, + amount: BigDecimal, +} + /// Call approve method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#allowance). /// Returns approval transaction hash. -pub async fn approve_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { +pub async fn approve_token_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; let tx = eth_coin.approve(req.spender, amount).compat().await?; - Ok(tx.tx_hash_as_bytes()) + Ok(format!("0x{:02x}", tx.tx_hash_as_bytes())) } async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index bd03dbafc1..6e3b4611a5 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -21,7 +21,7 @@ use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, di enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, - MarketMakerIt, Mm2TestConf}; + MarketMakerIt, Mm2TestConf, DEFAULT_RPC_PASSWORD}; use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -5525,22 +5525,14 @@ fn test_approve_erc20() { let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); let mm = MarketMakerIt::start( - json!({ - "gui": "nogui", - "netid": 9000, - "dht": "on", // Enable DHT without delay. - "passphrase": format!("0x{}", hex::encode(privkey)), - "coins": coins, - "rpc_password": "pass", - "i_am_seed": true, - }), - "pass".to_string(), + Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins).conf, + DEFAULT_RPC_PASSWORD.to_string(), None, ) .unwrap(); let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); - log!("Alice log path: {}", mm.log_path.display()); + log!("Node log path: {}", mm.log_path.display()); let swap_contract = format!("0x{}", hex::encode(swap_contract())); let _eth_enable = block_on(enable_eth_coin( @@ -5562,39 +5554,41 @@ fn test_approve_erc20() { let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, - "method":"allowance", + "method":"approve_token", "mmrpc":"2.0", "id": 0, "params":{ "coin": "ERC20DEV", "spender": swap_contract, + "amount": BigDecimal::from_str("11.0").unwrap(), } }))) .unwrap(); - assert!(rc.0.is_success(), "allowance error: {}", rc.1); + assert!(rc.0.is_success(), "approve_token error: {}", rc.1); let res = serde_json::from_str::(&rc.1).unwrap(); assert!( - BigDecimal::from_str(res["result"].as_str().unwrap()).is_ok(), - "allowance result incorrect" + hex::decode(str_strip_0x!(res["result"].as_str().unwrap())).is_ok(), + "approve_token result incorrect" ); let rc = block_on(mm.rpc(&json!({ "userpass": mm.userpass, - "method":"approve", + "method":"get_token_allowance", "mmrpc":"2.0", "id": 0, "params":{ "coin": "ERC20DEV", "spender": swap_contract, - "amount": BigDecimal::from_str("11.0").unwrap(), } }))) .unwrap(); - assert!(rc.0.is_success(), "approve error: {}", rc.1); + assert!(rc.0.is_success(), "get_token_allowance error: {}", rc.1); let res = serde_json::from_str::(&rc.1).unwrap(); - assert!( - hex::decode(res["result"].as_str().unwrap()).is_ok(), - "approve result incorrect" + assert_eq!( + BigDecimal::from_str(res["result"].as_str().unwrap()).unwrap(), + BigDecimal::from_str("11.0").unwrap(), + "get_token_allowance result incorrect" ); + block_on(mm.stop()).unwrap(); } diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml index 5dfde994da..4fd9514fb9 100644 --- a/mm2src/trading_api/Cargo.toml +++ b/mm2src/trading_api/Cargo.toml @@ -21,5 +21,8 @@ serde_derive = "1.0" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } url = { version = "2.2.2", features = ["serde"] } +[features] +test-ext-api = [] # use test config to connect to an external api + [dev-dependencies] mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs index cd8f54a880..9c7136148a 100644 --- a/mm2src/trading_api/src/one_inch_api/client.rs +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -1,7 +1,7 @@ use super::errors::ApiClientError; use crate::one_inch_api::errors::NativeError; use common::StatusCode; -use lazy_static::lazy_static; +#[cfg(feature = "test-ext-api")] use lazy_static::lazy_static; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::{map_mm_error::MapMmError, map_to_mm::MapToMmResult, @@ -25,6 +25,7 @@ const ONE_INCH_ETH_SPECIAL_CONTRACT: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee #[cfg(test)] const ONE_INCH_API_TEST_URL: &str = "https://api.1inch.dev"; +#[cfg(feature = "test-ext-api")] lazy_static! { /// API key for testing static ref ONE_INCH_API_TEST_AUTH: String = std::env::var("ONE_INCH_API_TEST_AUTH").unwrap_or_default(); @@ -123,6 +124,7 @@ impl ApiClient { fn get_headers() -> Vec<(&'static str, &'static str)> { vec![ + #[cfg(feature = "test-ext-api")] ("Authorization", ONE_INCH_API_TEST_AUTH.as_str()), ("accept", "application/json"), ] From df5f045bde015bbdfb951929f0997d8813c3c322 Mon Sep 17 00:00:00 2001 From: dimxy Date: Sat, 9 Nov 2024 22:48:34 +0500 Subject: [PATCH 23/24] refactor eth type import --- mm2src/coins/lp_coins.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index a4e042094b..c5511cf919 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -54,7 +54,7 @@ use crypto::{derive_secp256k1_secret, Bip32Error, Bip44Chain, CryptoCtx, CryptoC Secp256k1ExtendedPublicKey, Secp256k1Secret, WithHwRpcError}; use derive_more::Display; use enum_derives::{EnumFromStringify, EnumFromTrait}; -use ethereum_types::H256; +use ethereum_types::{H256, U256}; use futures::compat::Future01CompatExt; use futures::lock::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use futures::{FutureExt, TryFutureExt}; @@ -220,7 +220,6 @@ pub mod eth; use eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; use eth::{eth_coin_from_conf_and_request, get_eth_address, EthCoin, EthGasDetailsErr, EthTxFeeDetails, GetEthAddressError, GetValidEthWithdrawAddError, SignedEthTx}; -use ethereum_types::U256; pub mod hd_wallet; use hd_wallet::{AccountUpdatingError, AddressDerivingError, HDAccountOps, HDAddressId, HDAddressOps, HDCoinAddress, From 7b5bed314c190314d5e1aa7811f37a0a9d5fee63 Mon Sep 17 00:00:00 2001 From: dimxy Date: Mon, 11 Nov 2024 19:51:19 +0500 Subject: [PATCH 24/24] fix fmt --- .../mm2_main/tests/docker_tests/eth_docker_tests.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index 862dcfd75c..831a891fed 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -13,18 +13,18 @@ use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivation use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, - DerivationMethod, Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, - ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, - SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, - SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, - ValidateNftMakerPaymentArgs}; #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use coins::{lp_coinfind, CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, MakerCoinSwapOpsV2, MmCoinEnum, MmCoinStruct, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, ValidateMakerPaymentArgs, ValidateTakerFundingArgs}; +use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, DerivationMethod, + Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, ParseCoinAssocTypes, + ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, + ValidateNftMakerPaymentArgs}; use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256;