From 6a0d51ccd200b22b246d62aae1f3ee42e1eb8d11 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Tue, 26 Nov 2024 17:35:16 +0200 Subject: [PATCH 01/15] feat: added base structure to mayan swift integration --- crates/gem_evm/src/address.rs | 4 + crates/gem_evm/src/jsonrpc.rs | 16 + crates/gem_evm/src/lib.rs | 1 + crates/gem_evm/src/mayan/mod.rs | 1 + crates/gem_evm/src/mayan/swift/deployment.rs | 116 +++++ crates/gem_evm/src/mayan/swift/fee_manager.rs | 69 +++ crates/gem_evm/src/mayan/swift/mod.rs | 3 + crates/gem_evm/src/mayan/swift/swift.rs | 104 +++++ gemstone/src/swapper/mayan/fee_manager.rs | 101 +++++ .../src/swapper/mayan/mayan_swift_contract.rs | 402 ++++++++++++++++++ .../src/swapper/mayan/mayan_swift_provider.rs | 353 +++++++++++++++ gemstone/src/swapper/mayan/mod.rs | 3 + gemstone/src/swapper/mod.rs | 2 + gemstone/src/swapper/models.rs | 2 + gemstone/src/swapper/universal_router/mod.rs | 8 + gemstone/tests/integration_test.rs | 98 ++++- 16 files changed, 1281 insertions(+), 2 deletions(-) create mode 100644 crates/gem_evm/src/mayan/mod.rs create mode 100644 crates/gem_evm/src/mayan/swift/deployment.rs create mode 100644 crates/gem_evm/src/mayan/swift/fee_manager.rs create mode 100644 crates/gem_evm/src/mayan/swift/mod.rs create mode 100644 crates/gem_evm/src/mayan/swift/swift.rs create mode 100644 gemstone/src/swapper/mayan/fee_manager.rs create mode 100644 gemstone/src/swapper/mayan/mayan_swift_contract.rs create mode 100644 gemstone/src/swapper/mayan/mayan_swift_provider.rs create mode 100644 gemstone/src/swapper/mayan/mod.rs diff --git a/crates/gem_evm/src/address.rs b/crates/gem_evm/src/address.rs index f8712725..241cb57c 100644 --- a/crates/gem_evm/src/address.rs +++ b/crates/gem_evm/src/address.rs @@ -60,6 +60,10 @@ impl FromStr for EthereumAddress { impl EthereumAddress { pub const LEN: usize = 20; + pub fn zero() -> Self { + Self { bytes: vec![0u8; Self::LEN] } + } + pub fn parse(str: &str) -> Option { Self::from_str(str).ok() } diff --git a/crates/gem_evm/src/jsonrpc.rs b/crates/gem_evm/src/jsonrpc.rs index 25326bc1..8388753f 100644 --- a/crates/gem_evm/src/jsonrpc.rs +++ b/crates/gem_evm/src/jsonrpc.rs @@ -1,3 +1,4 @@ +use alloy_primitives::U256; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] @@ -35,6 +36,17 @@ impl TransactionObject { data: format!("0x{}", hex::encode(data)), } } + + pub fn new_call_with_value(from: &str, to: &str, data: Vec, value: &str) -> Self { + Self { + from: Some(from.to_string()), + to: to.to_string(), + gas: None, + gas_price: None, + value: Some(value.to_string()), + data: format!("0x{}", hex::encode(data)), + } + } } #[derive(Debug, Serialize, Deserialize)] @@ -73,6 +85,8 @@ pub enum EthereumRpc { GasPrice, GetBalance(&'static str), Call(TransactionObject, BlockParameter), + GetTransactionReceipt(String), + EstimateGas(TransactionObject), } impl EthereumRpc { @@ -81,6 +95,8 @@ impl EthereumRpc { EthereumRpc::GasPrice => "eth_gasPrice", EthereumRpc::GetBalance(_) => "eth_getBalance", EthereumRpc::Call(_, _) => "eth_call", + EthereumRpc::GetTransactionReceipt(_) => "eth_getTransactionReceipt", + EthereumRpc::EstimateGas(_) => "eth_estimateGas", } } } diff --git a/crates/gem_evm/src/lib.rs b/crates/gem_evm/src/lib.rs index 8907f054..9b880afb 100644 --- a/crates/gem_evm/src/lib.rs +++ b/crates/gem_evm/src/lib.rs @@ -3,5 +3,6 @@ pub mod erc20; pub mod erc2612; pub mod jsonrpc; pub mod lido; +pub mod mayan; pub mod permit2; pub mod uniswap; diff --git a/crates/gem_evm/src/mayan/mod.rs b/crates/gem_evm/src/mayan/mod.rs new file mode 100644 index 00000000..c1023c86 --- /dev/null +++ b/crates/gem_evm/src/mayan/mod.rs @@ -0,0 +1 @@ +pub mod swift; diff --git a/crates/gem_evm/src/mayan/swift/deployment.rs b/crates/gem_evm/src/mayan/swift/deployment.rs new file mode 100644 index 00000000..364c762b --- /dev/null +++ b/crates/gem_evm/src/mayan/swift/deployment.rs @@ -0,0 +1,116 @@ +use std::collections::HashMap; + +use primitives::Chain; + +#[derive(Debug, Clone, PartialEq)] +pub struct MayanSwiftDeployment { + pub address: String, + pub wormhole_id: u16, +} + +pub fn get_swift_providers() -> HashMap { + let mut map = HashMap::new(); + map.insert( + Chain::Solana, + MayanSwiftDeployment { + address: "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY".to_string(), + wormhole_id: 1, + }, + ); + map.insert( + Chain::Ethereum, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: 2, + }, + ); + map.insert( + Chain::SmartChain, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: 4, + }, + ); + map.insert( + Chain::Polygon, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: 5, + }, + ); + map.insert( + Chain::Arbitrum, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: 23, + }, + ); + map.insert( + Chain::Optimism, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: 24, + }, + ); + map.insert( + Chain::Base, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: 30, + }, + ); + + map +} + +pub fn get_swift_deployment_chains() -> Vec { + get_swift_providers().keys().cloned().collect() +} + +pub fn get_swift_deployment_by_chain(chain: Chain) -> Option { + get_swift_providers().get(&chain).map(|x| x.clone()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_swift_provider_address() { + // Test all supported chains + assert_eq!( + get_swift_deployment_by_chain(Chain::Solana), + Some(MayanSwiftDeployment { + address: "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY".to_string(), + wormhole_id: 1, + }) + ); + + let evm_address = "0xC38e4e6A15593f908255214653d3D947CA1c2338"; + + assert_eq!(get_swift_deployment_by_chain(Chain::Ethereum).map(|x| x.address), Some(evm_address.to_string())); + assert_eq!( + get_swift_deployment_by_chain(Chain::SmartChain).map(|x| x.address), + Some(evm_address.to_string()) + ); + assert_eq!(get_swift_deployment_by_chain(Chain::Polygon).map(|x| x.address), Some(evm_address.to_string())); + // assert_eq!(get_swift_deployment_by_chain(Chain::Avalanche).map(|x| x.address), Some(evm_address.to_string())); + assert_eq!(get_swift_deployment_by_chain(Chain::Arbitrum).map(|x| x.address), Some(evm_address.to_string())); + assert_eq!(get_swift_deployment_by_chain(Chain::Optimism).map(|x| x.address), Some(evm_address.to_string())); + + // Test unsupported chain + assert_eq!(get_swift_deployment_by_chain(Chain::Sui), None); + } + + #[test] + fn test_chain_ids() { + // Verify chain IDs match the provided table, and that they are in order + assert_eq!(get_swift_deployment_by_chain(Chain::Solana).map(|x| x.wormhole_id), Some(1)); + assert_eq!(get_swift_deployment_by_chain(Chain::Ethereum).map(|x| x.wormhole_id), Some(2)); + assert_eq!(get_swift_deployment_by_chain(Chain::SmartChain).map(|x| x.wormhole_id), Some(4)); + assert_eq!(get_swift_deployment_by_chain(Chain::Polygon).map(|x| x.wormhole_id), Some(5)); + // assert_eq!(get_swift_deployment_by_chain(Chain::Avalanche).map(|x| x.wormhole_id), Some(6)); + assert_eq!(get_swift_deployment_by_chain(Chain::Arbitrum).map(|x| x.wormhole_id), Some(23)); + assert_eq!(get_swift_deployment_by_chain(Chain::Optimism).map(|x| x.wormhole_id), Some(24)); + } +} diff --git a/crates/gem_evm/src/mayan/swift/fee_manager.rs b/crates/gem_evm/src/mayan/swift/fee_manager.rs new file mode 100644 index 00000000..95c17d60 --- /dev/null +++ b/crates/gem_evm/src/mayan/swift/fee_manager.rs @@ -0,0 +1,69 @@ +use alloy_core::sol; +use alloy_primitives::{Address, U256}; + +sol! { + /// @notice Fee Manager interface for managing protocol fees and treasury operations + #[derive(Debug, PartialEq)] + interface IFeeManager { + /// @notice Calculates the protocol fee in basis points + /// @param amountIn The input amount for the swap + /// @param tokenIn The input token address + /// @param tokenOut The output token identifier + /// @param destChain The destination chain identifier + /// @param referrerBps The referrer's basis points + /// @return The protocol fee in basis points + function calcProtocolBps( + uint64 amountIn, + address tokenIn, + bytes32 tokenOut, + uint16 destChain, + uint8 referrerBps + ) external view returns (uint8); + + /// @notice Returns the current fee collector address + /// @return The address of the fee collector (treasury or contract) + function feeCollector() external view returns (address); + + /// @notice Changes the operator to a new address + /// @param nextOperator The address of the new operator + function changeOperator(address nextOperator) external; + + /// @notice Allows the next operator to claim the operator role + function claimOperator() external; + + /// @notice Sweeps ERC20 tokens from the contract + /// @param token The token address to sweep + /// @param amount The amount to sweep + /// @param to The recipient address + function sweepToken(address token, uint256 amount, address to) external; + + /// @notice Sweeps ETH from the contract + /// @param amount The amount of ETH to sweep + /// @param to The recipient address + function sweepEth(uint256 amount, address payable to) external; + + /// @notice Sets the base fee in basis points + /// @param baseBps The new base fee in basis points + function setBaseBps(uint8 baseBps) external; + + /// @notice Sets the treasury address + /// @param treasury The new treasury address + function setTreasury(address treasury) external; + } + + /// @notice Fee Manager contract state and events + #[derive(Debug, PartialEq)] + contract FeeManager { + /// @notice The current operator address + address public operator; + + /// @notice The next operator address + address public nextOperator; + + /// @notice The base fee in basis points + uint8 public baseBps; + + /// @notice The treasury address + address public treasury; + } +} diff --git a/crates/gem_evm/src/mayan/swift/mod.rs b/crates/gem_evm/src/mayan/swift/mod.rs new file mode 100644 index 00000000..bd6f6c3c --- /dev/null +++ b/crates/gem_evm/src/mayan/swift/mod.rs @@ -0,0 +1,3 @@ +pub mod deployment; +pub mod fee_manager; +pub mod swift; diff --git a/crates/gem_evm/src/mayan/swift/swift.rs b/crates/gem_evm/src/mayan/swift/swift.rs new file mode 100644 index 00000000..135f601a --- /dev/null +++ b/crates/gem_evm/src/mayan/swift/swift.rs @@ -0,0 +1,104 @@ +use alloy_core::sol; + +// First define the enums used in the contract +sol! { + #[derive(Debug)] + enum Status { + CREATED, + FULFILLED, + UNLOCKED, + CANCELED, + REFUNDED + } + + enum Action { + NONE, + FULFILL, + UNLOCK, + REFUND, + BATCH_UNLOCK + } + + enum AuctionMode { + NONE, + BYPASS, + ENGLISH + } +} + +// Now define the main contract interface +sol! { + /// @title MayanSwift Cross-Chain Swap Contract + #[derive(Debug)] + contract MayanSwift { + // Events + event OrderCreated(bytes32 indexed key); + event OrderFulfilled(bytes32 indexed key, uint64 sequence, uint256 netAmount); + event OrderUnlocked(bytes32 indexed key); + event OrderCanceled(bytes32 indexed key, uint64 sequence); + event OrderRefunded(bytes32 indexed key, uint256 netAmount); + + // Storage + address public immutable wormhole; + uint16 public immutable auctionChainId; + bytes32 public immutable auctionAddr; + bytes32 public immutable solanaEmitter; + address public feeManager; + uint8 public consistencyLevel; + address public guardian; + address public nextGuardian; + bool public paused; + + struct Order { + uint8 status; // Status enum + uint64 amountIn; + uint16 destChainId; + } + + struct OrderParams { + bytes32 trader; + bytes32 tokenOut; + uint64 minAmountOut; + uint64 gasDrop; + uint64 cancelFee; + uint64 refundFee; + uint64 deadline; + bytes32 destAddr; + uint16 destChainId; + bytes32 referrerAddr; + uint8 referrerBps; + uint8 auctionMode; + bytes32 random; + } + + struct PermitParams { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + // State changing functions + function setPause(bool _pause) external; + function setFeeManager(address _feeManager) external; + function setConsistencyLevel(uint8 _consistencyLevel) external; + function changeGuardian(address newGuardian) external; + function claimGuardian() external; + + // External functions + function createOrderWithEth(OrderParams memory params) external payable returns (bytes32 orderHash); + function createOrderWithToken(address tokenIn, uint256 amountIn, OrderParams memory params) external returns (bytes32 orderHash); + function createOrderWithSig( + address tokenIn, + uint256 amountIn, + OrderParams memory params, + uint256 submissionFee, + bytes calldata signedOrderHash, + PermitParams calldata permitParams + ) external returns (bytes32 orderHash); + + // View functions + function getOrders(bytes32[] memory orderHashes) external view returns (Order[] memory); + } +} diff --git a/gemstone/src/swapper/mayan/fee_manager.rs b/gemstone/src/swapper/mayan/fee_manager.rs new file mode 100644 index 00000000..ba1d89db --- /dev/null +++ b/gemstone/src/swapper/mayan/fee_manager.rs @@ -0,0 +1,101 @@ +use std::{str::FromStr, sync::Arc}; + +use alloy_core::{ + hex::decode as HexDecode, + primitives::{Address, FixedBytes, U256, U8}, + sol_types::SolCall, +}; +use gem_evm::{ + address::EthereumAddress, + jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, + mayan::swift::fee_manager::IFeeManager, +}; +use primitives::Chain; +use thiserror::Error; + +use crate::network::{jsonrpc_call, AlienProvider}; + +#[derive(Debug, Error)] +pub enum FeeManagerError { + #[error("Only operator")] + OnlyOperator, + + #[error("Only next operator")] + OnlyNextOperator, + + #[error("Zero address")] + ZeroAddress, + + #[error("Call failed: {msg}")] + CallFailed { msg: String }, + + #[error("Invalid address: {address}")] + InvalidAddress { address: String }, + + #[error("ABI error: {msg}")] + ABIError { msg: String }, +} + +pub struct CalcProtocolBpsParams { + pub amount_in: u64, + pub token_in: EthereumAddress, + pub token_out: FixedBytes<32>, // bytes32 + pub dest_chain: u16, + pub referrer_bps: u8, +} + +pub struct SweepParams { + pub token: Option, // None for ETH, Some(address) for ERC20 + pub amount: U256, + pub to: EthereumAddress, +} + +#[derive(Debug)] +pub struct FeeManager { + address: String, +} + +impl FeeManager { + pub fn new(address: String) -> Self { + Self { address } + } + + pub async fn calc_protocol_bps( + &self, + sender: String, + chain: &Chain, + provider: Arc, + params: CalcProtocolBpsParams, + ) -> Result { + let token_in_address = Address::from_str(¶ms.token_in.to_checksum()).map_err(|_| FeeManagerError::InvalidAddress { + address: params.token_in.to_checksum(), + })?; + + let call_data = IFeeManager::calcProtocolBpsCall { + amountIn: params.amount_in, + tokenIn: token_in_address, + tokenOut: params.token_out, + destChain: params.dest_chain, + referrerBps: params.referrer_bps, + } + .abi_encode(); + + let calc_protocol_bps_call = EthereumRpc::Call(TransactionObject::new_call_with_from(&sender, &self.address, call_data), BlockParameter::Latest); + + let response = jsonrpc_call(&calc_protocol_bps_call, provider, chain) + .await + .map_err(|e| FeeManagerError::CallFailed { msg: e.to_string() })?; + + let result: String = response.extract_result().map_err(|e| FeeManagerError::CallFailed { msg: e.to_string() })?; + + let decoded = HexDecode(&result).map_err(|e| FeeManagerError::ABIError { + msg: format!("Failed to decode hex result: {}", e), + })?; + + let calculated_bps = IFeeManager::calcProtocolBpsCall::abi_decode_returns(&decoded, false).map_err(|e| FeeManagerError::ABIError { + msg: format!("Invalid calcProtocolBpsCall response: {}", e), + })?; + + Ok(U8::from(calculated_bps._0)) + } +} diff --git a/gemstone/src/swapper/mayan/mayan_swift_contract.rs b/gemstone/src/swapper/mayan/mayan_swift_contract.rs new file mode 100644 index 00000000..fb2eadd4 --- /dev/null +++ b/gemstone/src/swapper/mayan/mayan_swift_contract.rs @@ -0,0 +1,402 @@ +use crate::{ + network::{jsonrpc_call, AlienProvider}, + swapper::{ApprovalData, ApprovalType}, +}; +use alloy_core::{ + hex::{decode as HexDecode, encode_prefixed, ToHexExt}, + primitives::{Address, FixedBytes, U256, U8}, + sol_types::{SolCall, SolValue}, +}; + +use gem_evm::{ + address::EthereumAddress, + erc20::IERC20, + jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, + mayan::swift::swift::MayanSwift, +}; +use primitives::Chain; +use std::{str::FromStr, sync::Arc}; +use thiserror::Error; + +pub struct MayanSwiftContract { + address: String, + provider: Arc, + chain: Chain, +} + +#[derive(Error, Debug)] +pub enum MayanSwiftContractError { + #[error("Call failed: {msg}")] + CallFailed { msg: String }, + + #[error("Invalid response: {msg}")] + InvalidResponse { msg: String }, + + #[error("ABI error: {msg}")] + ABIError { msg: String }, + + #[error("Invalid amount")] + InvalidAmount, +} + +// Parameter structs with native types +#[derive(Debug, Clone)] +pub struct OrderParams { + pub trader: [u8; 32], + pub token_out: [u8; 32], + pub min_amount_out: u64, + pub gas_drop: u64, + pub cancel_fee: u64, + pub refund_fee: u64, + pub deadline: u64, + pub dest_addr: [u8; 32], + pub dest_chain_id: u16, + pub referrer_addr: [u8; 32], + pub referrer_bps: u8, + pub auction_mode: u8, + pub random: [u8; 32], +} + +#[derive(Debug)] +pub struct PermitParams { + pub value: String, + pub deadline: u64, + pub v: u8, + pub r: [u8; 32], + pub s: [u8; 32], +} + +impl MayanSwiftContract { + pub fn new(address: String, provider: Arc, chain: Chain) -> Self { + Self { address, provider, chain } + } + + pub async fn get_fee_manager_address(&self) -> Result { + let call_data = MayanSwift::feeManagerCall {}.abi_encode(); + let fee_manager_call = EthereumRpc::Call(TransactionObject::new_call(&self.address, call_data), BlockParameter::Latest); + + let response = jsonrpc_call(&fee_manager_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let decoded = HexDecode(&result).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + + let fee_manager = + MayanSwift::feeManagerCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftContractError::ABIError { msg: e.to_string() })?; + + let address = EthereumAddress::from_str(&fee_manager.feeManager.to_string()).map_err(|e| MayanSwiftContractError::ABIError { + msg: format!("Failed to parse fee manager address: {}", e), + })?; + + Ok(address.to_checksum()) + } + + pub async fn create_order_with_eth(&self, from: &str, params: OrderParams, value: &str) -> Result { + let call_data = self + .encode_create_order_with_eth(params, U256::from_str(value).map_err(|_| MayanSwiftContractError::InvalidAmount)?) + .await?; + + let create_order_call = EthereumRpc::Call( + TransactionObject::new_call_with_value(from, &self.address, call_data, value), + BlockParameter::Latest, + ); + + let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + Ok(result) + } + + pub async fn create_order_with_token(&self, from: &str, token_in: &str, amount_in: &str, params: OrderParams) -> Result { + let call_data = self + .encode_create_order_with_token(token_in, U256::from_str(amount_in).map_err(|_| MayanSwiftContractError::InvalidAmount)?, params) + .await?; + + let create_order_call = EthereumRpc::Call(TransactionObject::new_call_with_from(from, &self.address, call_data), BlockParameter::Latest); + + let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + Ok(result) + } + + pub async fn estimate_create_order_with_eth(&self, from: &str, params: OrderParams, amount: U256) -> Result { + let call_data = self.encode_create_order_with_eth(params, amount).await?; + + // let value = encode_prefixed(amount.to_be_bytes_vec()); + let value = format!("0x{:x}", amount); + + let estimate_gas_call = EthereumRpc::EstimateGas(TransactionObject::new_call_with_value(from, &self.address, call_data, &value)); + + let response = jsonrpc_call(&estimate_gas_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let hex_str = result.trim_start_matches("0x"); + + Ok(U256::from_str_radix(hex_str, 16).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?) + } + + pub async fn estimate_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result { + let call_data = self.encode_create_order_with_token(token_in, amount, params).await?; + let estimate_gas_call = EthereumRpc::EstimateGas(TransactionObject::new_call_with_value(&self.address, token_in, call_data, &amount.to_string())); + + let response = jsonrpc_call(&estimate_gas_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let decoded = HexDecode(&result).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + + Ok(U256::from_str(decoded.encode_hex().as_str()).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?) + } + + pub async fn encode_create_order_with_eth(&self, params: OrderParams, amount: U256) -> Result, MayanSwiftContractError> { + let call_data = MayanSwift::createOrderWithEthCall { + params: self.convert_order_params(params), + } + .abi_encode(); + + Ok(call_data) + } + + pub async fn encode_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result, MayanSwiftContractError> { + let call_data = MayanSwift::createOrderWithTokenCall { + tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftContractError::ABIError { + msg: format!("Invalid token address: {}", e), + })?, + amountIn: amount, + params: self.convert_order_params(params), + } + .abi_encode(); + + Ok(call_data) + } + + pub async fn get_orders(&self, order_hashes: Vec<[u8; 32]>) -> Result, MayanSwiftContractError> { + let call_data = MayanSwift::getOrdersCall { + orderHashes: order_hashes.into_iter().map(|x| x.into()).collect(), + } + .abi_encode(); + + let get_orders_call = EthereumRpc::Call(TransactionObject::new_call(&self.address, call_data), BlockParameter::Latest); + + let response = jsonrpc_call(&get_orders_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let decoded = HexDecode(&result).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + + let orders = MayanSwift::getOrdersCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftContractError::ABIError { msg: e.to_string() })?; + + Ok(orders + ._0 + .into_iter() + .map(|order| (order.status, order.amountIn.try_into().unwrap_or(0), order.destChainId)) + .collect()) + } + + pub async fn check_token_approval(&self, owner: &str, token: &str, amount: &str) -> Result { + // Encode allowance call for ERC20 token + let call_data = IERC20::allowanceCall { + owner: Address::from_str(owner).map_err(|e| MayanSwiftContractError::ABIError { + msg: format!("Invalid owner address: {}", e), + })?, + spender: Address::from_str(&self.address).map_err(|e| MayanSwiftContractError::ABIError { + msg: format!("Invalid spender address: {}", e), + })?, + } + .abi_encode(); + + // Create RPC call + let allowance_call = EthereumRpc::Call(TransactionObject::new_call(token, call_data), BlockParameter::Latest); + + // Execute the call + let response = jsonrpc_call(&allowance_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + // Decode the response + let decoded = hex::decode(result.trim_start_matches("0x")).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + + let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftContractError::ABIError { msg: e.to_string() })?; + + // Convert amount string to U256 for comparison + let required_amount = U256::from_str(amount).map_err(|e| MayanSwiftContractError::ABIError { + msg: format!("Invalid amount: {}", e), + })?; + + // Compare allowance with required amount + Ok(if allowance._0 >= required_amount { + ApprovalType::Approve(ApprovalData { + token: token.into(), + spender: self.address.clone(), + value: amount.into(), + }) + } else { + ApprovalType::None + }) + } + + pub async fn encode_create_order_with_sig( + &self, + token_in: &str, + amount_in: U256, + params: OrderParams, + submission_fee: U256, + signed_order_hash: Vec, + permit_params: PermitParams, + ) -> Result, MayanSwiftContractError> { + let call_data = MayanSwift::createOrderWithSigCall { + tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftContractError::ABIError { + msg: format!("Invalid token address: {}", e), + })?, + amountIn: amount_in, + params: self.convert_order_params(params), + submissionFee: submission_fee, + signedOrderHash: signed_order_hash.into(), + permitParams: self.convert_permit_params(permit_params), + } + .abi_encode(); + + Ok(call_data) + } + + pub async fn create_order_with_sig( + &self, + from: &str, + token_in: &str, + amount_in: &str, + params: OrderParams, + submission_fee: &str, + signed_order_hash: Vec, + permit_params: PermitParams, + ) -> Result { + let call_data = self + .encode_create_order_with_sig( + token_in, + U256::from_str(amount_in).map_err(|_| MayanSwiftContractError::InvalidAmount)?, + params, + U256::from_str(submission_fee).map_err(|_| MayanSwiftContractError::InvalidAmount)?, + signed_order_hash, + permit_params, + ) + .await?; + + let create_order_call = EthereumRpc::Call(TransactionObject::new_call_with_from(from, &self.address, call_data), BlockParameter::Latest); + + let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) + .await + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + let result: String = response + .extract_result() + .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + + Ok(result) + } + + pub fn convert_permit_params(&self, permit_params: PermitParams) -> MayanSwift::PermitParams { + MayanSwift::PermitParams { + value: U256::from_str(&permit_params.value).map_err(|_| MayanSwiftContractError::InvalidAmount)?, + deadline: U256::from(permit_params.deadline), + v: permit_params.v.into(), + r: permit_params.r.into(), + s: permit_params.s.into(), + } + } + + // Helper method to convert our native OrderParams to contract format + pub fn convert_order_params(&self, params: OrderParams) -> MayanSwift::OrderParams { + MayanSwift::OrderParams { + trader: params.trader.into(), + tokenOut: params.token_out.into(), + minAmountOut: params.min_amount_out, + gasDrop: params.gas_drop, + cancelFee: params.cancel_fee, + refundFee: params.refund_fee, + deadline: params.deadline, + destAddr: params.dest_addr.into(), + destChainId: params.dest_chain_id, + referrerAddr: params.referrer_addr.into(), + referrerBps: params.referrer_bps, + auctionMode: params.auction_mode, + random: params.random.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{future::pending, time::Duration}; + + use async_std::future::timeout; + use async_trait::async_trait; + + use crate::network::{mock::AlienProviderWarp, AlienError, AlienTarget, Data}; + + #[derive(Debug)] + pub struct AlienProviderMock { + pub response: String, + pub timeout: Duration, + } + + #[async_trait] + impl AlienProvider for AlienProviderMock { + async fn request(&self, _target: AlienTarget) -> Result { + let responses = self.batch_request(vec![_target]).await; + responses.map(|responses| responses.first().unwrap().clone()) + } + + async fn batch_request(&self, _targets: Vec) -> Result, AlienError> { + let never = pending::<()>(); + let _ = timeout(self.timeout, never).await; + Ok(vec![self.response.as_bytes().to_vec()]) + } + + fn get_endpoint(&self, _chain: Chain) -> Result { + Ok(String::from("http://localhost:8080")) + } + } + + // #[test] + // fn test_encode_amount_hex() { + // let amount = U256::from(100); + // let mock_provider = AlienProviderMock { + // response: String::from("0x0000000000000000000000000000000000000000000000000000000000000064"), + // timeout: Duration::from_millis(100), + // }; + // let encoded = MayanSwiftContract::new("0x1234567890abcdef".into(), Arc::new(mock_provider), Chain::Ethereum).encode_amount_hex(amount); + // assert_eq!(encoded, "0000000000000000000000000000000000000000000000000000000000000064"); + // } +} diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs new file mode 100644 index 00000000..4db66d1a --- /dev/null +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -0,0 +1,353 @@ +use std::{str::FromStr, sync::Arc}; + +use alloy_core::{hex::ToHexExt, primitives::Address}; +use alloy_primitives::U256; +use async_trait::async_trait; +use gem_evm::{ + address::EthereumAddress, + jsonrpc::EthereumRpc, + mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains}, +}; +use primitives::Chain; + +use crate::{ + network::{jsonrpc_call, AlienProvider}, + swapper::{ + ApprovalType, FetchQuoteData, GemSwapProvider, SwapProvider, SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteRequest, SwapRoute, SwapperError, + }, +}; + +use super::{ + fee_manager::{CalcProtocolBpsParams, FeeManager}, + mayan_swift_contract::{MayanSwiftContract, MayanSwiftContractError, OrderParams}, +}; + +#[derive(Debug)] +pub struct MayanSwiftProvider {} + +impl From for SwapperError { + fn from(err: MayanSwiftContractError) -> Self { + SwapperError::NetworkError { msg: err.to_string() } + } +} + +impl MayanSwiftProvider { + pub fn new() -> Self { + Self {} + } + + fn get_address_by_chain(chain: Chain) -> Option { + get_swift_deployment_by_chain(chain).map(|x| x.address) + } + + async fn check_approval(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + if request.from_asset.is_native() { + return Ok(ApprovalType::None); + } + + let token_id = request.from_asset.token_id.as_ref().ok_or(SwapperError::NotSupportedAsset)?; + + let deployment = get_swift_deployment_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + + let swift_contract = MayanSwiftContract::new(deployment.address, provider.clone(), request.from_asset.chain); + + let amount = &request.value; + swift_contract + .check_token_approval(&request.wallet_address, token_id, amount) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() }) + } + + async fn calculate_output_value( + &self, + request: &SwapQuoteRequest, + provider: Arc, + order_params: &OrderParams, + ) -> Result { + let fee_manager_address = Self::get_address_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let fee_manager = FeeManager::new(fee_manager_address); + + let token_out = if let Some(token_id) = &request.to_asset.token_id { + let mut bytes = [0u8; 32]; + if let Ok(addr) = EthereumAddress::from_str(token_id) { + bytes.copy_from_slice(&addr.bytes); + } + bytes + } else { + [0u8; 32] + }; + + let fees = fee_manager + .calc_protocol_bps( + request.wallet_address.clone(), + &request.from_asset.chain, + provider.clone(), + CalcProtocolBpsParams { + amount_in: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, + token_in: EthereumAddress::zero(), + token_out: token_out.into(), + dest_chain: request.to_asset.chain.network_id().parse().unwrap(), + referrer_bps: 0, + }, + ) + .await + .map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; + + // TODO: do something with fees + let output_value = U256::from_str(request.value.as_str()).map_err(|_| SwapperError::InvalidAmount)?; + + // Calculate output value with fees + let output_value = output_value.checked_sub(U256::from(fees)).ok_or(SwapperError::ComputeQuoteError { + msg: "Protocol fees calculation error".to_string(), + })?; + + Ok(output_value.to_string()) + } + + fn add_referrer(&self, request: &SwapQuoteRequest, order_params: &mut OrderParams) { + // let referrer_bps = if let Some(options) = &request.options { + // if let Some(ref_fees) = &options.fee { + // if let Ok(addr) = EthereumAddress::from_str(&ref_fees.evm.address) { + // order_params.referrer_addr.copy_from_slice(&addr.bytes); + // } + // ref_fees.evm.bps as u8 + // } else { + // 0 + // } + // } else { + // 0 + // }; + + // TODO: implement + } + + fn build_swift_order_params(&self, request: &SwapQuoteRequest) -> Result { + let mut order_params = OrderParams { + trader: [0u8; 32], + token_out: [0u8; 32], + min_amount_out: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, // TODO:: + // do i need to calculate output + fees here? + gas_drop: 0, + cancel_fee: 0, + refund_fee: 0, + deadline: (std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + 3600) as u64, + dest_addr: [0u8; 32], + dest_chain_id: request.to_asset.chain.network_id().parse().unwrap(), + referrer_addr: [0u8; 32], + referrer_bps: 0, + auction_mode: 0, + random: [0u8; 32], + }; + + // TODO: move to separated method to test + let token_in = if request.from_asset.is_native() { + EthereumAddress::zero() + } else { + EthereumAddress::from_str(request.from_asset.token_id.as_ref().ok_or(SwapperError::NotSupportedAsset)?).map_err(|_| { + SwapperError::InvalidAddress { + address: request.from_asset.token_id.clone().unwrap(), + } + })? + }; + + if let Ok(wallet_addr) = EthereumAddress::from_str(&request.wallet_address) { + let mut trader_bytes = [0u8; 32]; + trader_bytes[12..].copy_from_slice(&wallet_addr.bytes); + order_params.trader.copy_from_slice(&trader_bytes); + } + + // Set destination address + if let Ok(dest_addr) = EthereumAddress::from_str(&request.destination_address) { + let mut dest_bytes = [0u8; 32]; + dest_bytes[12..].copy_from_slice(&dest_addr.bytes); + order_params.dest_addr.copy_from_slice(&dest_bytes); + } + + // Set token_out for the destination token + if let Some(token_id) = &request.to_asset.token_id { + if let Ok(token_addr) = EthereumAddress::from_str(token_id) { + let mut token_bytes = [0u8; 32]; + token_bytes[12..].copy_from_slice(&token_addr.bytes); + order_params.token_out.copy_from_slice(&token_bytes); + } + } + + Ok(order_params) + } +} + +#[async_trait] +impl GemSwapProvider for MayanSwiftProvider { + fn provider(&self) -> SwapProvider { + SwapProvider::MayanSwift + } + + fn supported_chains(&self) -> Vec { + get_swift_deployment_chains() + } + + async fn fetch_quote_data(&self, quote: &SwapQuote, provider: Arc, data: FetchQuoteData) -> Result { + let request = "e.request; + let swift_address = Self::get_address_by_chain(quote.request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let swift_contract = MayanSwiftContract::new(swift_address.clone(), provider.clone(), quote.request.from_asset.chain); + let swift_order_params = self + .build_swift_order_params(request) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; + + let amount_in = quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?; + let data = if quote.request.from_asset.is_native() { + swift_contract + .encode_create_order_with_eth(swift_order_params, amount_in) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + } else { + swift_contract + .encode_create_order_with_token(request.from_asset.token_id.as_ref().unwrap(), amount_in, swift_order_params) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + }; + + Ok(SwapQuoteData { + to: swift_address, + value: quote.from_value.clone(), + data: data.encode_hex(), + }) + } + + async fn get_transaction_status(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { + let receipt_call = EthereumRpc::GetTransactionReceipt(transaction_hash.to_string()); + + let response = jsonrpc_call(&receipt_call, provider, &chain) + .await + .map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; + + let result: String = response.extract_result().map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; + + Ok(result == "0x1") + } + + async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + // Validate chain support + if !self.supported_chains().contains(&request.from_asset.chain) { + return Err(SwapperError::NotSupportedChain); + } + + let swift_order_params = self + .build_swift_order_params(request) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; + + // Check approvals if needed + let approval = self.check_approval(request, provider.clone()).await?; + + // Get fee manager address for referral info + let swift_address = Self::get_address_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let swift_contract = MayanSwiftContract::new(swift_address.clone(), provider.clone(), request.from_asset.chain); + let amount_in = U256::from_str(&request.value).map_err(|_| SwapperError::InvalidAmount)?; + let estimated_gas = if request.from_asset.is_native() { + swift_contract + .estimate_create_order_with_eth(request.wallet_address.as_str(), swift_order_params.clone(), amount_in) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + } else { + swift_contract + .estimate_create_order_with_token(request.from_asset.token_id.as_ref().unwrap(), amount_in, swift_order_params.clone()) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + }; + // Create route information + let route = SwapRoute { + route_type: "swift-order".to_string(), + input: request + .from_asset + .token_id + .clone() + .unwrap_or_else(|| request.from_asset.chain.as_ref().to_string()), + output: request.to_asset.token_id.clone().unwrap_or_else(|| request.to_asset.chain.as_ref().to_string()), + fee_tier: "0".to_string(), // MayanSwift doesn't use fee tiers + gas_estimate: Some(estimated_gas.to_string()), // TODO: check if this is correct + }; + + let output_value = self.calculate_output_value(request, provider.clone(), &swift_order_params).await?; + + Ok(SwapQuote { + from_value: request.value.clone(), + to_value: output_value, + data: SwapProviderData { + provider: self.provider().clone(), + routes: vec![route], + }, + approval, + request: request.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use alloy_core::sol_types::SolValue; + use alloy_primitives::U256; + use primitives::AssetId; + + use crate::{ + network::{AlienError, AlienTarget, Data}, + swapper::GemSwapMode, + }; + + use super::*; + + #[test] + fn test_eth_value_conversion() { + let decimal_str = "1000000000000000000"; // 1 ETH + let value = U256::from_str(decimal_str).unwrap(); + + let hex_value = format!("0x{}", value.to_string().encode_hex()); + + assert_eq!(hex_value, "0xde0b6b3a7640000"); + } + + #[test] + fn test_supported_chains() { + let provider = MayanSwiftProvider::new(); + let chains = provider.supported_chains(); + + assert!(chains.contains(&Chain::Solana)); + assert!(chains.contains(&Chain::Ethereum)); + assert!(chains.contains(&Chain::SmartChain)); + assert!(chains.contains(&Chain::Polygon)); + assert!(chains.contains(&Chain::Arbitrum)); + assert!(chains.contains(&Chain::Optimism)); + assert!(chains.contains(&Chain::Base)); + } + + #[test] + fn test_address_parameters() { + let wallet = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; + let destination = "0x1234567890123456789012345678901234567890"; + + let request = SwapQuoteRequest { + from_asset: AssetId::from_chain(Chain::Base), + to_asset: AssetId::from_chain(Chain::Ethereum), + wallet_address: wallet.to_string(), + destination_address: destination.to_string(), + value: "292840000000000".to_string(), + mode: GemSwapMode::ExactIn, + options: None, + }; + + // Create provider and get params + let provider = Arc::new(MockProvider::new()); + let swift_provider = MayanSwiftProvider::new(); + + let params = tokio_test::block_on(swift_provider.build_swift_order_params(&request, provider)).unwrap(); + + // Verify trader address (wallet) + let wallet_addr = EthereumAddress::from_str(wallet).unwrap(); + assert_eq!(¶ms.trader[12..], &wallet_addr.bytes); + assert_eq!(¶ms.trader[..12], &[0u8; 12]); // First 12 bytes should be zero + + // Verify destination address + let dest_addr = EthereumAddress::from_str(destination).unwrap(); + assert_eq!(¶ms.dest_addr[12..], &dest_addr.bytes); + assert_eq!(¶ms.dest_addr[..12], &[0u8; 12]); + } +} diff --git a/gemstone/src/swapper/mayan/mod.rs b/gemstone/src/swapper/mayan/mod.rs new file mode 100644 index 00000000..8283c3b2 --- /dev/null +++ b/gemstone/src/swapper/mayan/mod.rs @@ -0,0 +1,3 @@ +mod fee_manager; +pub mod mayan_swift_contract; +pub mod mayan_swift_provider; diff --git a/gemstone/src/swapper/mod.rs b/gemstone/src/swapper/mod.rs index 009100c1..fb928ee3 100644 --- a/gemstone/src/swapper/mod.rs +++ b/gemstone/src/swapper/mod.rs @@ -6,6 +6,7 @@ use std::{fmt::Debug, sync::Arc}; mod custom_types; mod permit2_data; +pub mod mayan; pub mod models; pub mod orca; pub mod slippage; @@ -41,6 +42,7 @@ impl GemSwapper { Box::new(universal_router::UniswapV3::new_uniswap()), Box::new(universal_router::UniswapV3::new_pancakeswap()), Box::new(thorchain::ThorChain::default()), + Box::new(mayan::mayan_swift_provider::MayanSwiftProvider::new()), ], } } diff --git a/gemstone/src/swapper/models.rs b/gemstone/src/swapper/models.rs index 039cd8d7..291e39a5 100644 --- a/gemstone/src/swapper/models.rs +++ b/gemstone/src/swapper/models.rs @@ -55,6 +55,7 @@ pub enum SwapProvider { PancakeSwapV3, Thorchain, Orca, + MayanSwift, } impl SwapProvider { pub fn name(&self) -> &str { @@ -63,6 +64,7 @@ impl SwapProvider { Self::PancakeSwapV3 => "PancakeSwap v3", Self::Thorchain => "THORChain", Self::Orca => "Orca Whirlpool", + Self::MayanSwift => "Mayan Swift", } } } diff --git a/gemstone/src/swapper/universal_router/mod.rs b/gemstone/src/swapper/universal_router/mod.rs index be6bf98c..8ca6f58b 100644 --- a/gemstone/src/swapper/universal_router/mod.rs +++ b/gemstone/src/swapper/universal_router/mod.rs @@ -53,6 +53,14 @@ impl JsonRpcRequestConvert for EthereumRpc { let value = serde_json::to_value(tx).unwrap(); vec![value, block.into()] } + EthereumRpc::GetTransactionReceipt(hash) => { + let value = serde_json::to_value(hash).unwrap(); + vec![value] + } + EthereumRpc::EstimateGas(tx) => { + let value = serde_json::to_value(tx).unwrap(); + vec![value] + } }; JsonRpcRequest::new(id, method, params) diff --git a/gemstone/tests/integration_test.rs b/gemstone/tests/integration_test.rs index 81c08347..d5394ca6 100644 --- a/gemstone/tests/integration_test.rs +++ b/gemstone/tests/integration_test.rs @@ -6,7 +6,11 @@ mod tests { network::{provider::AlienProvider, target::*, *}, swapper::{orca::Orca, *}, }; - use primitives::{AssetId, Chain}; + use mayan::{ + mayan_swift_contract::{MayanSwiftContract, MayanSwiftContractError}, + mayan_swift_provider::MayanSwiftProvider, + }; + use primitives::{Asset, AssetId, Chain}; use reqwest::Client; use std::{collections::HashMap, sync::Arc}; @@ -110,7 +114,97 @@ mod tests { let quote = swap_provider.fetch_quote(&request, network_provider.clone()).await?; assert_eq!(quote.from_value, "1000000"); - assert!(quote.to_value > 0); + assert!(quote.to_value.parse::().unwrap() > 0); + + Ok(()) + } + + #[tokio::test] + async fn test_swift_get_fee_manager() -> Result<(), MayanSwiftContractError> { + const TEST_SWIFT_ADDRESS: &str = "0xC38e4e6A15593f908255214653d3D947CA1c2338"; + const TEST_FEE_MANAGER_ADDRESS: &str = "0xF93191d350117723DBEda5484a3b0996d285CECF"; + + // Setup Base chain node config + let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".into())]); + let network_provider = Arc::new(NativeProvider::new(node_config)); + + let swift_provider = MayanSwiftContract::new(TEST_SWIFT_ADDRESS.to_string(), network_provider, Chain::Base); + + // Get fee manager address + let fee_manager_address = swift_provider.get_fee_manager_address().await?; + println!("Fee Manager Address: {}", fee_manager_address); + + // Verify the address format + assert!(fee_manager_address.starts_with("0x")); + assert_eq!(fee_manager_address.len(), 42); // Standard Ethereum address length + assert_eq!(fee_manager_address, TEST_FEE_MANAGER_ADDRESS); + + Ok(()) + } + + #[tokio::test] + async fn test_mayan_swift_quote() -> Result<(), SwapperError> { + const TEST_WALLET_ADDRESS: &str = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; + + let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".into())]); + let network_provider = Arc::new(NativeProvider::new(node_config)); + + let mayan_swift_provider = MayanSwiftProvider::new(); + + // Create a swap quote request + let request = SwapQuoteRequest { + from_asset: AssetId::from_chain(Chain::Base), + to_asset: AssetId::from_chain(Chain::Ethereum), + wallet_address: TEST_WALLET_ADDRESS.to_string(), + destination_address: TEST_WALLET_ADDRESS.to_string(), + value: "10000000000000".to_string(), + mode: GemSwapMode::ExactIn, // Swap mode + options: None, + }; + + let quote = mayan_swift_provider.fetch_quote(&request, network_provider.clone()).await?; + + assert_eq!(quote.from_value, "10000000000000"); + assert_eq!(quote.to_value, "10000000000000"); + + // Verify + assert_eq!(quote.data.routes.len(), 1); + assert_eq!(quote.data.routes[0].route_type, "swift-order"); + assert_eq!(quote.data.routes[0].input, "base"); + assert_eq!(quote.data.routes[0].output, "ethereum"); + assert_eq!(quote.data.routes[0].fee_tier, "0"); + // assert!(quote.data.routes[0].gas_estimate.is_some(); + + // Verify + // assert_eq!(quote.approval, ApprovalType::None); + + Ok(()) + } + + #[tokio::test] + async fn test_mayan_swift_quote_data() -> Result<(), SwapperError> { + let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".into())]); + let network_provider = Arc::new(NativeProvider::new(node_config)); + + let mayan_swift_provider = MayanSwiftProvider::new(); + + // Create + let request = SwapQuoteRequest { + from_asset: AssetId::from_chain(Chain::Base), + to_asset: AssetId::from_chain(Chain::Ethereum), + wallet_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".to_string(), + destination_address: "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c".to_string(), + value: "10000000000000".to_string(), // 0.00001 ETH + mode: GemSwapMode::ExactIn, + options: None, + }; + + let quote = mayan_swift_provider.fetch_quote(&request, network_provider.clone()).await?; + let quote_data = mayan_swift_provider + .fetch_quote_data("e, network_provider.clone(), FetchQuoteData::None) + .await; + + assert!(quote_data.is_ok()); Ok(()) } From a9ae31d2038b5041df35faa43580f64b9a635152 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 28 Nov 2024 07:26:25 +0200 Subject: [PATCH 02/15] feat: use mayan relayer for quota --- Cargo.lock | 1 + Cargo.toml | 59 +-- .../src/mayan/{swift => }/fee_manager.rs | 0 crates/gem_evm/src/mayan/forwarder.rs | 88 ++++ crates/gem_evm/src/mayan/mod.rs | 2 + crates/gem_evm/src/mayan/swift/mod.rs | 1 - crates/gem_evm/src/mayan/swift/swift.rs | 9 +- gemstone/Cargo.toml | 7 +- gemstone/src/network/mock.rs | 53 +-- gemstone/src/swapper/mayan/fee_manager.rs | 2 +- gemstone/src/swapper/mayan/forwarder.rs | 80 ++++ gemstone/src/swapper/mayan/mayan_relayer.rs | 444 ++++++++++++++++++ ...mayan_swift_contract.rs => mayan_swift.rs} | 190 ++++---- .../src/swapper/mayan/mayan_swift_provider.rs | 362 +++++++------- gemstone/src/swapper/mayan/mod.rs | 4 +- gemstone/tests/integration_test.rs | 63 +-- 16 files changed, 971 insertions(+), 394 deletions(-) rename crates/gem_evm/src/mayan/{swift => }/fee_manager.rs (100%) create mode 100644 crates/gem_evm/src/mayan/forwarder.rs create mode 100644 gemstone/src/swapper/mayan/forwarder.rs create mode 100644 gemstone/src/swapper/mayan/mayan_relayer.rs rename gemstone/src/swapper/mayan/{mayan_swift_contract.rs => mayan_swift.rs} (67%) diff --git a/Cargo.lock b/Cargo.lock index 4e61a1eb..5f10d6df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3315,6 +3315,7 @@ dependencies = [ "num-bigint 0.4.6", "orca_whirlpools_core", "primitives", + "rand", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 71720d24..50b437ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,42 +10,42 @@ documentation = "https://github.com/gemwalletcom" [workspace] resolver = "2" members = [ - "apps/api", - "apps/daemon", - "apps/parser", - "apps/setup", + "apps/api", + "apps/daemon", + "apps/parser", + "apps/setup", - "bin/img-downloader", - "bin/generate", + "bin/img-downloader", + "bin/generate", - "bin/uniffi-bindgen", - "gemstone", + "bin/uniffi-bindgen", + "gemstone", - "crates/primitives", - "crates/blockchain", - "crates/fiat", - "crates/cacher", - "crates/name_resolver", - "crates/api_connector", - "crates/settings", - "crates/settings_chain", - "crates/pricer", - "crates/chain_primitives", + "crates/primitives", + "crates/blockchain", + "crates/fiat", + "crates/cacher", + "crates/name_resolver", + "crates/api_connector", + "crates/settings", + "crates/settings_chain", + "crates/pricer", + "crates/chain_primitives", - "crates/security_*", - "crates/gem_*", + "crates/security_*", + "crates/gem_*", - "crates/localizer", - "crates/job_runner", + "crates/localizer", + "crates/job_runner", ] default-members = [ - "apps/api", - "apps/daemon", - "apps/parser", - "apps/setup", - "bin/generate", - "gemstone", + "apps/api", + "apps/daemon", + "apps/parser", + "apps/setup", + "bin/generate", + "gemstone", ] [workspace.dependencies] @@ -73,6 +73,7 @@ lazy_static = "1.4.0" futures-util = "0.3.30" uuid = { version = "1.8.0", features = ["v4"] } rand = { version = "0.8.5" } +rand_core = { version = "0.6.4" } # db diesel = { version = "2.2.3", features = ["postgres", "chrono", "serde_json"] } @@ -116,5 +117,5 @@ rust-embed = { version = "8.5.0" } # numbers rusty-money = { git = "https://github.com/varunsrin/rusty_money.git", rev = "bbc0150", features = [ - "iso", + "iso", ] } diff --git a/crates/gem_evm/src/mayan/swift/fee_manager.rs b/crates/gem_evm/src/mayan/fee_manager.rs similarity index 100% rename from crates/gem_evm/src/mayan/swift/fee_manager.rs rename to crates/gem_evm/src/mayan/fee_manager.rs diff --git a/crates/gem_evm/src/mayan/forwarder.rs b/crates/gem_evm/src/mayan/forwarder.rs new file mode 100644 index 00000000..3cf617aa --- /dev/null +++ b/crates/gem_evm/src/mayan/forwarder.rs @@ -0,0 +1,88 @@ +use alloy_core::sol; + +sol! { + /// @title MayanForwarder Interface + #[derive(Debug, PartialEq)] + interface IMayanForwarder { + /// @notice Guardian address + function guardian() external view returns (address); + + /// @notice Next guardian address + function nextGuardian() external view returns (address); + + /// @notice Check if protocol is supported for swaps + function swapProtocols(address protocol) external view returns (bool); + + /// @notice Check if protocol is supported for Mayan operations + function mayanProtocols(address protocol) external view returns (bool); + + /// @notice Forward ETH to Mayan protocol + function forwardEth(address mayanProtocol, bytes calldata protocolData) external payable; + + /// @notice Forward ERC20 tokens to Mayan protocol + function forwardERC20( + address tokenIn, + uint256 amountIn, + PermitParams calldata permitParams, + address mayanProtocol, + bytes calldata protocolData + ) external payable; + + /// @notice Swap ETH to token and forward to Mayan protocol + function swapAndForwardEth( + uint256 amountIn, + address swapProtocol, + bytes calldata swapData, + address middleToken, + uint256 minMiddleAmount, + address mayanProtocol, + bytes calldata mayanData + ) external payable; + + /// @notice Swap ERC20 token and forward to Mayan protocol + function swapAndForwardERC20( + address tokenIn, + uint256 amountIn, + PermitParams calldata permitParams, + address swapProtocol, + bytes calldata swapData, + address middleToken, + uint256 minMiddleAmount, + address mayanProtocol, + bytes calldata mayanData + ) external payable; + + /// @notice Rescue ERC20 tokens + function rescueToken(address token, uint256 amount, address to) external; + + /// @notice Rescue ETH + function rescueEth(uint256 amount, address payable to) external; + + /// @notice Change guardian + function changeGuardian(address newGuardian) external; + + /// @notice Claim guardian role + function claimGuardian() external; + + /// @notice Set swap protocol status + function setSwapProtocol(address swapProtocol, bool enabled) external; + + /// @notice Set Mayan protocol status + function setMayanProtocol(address mayanProtocol, bool enabled) external; + + /// Events + event ForwardedEth(address mayanProtocol, bytes protocolData); + event ForwardedERC20(address token, uint256 amount, address mayanProtocol, bytes protocolData); + event SwapAndForwardedEth(uint256 amountIn, address swapProtocol, address middleToken, uint256 middleAmount, address mayanProtocol, bytes mayanData); + event SwapAndForwardedERC20(address tokenIn, uint256 amountIn, address swapProtocol, address middleToken, uint256 middleAmount, address mayanProtocol, bytes mayanData); + + /// Structs + struct PermitParams { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + } +} diff --git a/crates/gem_evm/src/mayan/mod.rs b/crates/gem_evm/src/mayan/mod.rs index c1023c86..6968936a 100644 --- a/crates/gem_evm/src/mayan/mod.rs +++ b/crates/gem_evm/src/mayan/mod.rs @@ -1 +1,3 @@ +pub mod fee_manager; +pub mod forwarder; pub mod swift; diff --git a/crates/gem_evm/src/mayan/swift/mod.rs b/crates/gem_evm/src/mayan/swift/mod.rs index bd6f6c3c..1adfd3dd 100644 --- a/crates/gem_evm/src/mayan/swift/mod.rs +++ b/crates/gem_evm/src/mayan/swift/mod.rs @@ -1,3 +1,2 @@ pub mod deployment; -pub mod fee_manager; pub mod swift; diff --git a/crates/gem_evm/src/mayan/swift/swift.rs b/crates/gem_evm/src/mayan/swift/swift.rs index 135f601a..6d070182 100644 --- a/crates/gem_evm/src/mayan/swift/swift.rs +++ b/crates/gem_evm/src/mayan/swift/swift.rs @@ -30,7 +30,7 @@ sol! { sol! { /// @title MayanSwift Cross-Chain Swap Contract #[derive(Debug)] - contract MayanSwift { + contract IMayanSwift{ // Events event OrderCreated(bytes32 indexed key); event OrderFulfilled(bytes32 indexed key, uint64 sequence, uint256 netAmount); @@ -79,6 +79,13 @@ sol! { bytes32 s; } + struct KeyStruct { + OrderParams params; + bytes32 tokenIn; + uint16 chainId; + uint16 protocolBps; + } + // State changing functions function setPause(bool _pause) external; function setFeeManager(address _feeManager) external; diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 1c7c1c63..1a80d2e3 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -5,9 +5,9 @@ version = "0.1.1" [lib] crate-type = [ - "staticlib", # iOS - "rlib", # for Other crate - "cdylib", # Android + "staticlib", # iOS + "rlib", # for Other crate + "cdylib", # Android ] name = "gemstone" @@ -39,6 +39,7 @@ num-bigint.workspace = true futures.workspace = true borsh.workspace = true orca_whirlpools_core = "0.3.1" +rand.workspace = true [build-dependencies] uniffi = { workspace = true, features = ["build"] } diff --git a/gemstone/src/network/mock.rs b/gemstone/src/network/mock.rs index 8545421a..0447ce27 100644 --- a/gemstone/src/network/mock.rs +++ b/gemstone/src/network/mock.rs @@ -1,5 +1,8 @@ +use async_trait::async_trait; +use primitives::Chain; + use super::{AlienError, AlienProvider, AlienTarget, Data}; -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, sync::Arc, time::Duration}; #[derive(Debug, uniffi::Object)] pub struct AlienProviderWarp { @@ -18,38 +21,32 @@ impl AlienProviderWarp { } } -#[cfg(test)] -pub mod tests { - use crate::network::*; - use crate::network::{mock::*, target::*}; - use async_std::future::{pending, timeout}; - use async_trait::async_trait; - use primitives::Chain; - use std::time::Duration; +#[derive(Debug)] +pub struct AlienProviderMock { + pub response: String, + pub timeout: Duration, +} - #[derive(Debug)] - pub struct AlienProviderMock { - pub response: String, - pub timeout: Duration, +#[async_trait] +impl AlienProvider for AlienProviderMock { + async fn request(&self, _target: AlienTarget) -> Result { + let responses = self.batch_request(vec![_target]).await; + responses.map(|responses| responses.first().unwrap().clone()) } - #[async_trait] - impl AlienProvider for AlienProviderMock { - async fn request(&self, _target: AlienTarget) -> Result { - let responses = self.batch_request(vec![_target]).await; - responses.map(|responses| responses.first().unwrap().clone()) - } - - async fn batch_request(&self, _targets: Vec) -> Result, AlienError> { - let never = pending::<()>(); - let _ = timeout(self.timeout, never).await; - Ok(vec![self.response.as_bytes().to_vec()]) - } + async fn batch_request(&self, _targets: Vec) -> Result, AlienError> { + Ok(vec![self.response.as_bytes().to_vec()]) + } - fn get_endpoint(&self, _chain: Chain) -> Result { - Ok(String::from("http://localhost:8080")) - } + fn get_endpoint(&self, _chain: Chain) -> Result { + Ok(String::from("http://localhost:8080")) } +} + +#[cfg(test)] +pub mod tests { + use crate::network::{mock::*, target::*}; + use std::time::Duration; #[tokio::test] async fn test_mock_call() { diff --git a/gemstone/src/swapper/mayan/fee_manager.rs b/gemstone/src/swapper/mayan/fee_manager.rs index ba1d89db..66a5d049 100644 --- a/gemstone/src/swapper/mayan/fee_manager.rs +++ b/gemstone/src/swapper/mayan/fee_manager.rs @@ -8,7 +8,7 @@ use alloy_core::{ use gem_evm::{ address::EthereumAddress, jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::swift::fee_manager::IFeeManager, + mayan::fee_manager::IFeeManager, }; use primitives::Chain; use thiserror::Error; diff --git a/gemstone/src/swapper/mayan/forwarder.rs b/gemstone/src/swapper/mayan/forwarder.rs new file mode 100644 index 00000000..bec442a7 --- /dev/null +++ b/gemstone/src/swapper/mayan/forwarder.rs @@ -0,0 +1,80 @@ +use std::{str::FromStr, sync::Arc}; + +use alloy_core::{ + primitives::{Address, U256}, + sol_types::{SolCall, SolValue}, +}; +use gem_evm::{ + jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, + mayan::forwarder::IMayanForwarder, +}; +use primitives::Chain; +use thiserror::Error; + +use crate::network::{jsonrpc_call, AlienProvider}; + +#[derive(Debug, Error)] +pub enum MayanForwarderError { + #[error("Unsupported protocol")] + UnsupportedProtocol, + #[error("Call failed: {msg}")] + CallFailed { msg: String }, + #[error("Invalid response")] + InvalidResponse, + #[error("ABI error: {msg}")] + ABIError { msg: String }, +} + +pub struct MayanForwarder { + address: String, + provider: Arc, + chain: Chain, +} + +impl MayanForwarder { + pub fn new(address: String, provider: Arc, chain: Chain) -> Self { + Self { address, provider, chain } + } + + pub async fn encode_forward_eth_call(&self, mayan_protocol: &str, protocol_data: Vec) -> Result, MayanForwarderError> { + let call_data = IMayanForwarder::forwardEthCall { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid protocol address: {}", e), + })?, + protocolData: protocol_data.into(), + } + .abi_encode(); + + Ok(call_data) + } + + pub async fn encode_swap_and_forward_eth_call( + &self, + amount_in: U256, + swap_protocol: &str, + swap_data: Vec, + middle_token: &str, + min_middle_amount: U256, + mayan_protocol: &str, + mayan_data: Vec, + ) -> Result, MayanForwarderError> { + let call_data = IMayanForwarder::swapAndForwardEthCall { + amountIn: amount_in, + swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid swap protocol address: {}", e), + })?, + swapData: swap_data.into(), + middleToken: Address::from_str(middle_token).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid middle token address: {}", e), + })?, + minMiddleAmount: min_middle_amount, + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid mayan protocol address: {}", e), + })?, + mayanData: mayan_data.into(), + } + .abi_encode(); + + Ok(call_data) + } +} diff --git a/gemstone/src/swapper/mayan/mayan_relayer.rs b/gemstone/src/swapper/mayan/mayan_relayer.rs new file mode 100644 index 00000000..a7edbfa3 --- /dev/null +++ b/gemstone/src/swapper/mayan/mayan_relayer.rs @@ -0,0 +1,444 @@ +use std::sync::Arc; + +use primitives::Chain; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::network::{AlienHttpMethod, AlienProvider, AlienTarget}; + +const MAYAN_PROGRAM_ID: &str = "FC4eXxkyrMPTjiYUpp4EAnkmwMbQyZ6NDCh1kfLn6vsf"; +pub const MAYAN_FORWARDER_CONTRACT: &str = "0x0654874eb7F59C6f5b39931FC45dC45337c967c3"; +const SDK_VERSION: &str = "9_7_0"; + +#[derive(Debug, Deserialize)] +struct ApiError { + code: String, + msg: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct QuoteParams { + pub amount: u64, + pub from_token: String, + pub from_chain: Chain, + pub to_token: String, + pub to_chain: Chain, + #[serde(skip_serializing_if = "Option::is_none")] + pub slippage_bps: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_drop: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer_bps: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct QuoteOptions { + #[serde(default = "default_true")] + pub swift: bool, + #[serde(default = "default_true")] + pub mctp: bool, + #[serde(default = "default_false")] + pub gasless: bool, + #[serde(default = "default_false")] + pub only_direct: bool, +} + +fn deserialize_string_or_u64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::{Error, Unexpected}; + let value: Option = Option::deserialize(deserializer)?; + if let Some(s) = value { + s.parse::() + .map(Some) + .map_err(|_| Error::invalid_value(Unexpected::Str(&s), &"a valid u64")) + // Convert string to u64 + } else { + Ok(None) + } +} + +impl Default for QuoteOptions { + fn default() -> Self { + Self { + swift: true, + mctp: true, + gasless: false, + only_direct: false, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Token { + pub name: String, + pub standard: String, + pub symbol: String, + pub mint: String, + pub verified: bool, // Added + pub contract: String, + #[serde(rename = "wrappedAddress")] + pub wrapped_address: Option, // Added + #[serde(rename = "chainId")] + pub chain_id: Option, + #[serde(rename = "wChainId")] + pub w_chain_id: Option, + pub decimals: u8, + #[serde(rename = "logoURI")] + pub logo_uri: String, + #[serde(rename = "coingeckoId")] + pub coingecko_id: String, + #[serde(rename = "realOriginChainId")] + pub real_origin_chain_id: Option, + #[serde(rename = "realOriginContractAddress")] + pub real_origin_contract_address: Option, + #[serde(rename = "supportsPermit")] + pub supports_permit: bool, + #[serde(rename = "hasAuction")] + pub has_auction: bool, // Added +} + +#[derive(Debug, PartialEq)] +pub enum QuoteType { + SWIFT, + MCTP, + SWAP, + WH, +} + +impl ToString for QuoteType { + fn to_string(&self) -> String { + match self { + QuoteType::SWIFT => "SWIFT".to_string(), + QuoteType::MCTP => "MCTP".to_string(), + QuoteType::SWAP => "SWAP".to_string(), + QuoteType::WH => "WH".to_string(), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Quote { + #[serde(rename = "type")] + pub r#type: String, + #[serde(rename = "effectiveAmountIn")] + pub effective_amount_in: f64, + #[serde(rename = "expectedAmountOut")] + pub expected_amount_out: f64, + #[serde(rename = "priceImpact")] + pub price_impact: Option, + #[serde(rename = "minAmountOut")] + pub min_amount_out: f64, + #[serde(rename = "minReceived")] + pub min_received: f64, + #[serde(rename = "gasDrop")] + pub gas_drop: f64, + pub price: f64, + #[serde(rename = "swapRelayerFee")] + pub swap_relayer_fee: Option, + #[serde(rename = "redeemRelayerFee")] + pub redeem_relayer_fee: Option, + #[serde(rename = "refundRelayerFee")] + pub refund_relayer_fee: Option, + #[serde(rename = "solanaRelayerFee")] + pub solana_relayer_fee: Option, + #[serde(rename = "refundRelayerFee64")] + pub refund_relayer_fee64: String, + #[serde(rename = "cancelRelayerFee64")] + pub cancel_relayer_fee64: String, + #[serde(rename = "submitRelayerFee64")] + pub submit_relayer_fee64: String, + #[serde(rename = "solanaRelayerFee64")] + pub solana_relayer_fee64: Option, + #[serde(rename = "clientRelayerFeeSuccess")] + pub client_relayer_fee_success: Option, + #[serde(rename = "clientRelayerFeeRefund")] + pub client_relayer_fee_refund: Option, + pub eta: u64, + #[serde(rename = "etaSeconds")] + pub eta_seconds: u64, + #[serde(rename = "clientEta")] + pub client_eta: String, + #[serde(rename = "fromToken")] + pub from_token: Token, + #[serde(rename = "toToken")] + pub to_token: Token, + #[serde(rename = "fromChain")] + pub from_chain: String, + #[serde(rename = "toChain")] + pub to_chain: String, + #[serde(rename = "slippageBps")] + pub slippage_bps: u32, + #[serde(rename = "bridgeFee")] + pub bridge_fee: f64, + #[serde(rename = "suggestedPriorityFee")] + pub suggested_priority_fee: f64, + #[serde(rename = "onlyBridging")] + pub only_bridging: bool, + #[serde(rename = "deadline64")] + pub deadline64: String, + #[serde(rename = "referrerBps")] + pub referrer_bps: Option, + #[serde(rename = "protocolBps")] + pub protocol_bps: Option, + #[serde(rename = "swiftMayanContract")] + pub swift_mayan_contract: Option, + #[serde(rename = "swiftAuctionMode")] + pub swift_auction_mode: Option, + #[serde(rename = "swiftInputContract")] + pub swift_input_contract: String, + #[serde(rename = "swiftInputDecimals")] + pub swift_input_decimals: u8, + pub gasless: bool, + pub relayer: String, + #[serde(rename = "sendTransactionCost")] + pub send_transaction_cost: f64, + #[serde(rename = "maxUserGasDrop")] + pub max_user_gas_drop: f64, + + #[serde(rename = "rentCost", deserialize_with = "deserialize_string_or_u64")] + pub rent_cost: Option, +} + +#[derive(Debug, Deserialize)] +struct QuoteResponse { + quotes: Vec, + + #[serde(rename = "minimumSdkVersion")] + pub minimum_sdk_version: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum MayanRelayerError { + #[error("Network error: {0}")] + NetworkError(String), + #[error("Invalid response: {0}")] + InvalidResponse(String), + #[error("Route not found")] + RouteNotFound, + #[error("SDK version not supported")] + SdkVersionNotSupported, + #[error("Invalid parameters: {0}")] + InvalidParameters(String), +} + +#[derive(Debug)] +pub struct MayanRelayer { + url: String, + provider: Arc, +} + +impl MayanRelayer { + pub fn new(url: String, provider: Arc) -> Self { + Self { url, provider } + } + + pub fn default_relayer(provider: Arc) -> Self { + Self::new("https://price-api.mayan.finance/v3".to_string(), provider) + } + + fn convert_to_decimals(wei_amount: u64) -> f64 { + wei_amount as f64 / 1e18 + } + + pub async fn get_quote(&self, params: QuoteParams, options: Option) -> Result, MayanRelayerError> { + let options = options.unwrap_or_default(); + let amount_decimals = Self::convert_to_decimals(params.amount); + + let mut query_params = vec![ + ("swift", options.swift.to_string()), + ("mctp", options.mctp.to_string()), + ("gasless", options.gasless.to_string()), + ("onlyDirect", options.only_direct.to_string()), + ("solanaProgram", MAYAN_PROGRAM_ID.to_string()), + ("forwarderAddress", MAYAN_FORWARDER_CONTRACT.to_string()), + ("amountIn", amount_decimals.to_string()), + ("fromToken", params.from_token), + ("fromChain", params.from_chain.to_string()), + ("toToken", params.to_token), + ("toChain", params.to_chain.to_string()), + // ("slippageBps", params.slippage_bps.map_or("auto".to_string(), |v| v.to_string())), + // ("gasDrop", params.gas_drop.unwrap_or(0).to_string()), + ("sdkVersion", "9_7_0".to_string()), + ]; + + if let Some(slippage) = params.slippage_bps { + query_params.push(("slippageBps", slippage.to_string())); + } + if let Some(gas_drop) = params.gas_drop { + query_params.push(("gasDrop", gas_drop.to_string())); + } + if let Some(referrer) = params.referrer { + query_params.push(("referrer", referrer)); + } + if let Some(referrer_bps) = params.referrer_bps { + query_params.push(("referrerBps", referrer_bps.to_string())); + } + + let query = serde_urlencoded::to_string(&query_params).map_err(|e| MayanRelayerError::InvalidParameters(e.to_string()))?; + + let url = format!("{}/quote?{}", self.url, query); + + let target = AlienTarget { + url, + method: AlienHttpMethod::Get, + headers: None, + body: None, + }; + + let data = self + .provider + .request(target) + .await + .map_err(|err| MayanRelayerError::NetworkError(err.to_string()))?; + + let quote_response = serde_json::from_slice::(&data); + match quote_response { + Ok(response) => { + if !self.check_sdk_version(response.minimum_sdk_version) { + return Err(MayanRelayerError::SdkVersionNotSupported); + } + + Ok(response.quotes) + } + Err(err) => { + if let Ok(api_error) = serde_json::from_slice::(&data) { + return Err(MayanRelayerError::InvalidResponse(api_error.msg)); + } + Err(MayanRelayerError::NetworkError(err.to_string())) + } + } + } + + fn check_sdk_version(&self, minimum_version: Vec) -> bool { + let sdk_version = SDK_VERSION.split('_').filter_map(|x| x.parse::().ok()).collect::>(); + + // Major version check + if sdk_version[0] < minimum_version[0] { + return false; + } + if sdk_version[0] > minimum_version[0] { + return true; + } + + // Minor version check + if sdk_version[1] < minimum_version[1] { + return false; + } + if sdk_version[1] > minimum_version[1] { + return true; + } + + if sdk_version[2] >= minimum_version[2] { + return true; + } + + false + } +} + +#[cfg(test)] +mod tests { + use std::{sync::Arc, time::Duration}; + + use crate::network::mock::AlienProviderMock; + + use super::*; + + #[test] + fn test_quote_deserialization() { + let json_data = r#" + { + "type": "SWIFT", + "effectiveAmountIn": 0.01, + "expectedAmountOut": 0.002369675180011231, + "priceImpact": null, + "minAmountOut": 0.002345978428211119, + "minReceived": 0.002345978428211119, + "gasDrop": 0, + "price": 0.9996900030000001, + "swapRelayerFee": null, + "redeemRelayerFee": null, + "refundRelayerFee": null, + "solanaRelayerFee": null, + "refundRelayerFee64": "1110", + "cancelRelayerFee64": "299797", + "submitRelayerFee64": "0", + "solanaRelayerFee64": null, + "clientRelayerFeeSuccess": null, + "clientRelayerFeeRefund": 10.7424099907, + "eta": 1, + "etaSeconds": 20, + "clientEta": "20s", + "fromToken": { "name": "ETH", "symbol": "ETH", "mint": "", "contract": "0x0000000000000000000000000000000000000000", "chain_id": 8453, "w_chain_id": 30, "decimals": 18, "logo_uri": "", "coingecko_id": "eth", "real_origin_chain_id": null, "real_origin_contract_address": null, "supports_permit": false, "standard": "native" }, + "toToken": { "name": "ETH", "symbol": "ETH", "mint": "", "contract": "0x0000000000000000000000000000000000000000", "chain_id": 1, "w_chain_id": 2, "decimals": 18, "logo_uri": "", "coingecko_id": "eth", "real_origin_chain_id": null, "real_origin_contract_address": null, "supports_permit": false, "standard": "native" }, + "fromChain": "base", + "toChain": "ethereum", + "slippageBps": 100, + "bridgeFee": 0, + "suggestedPriorityFee": 0, + "onlyBridging": false, + "deadline64": "1732727937", + "referrerBps": 0, + "protocolBps": 3, + "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", + "swiftAuctionMode": 2, + "swiftInputContract": "0x0000000000000000000000000000000000000000", + "swiftInputDecimals": 18, + "gasless": false, + "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", + "sendTransactionCost": 0, + "maxUserGasDrop": 0.0007843624845837177, + "rentCost": 40000000 + }"#; + + let quote: Quote = serde_json::from_str(json_data).expect("Failed to deserialize Quote"); + assert_eq!(quote.r#type, "SWIFT"); + assert!(quote.price_impact.is_none()); + assert_eq!(quote.swift_input_decimals, 18); + } + + #[test] + fn test_token_deserialization() { + let json_data = r#" + { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 8453, + "wChainId": 30, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "realOriginChainId": 30, + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "supportsPermit": false, + "hasAuction": true + }"#; + + let token: Token = serde_json::from_str(json_data).expect("Failed to deserialize Token"); + assert_eq!(token.name, "ETH"); + assert!(token.verified); + assert_eq!(token.chain_id, Some(8453)); + assert_eq!(token.has_auction, true); + assert_eq!(token.wrapped_address.unwrap(), "0x4200000000000000000000000000000000000006"); + } + + #[test] + fn test_quote_response_deserialization() { + let json_data = r#" + { + "quotes": [], + "minimumSdkVersion": [7, 0, 0] + }"#; + + let response: QuoteResponse = serde_json::from_str(json_data).expect("Failed to deserialize QuoteResponse"); + assert_eq!(response.minimum_sdk_version, vec![7, 0, 0]); + } +} diff --git a/gemstone/src/swapper/mayan/mayan_swift_contract.rs b/gemstone/src/swapper/mayan/mayan_swift.rs similarity index 67% rename from gemstone/src/swapper/mayan/mayan_swift_contract.rs rename to gemstone/src/swapper/mayan/mayan_swift.rs index fb2eadd4..caab0166 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_contract.rs +++ b/gemstone/src/swapper/mayan/mayan_swift.rs @@ -12,20 +12,20 @@ use gem_evm::{ address::EthereumAddress, erc20::IERC20, jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::swift::swift::MayanSwift, + mayan::swift::swift::IMayanSwift, }; use primitives::Chain; use std::{str::FromStr, sync::Arc}; use thiserror::Error; -pub struct MayanSwiftContract { +pub struct MayanSwift { address: String, provider: Arc, chain: Chain, } #[derive(Error, Debug)] -pub enum MayanSwiftContractError { +pub enum MayanSwiftError { #[error("Call failed: {msg}")] CallFailed { msg: String }, @@ -57,6 +57,26 @@ pub struct OrderParams { pub random: [u8; 32], } +impl OrderParams { + pub fn to_contract_params(&self) -> IMayanSwift::OrderParams { + IMayanSwift::OrderParams { + trader: self.trader.into(), + tokenOut: self.token_out.into(), + minAmountOut: self.min_amount_out, + gasDrop: self.gas_drop, + cancelFee: self.cancel_fee, + refundFee: self.refund_fee, + deadline: self.deadline, + destAddr: self.dest_addr.into(), + destChainId: self.dest_chain_id, + referrerAddr: self.referrer_addr.into(), + referrerBps: self.referrer_bps, + auctionMode: self.auction_mode, + random: self.random.into(), + } + } +} + #[derive(Debug)] pub struct PermitParams { pub value: String, @@ -66,38 +86,56 @@ pub struct PermitParams { pub s: [u8; 32], } -impl MayanSwiftContract { +#[derive(Debug, Clone)] +pub struct KeyStruct { + pub params: OrderParams, + pub token_in: [u8; 32], + pub chain_id: u16, + pub protocol_bps: u16, +} + +impl KeyStruct { + pub fn abi_encode(&self) -> Vec { + let key = IMayanSwift::KeyStruct { + params: self.params.to_contract_params(), + tokenIn: self.token_in.into(), + chainId: self.chain_id, + protocolBps: self.protocol_bps, + }; + key.abi_encode() + } +} + +impl MayanSwift { pub fn new(address: String, provider: Arc, chain: Chain) -> Self { Self { address, provider, chain } } - pub async fn get_fee_manager_address(&self) -> Result { - let call_data = MayanSwift::feeManagerCall {}.abi_encode(); + pub async fn get_fee_manager_address(&self) -> Result { + let call_data = IMayanSwift::feeManagerCall {}.abi_encode(); let fee_manager_call = EthereumRpc::Call(TransactionObject::new_call(&self.address, call_data), BlockParameter::Latest); let response = jsonrpc_call(&fee_manager_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - let decoded = HexDecode(&result).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + let decoded = HexDecode(&result).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; let fee_manager = - MayanSwift::feeManagerCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftContractError::ABIError { msg: e.to_string() })?; + IMayanSwift::feeManagerCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - let address = EthereumAddress::from_str(&fee_manager.feeManager.to_string()).map_err(|e| MayanSwiftContractError::ABIError { + let address = EthereumAddress::from_str(&fee_manager.feeManager.to_string()).map_err(|e| MayanSwiftError::InvalidResponse { msg: format!("Failed to parse fee manager address: {}", e), })?; Ok(address.to_checksum()) } - pub async fn create_order_with_eth(&self, from: &str, params: OrderParams, value: &str) -> Result { + pub async fn create_order_with_eth(&self, from: &str, params: OrderParams, value: &str) -> Result { let call_data = self - .encode_create_order_with_eth(params, U256::from_str(value).map_err(|_| MayanSwiftContractError::InvalidAmount)?) + .encode_create_order_with_eth(params, U256::from_str(value).map_err(|_| MayanSwiftError::InvalidAmount)?) .await?; let create_order_call = EthereumRpc::Call( @@ -107,34 +145,30 @@ impl MayanSwiftContract { let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; Ok(result) } - pub async fn create_order_with_token(&self, from: &str, token_in: &str, amount_in: &str, params: OrderParams) -> Result { + pub async fn create_order_with_token(&self, from: &str, token_in: &str, amount_in: &str, params: OrderParams) -> Result { let call_data = self - .encode_create_order_with_token(token_in, U256::from_str(amount_in).map_err(|_| MayanSwiftContractError::InvalidAmount)?, params) + .encode_create_order_with_token(token_in, U256::from_str(amount_in).map_err(|_| MayanSwiftError::InvalidAmount)?, params) .await?; let create_order_call = EthereumRpc::Call(TransactionObject::new_call_with_from(from, &self.address, call_data), BlockParameter::Latest); let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; Ok(result) } - pub async fn estimate_create_order_with_eth(&self, from: &str, params: OrderParams, amount: U256) -> Result { + pub async fn estimate_create_order_with_eth(&self, from: &str, params: OrderParams, amount: U256) -> Result { let call_data = self.encode_create_order_with_eth(params, amount).await?; // let value = encode_prefixed(amount.to_be_bytes_vec()); @@ -144,36 +178,32 @@ impl MayanSwiftContract { let response = jsonrpc_call(&estimate_gas_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; let hex_str = result.trim_start_matches("0x"); - Ok(U256::from_str_radix(hex_str, 16).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?) + Ok(U256::from_str_radix(hex_str, 16).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?) } - pub async fn estimate_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result { + pub async fn estimate_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result { let call_data = self.encode_create_order_with_token(token_in, amount, params).await?; let estimate_gas_call = EthereumRpc::EstimateGas(TransactionObject::new_call_with_value(&self.address, token_in, call_data, &amount.to_string())); let response = jsonrpc_call(&estimate_gas_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let decoded = HexDecode(&result).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + let decoded = HexDecode(&result).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - Ok(U256::from_str(decoded.encode_hex().as_str()).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?) + Ok(U256::from_str(decoded.encode_hex().as_str()).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?) } - pub async fn encode_create_order_with_eth(&self, params: OrderParams, amount: U256) -> Result, MayanSwiftContractError> { - let call_data = MayanSwift::createOrderWithEthCall { + pub async fn encode_create_order_with_eth(&self, params: OrderParams, amount: U256) -> Result, MayanSwiftError> { + let call_data = IMayanSwift::createOrderWithEthCall { params: self.convert_order_params(params), } .abi_encode(); @@ -181,9 +211,9 @@ impl MayanSwiftContract { Ok(call_data) } - pub async fn encode_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result, MayanSwiftContractError> { - let call_data = MayanSwift::createOrderWithTokenCall { - tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftContractError::ABIError { + pub async fn encode_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result, MayanSwiftError> { + let call_data = IMayanSwift::createOrderWithTokenCall { + tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftError::ABIError { msg: format!("Invalid token address: {}", e), })?, amountIn: amount, @@ -194,8 +224,8 @@ impl MayanSwiftContract { Ok(call_data) } - pub async fn get_orders(&self, order_hashes: Vec<[u8; 32]>) -> Result, MayanSwiftContractError> { - let call_data = MayanSwift::getOrdersCall { + pub async fn get_orders(&self, order_hashes: Vec<[u8; 32]>) -> Result, MayanSwiftError> { + let call_data = IMayanSwift::getOrdersCall { orderHashes: order_hashes.into_iter().map(|x| x.into()).collect(), } .abi_encode(); @@ -204,15 +234,13 @@ impl MayanSwiftContract { let response = jsonrpc_call(&get_orders_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let decoded = HexDecode(&result).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + let decoded = HexDecode(&result).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - let orders = MayanSwift::getOrdersCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftContractError::ABIError { msg: e.to_string() })?; + let orders = IMayanSwift::getOrdersCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftError::ABIError { msg: e.to_string() })?; Ok(orders ._0 @@ -221,13 +249,13 @@ impl MayanSwiftContract { .collect()) } - pub async fn check_token_approval(&self, owner: &str, token: &str, amount: &str) -> Result { + pub async fn check_token_approval(&self, owner: &str, token: &str, amount: &str) -> Result { // Encode allowance call for ERC20 token let call_data = IERC20::allowanceCall { - owner: Address::from_str(owner).map_err(|e| MayanSwiftContractError::ABIError { + owner: Address::from_str(owner).map_err(|e| MayanSwiftError::ABIError { msg: format!("Invalid owner address: {}", e), })?, - spender: Address::from_str(&self.address).map_err(|e| MayanSwiftContractError::ABIError { + spender: Address::from_str(&self.address).map_err(|e| MayanSwiftError::ABIError { msg: format!("Invalid spender address: {}", e), })?, } @@ -239,19 +267,17 @@ impl MayanSwiftContract { // Execute the call let response = jsonrpc_call(&allowance_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; // Decode the response - let decoded = hex::decode(result.trim_start_matches("0x")).map_err(|e| MayanSwiftContractError::InvalidResponse { msg: e.to_string() })?; + let decoded = hex::decode(result.trim_start_matches("0x")).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftContractError::ABIError { msg: e.to_string() })?; + let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; // Convert amount string to U256 for comparison - let required_amount = U256::from_str(amount).map_err(|e| MayanSwiftContractError::ABIError { + let required_amount = U256::from_str(amount).map_err(|e| MayanSwiftError::ABIError { msg: format!("Invalid amount: {}", e), })?; @@ -275,9 +301,9 @@ impl MayanSwiftContract { submission_fee: U256, signed_order_hash: Vec, permit_params: PermitParams, - ) -> Result, MayanSwiftContractError> { - let call_data = MayanSwift::createOrderWithSigCall { - tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftContractError::ABIError { + ) -> Result, MayanSwiftError> { + let call_data = IMayanSwift::createOrderWithSigCall { + tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftError::ABIError { msg: format!("Invalid token address: {}", e), })?, amountIn: amount_in, @@ -300,13 +326,13 @@ impl MayanSwiftContract { submission_fee: &str, signed_order_hash: Vec, permit_params: PermitParams, - ) -> Result { + ) -> Result { let call_data = self .encode_create_order_with_sig( token_in, - U256::from_str(amount_in).map_err(|_| MayanSwiftContractError::InvalidAmount)?, + U256::from_str(amount_in).map_err(|_| MayanSwiftError::InvalidAmount)?, params, - U256::from_str(submission_fee).map_err(|_| MayanSwiftContractError::InvalidAmount)?, + U256::from_str(submission_fee).map_err(|_| MayanSwiftError::InvalidAmount)?, signed_order_hash, permit_params, ) @@ -316,18 +342,16 @@ impl MayanSwiftContract { let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) .await - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - let result: String = response - .extract_result() - .map_err(|e| MayanSwiftContractError::CallFailed { msg: e.to_string() })?; + let result: String = response.extract_result().map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; Ok(result) } - pub fn convert_permit_params(&self, permit_params: PermitParams) -> MayanSwift::PermitParams { - MayanSwift::PermitParams { - value: U256::from_str(&permit_params.value).map_err(|_| MayanSwiftContractError::InvalidAmount)?, + pub fn convert_permit_params(&self, permit_params: PermitParams) -> IMayanSwift::PermitParams { + IMayanSwift::PermitParams { + value: U256::from_str(&permit_params.value).unwrap(), deadline: U256::from(permit_params.deadline), v: permit_params.v.into(), r: permit_params.r.into(), @@ -336,22 +360,8 @@ impl MayanSwiftContract { } // Helper method to convert our native OrderParams to contract format - pub fn convert_order_params(&self, params: OrderParams) -> MayanSwift::OrderParams { - MayanSwift::OrderParams { - trader: params.trader.into(), - tokenOut: params.token_out.into(), - minAmountOut: params.min_amount_out, - gasDrop: params.gas_drop, - cancelFee: params.cancel_fee, - refundFee: params.refund_fee, - deadline: params.deadline, - destAddr: params.dest_addr.into(), - destChainId: params.dest_chain_id, - referrerAddr: params.referrer_addr.into(), - referrerBps: params.referrer_bps, - auctionMode: params.auction_mode, - random: params.random.into(), - } + pub fn convert_order_params(&self, params: OrderParams) -> IMayanSwift::OrderParams { + params.to_contract_params() } } diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs index 4db66d1a..444b29ba 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -1,14 +1,16 @@ +use rand::Rng; use std::{str::FromStr, sync::Arc}; use alloy_core::{hex::ToHexExt, primitives::Address}; -use alloy_primitives::U256; +use alloy_primitives::{keccak256, U256}; use async_trait::async_trait; use gem_evm::{ address::EthereumAddress, jsonrpc::EthereumRpc, mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains}, }; -use primitives::Chain; +use num_bigint::RandomBits; +use primitives::{Asset, AssetId, Chain}; use crate::{ network::{jsonrpc_call, AlienProvider}, @@ -19,14 +21,16 @@ use crate::{ use super::{ fee_manager::{CalcProtocolBpsParams, FeeManager}, - mayan_swift_contract::{MayanSwiftContract, MayanSwiftContractError, OrderParams}, + forwarder::MayanForwarder, + mayan_relayer::{self, MayanRelayer, Quote, QuoteOptions, QuoteParams, QuoteType, MAYAN_FORWARDER_CONTRACT}, + mayan_swift::{KeyStruct, MayanSwift, MayanSwiftError, OrderParams, PermitParams}, }; #[derive(Debug)] pub struct MayanSwiftProvider {} -impl From for SwapperError { - fn from(err: MayanSwiftContractError) -> Self { +impl From for SwapperError { + fn from(err: MayanSwiftError) -> Self { SwapperError::NetworkError { msg: err.to_string() } } } @@ -49,7 +53,7 @@ impl MayanSwiftProvider { let deployment = get_swift_deployment_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; - let swift_contract = MayanSwiftContract::new(deployment.address, provider.clone(), request.from_asset.chain); + let swift_contract = MayanSwift::new(deployment.address, provider.clone(), request.from_asset.chain); let amount = &request.value; swift_contract @@ -58,121 +62,131 @@ impl MayanSwiftProvider { .map_err(|e| SwapperError::ABIError { msg: e.to_string() }) } - async fn calculate_output_value( - &self, - request: &SwapQuoteRequest, - provider: Arc, - order_params: &OrderParams, - ) -> Result { - let fee_manager_address = Self::get_address_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; - let fee_manager = FeeManager::new(fee_manager_address); - - let token_out = if let Some(token_id) = &request.to_asset.token_id { - let mut bytes = [0u8; 32]; - if let Ok(addr) = EthereumAddress::from_str(token_id) { - bytes.copy_from_slice(&addr.bytes); - } - bytes + fn add_referrer(&self, request: &SwapQuoteRequest, order_params: &mut OrderParams) { + // TODO: implement if needed + } + + fn build_swift_order_params(&self, request: &SwapQuoteRequest, quote: &Quote) -> Result { + let deadline = quote.deadline64.parse::().map_err(|_| SwapperError::InvalidRoute)?; + + let trader_address = self.address_to_bytes32(&request.wallet_address)?; + let destination_address = self.address_to_bytes32(&request.destination_address)?; + + // Handle the output token address + let token_out = if let to_token_contract = "e.to_token.contract { + self.address_to_bytes32(to_token_contract)? } else { - [0u8; 32] + return Err(SwapperError::InvalidAddress { + address: "Missing to_token contract address".to_string(), + }); }; - let fees = fee_manager - .calc_protocol_bps( - request.wallet_address.clone(), - &request.from_asset.chain, - provider.clone(), - CalcProtocolBpsParams { - amount_in: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, - token_in: EthereumAddress::zero(), - token_out: token_out.into(), - dest_chain: request.to_asset.chain.network_id().parse().unwrap(), - referrer_bps: 0, - }, - ) - .await - .map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; - - // TODO: do something with fees - let output_value = U256::from_str(request.value.as_str()).map_err(|_| SwapperError::InvalidAmount)?; + // Calculate the minimum amount out in smallest unit + let min_amount_out = self.convert_amount_to_wei(quote.min_amount_out, &request.to_asset)?; - // Calculate output value with fees - let output_value = output_value.checked_sub(U256::from(fees)).ok_or(SwapperError::ComputeQuoteError { - msg: "Protocol fees calculation error".to_string(), - })?; + // Calculate the gas drop in smallest unit + let gas_drop = self.convert_amount_to_wei(quote.gas_drop, &request.to_asset)?; - Ok(output_value.to_string()) - } + let random_bytes = Self::generate_random_bytes32(); // TODO - fn add_referrer(&self, request: &SwapQuoteRequest, order_params: &mut OrderParams) { - // let referrer_bps = if let Some(options) = &request.options { - // if let Some(ref_fees) = &options.fee { - // if let Ok(addr) = EthereumAddress::from_str(&ref_fees.evm.address) { - // order_params.referrer_addr.copy_from_slice(&addr.bytes); - // } - // ref_fees.evm.bps as u8 - // } else { - // 0 - // } + // Handle referrer address + // let referrer_address = if let Some(referrer) = &request.options { + // self.address_to_bytes32(referrer)? // } else { - // 0 + // [0u8; 32] // }; - // TODO: implement + // Create the order params + // + let params = OrderParams { + trader: trader_address, + token_out, + min_amount_out: min_amount_out.parse().map_err(|_| SwapperError::InvalidAmount)?, + gas_drop: gas_drop.parse().map_err(|_| SwapperError::InvalidAmount)?, + cancel_fee: quote.cancel_relayer_fee64.parse::().map_err(|_| SwapperError::InvalidAmount)?, + refund_fee: quote.refund_relayer_fee64.parse::().map_err(|_| SwapperError::InvalidAmount)?, + deadline: quote.deadline64.parse().map_err(|_| SwapperError::InvalidRoute)?, + dest_addr: destination_address, + dest_chain_id: request.to_asset.chain.network_id().parse().map_err(|_| SwapperError::InvalidAmount)?, + referrer_addr: [0u8; 32], // Add referrer logic if applicable + referrer_bps: 0u8, + auction_mode: quote.swift_auction_mode.unwrap_or(0), + random: random_bytes, + }; + + Ok(params) + } + + fn generate_random_bytes32() -> [u8; 32] { + let mut rng = rand::thread_rng(); + let mut random_bytes = [0u8; 32]; + rng.fill(&mut random_bytes); + random_bytes + } + + fn address_to_bytes32(&self, address: &str) -> Result<[u8; 32], SwapperError> { + let addr = EthereumAddress::from_str(address).map_err(|_| SwapperError::InvalidAddress { address: address.to_string() })?; + let mut bytes32 = [0u8; 32]; + bytes32[12..].copy_from_slice(&addr.bytes); + Ok(bytes32) } - fn build_swift_order_params(&self, request: &SwapQuoteRequest) -> Result { - let mut order_params = OrderParams { - trader: [0u8; 32], - token_out: [0u8; 32], - min_amount_out: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, // TODO:: - // do i need to calculate output + fees here? - gas_drop: 0, - cancel_fee: 0, - refund_fee: 0, - deadline: (std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + 3600) as u64, - dest_addr: [0u8; 32], - dest_chain_id: request.to_asset.chain.network_id().parse().unwrap(), - referrer_addr: [0u8; 32], - referrer_bps: 0, - auction_mode: 0, - random: [0u8; 32], + async fn fetch_quote_from_request(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + let mayan_relayer = MayanRelayer::default_relayer(provider.clone()); + let quote_params = QuoteParams { + amount: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, + from_token: request.from_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), + from_chain: request.from_asset.chain.clone(), + to_token: request.to_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), + to_chain: request.to_asset.chain.clone(), + slippage_bps: Some(100), + gas_drop: None, + referrer: None, + referrer_bps: None, }; - // TODO: move to separated method to test - let token_in = if request.from_asset.is_native() { - EthereumAddress::zero() - } else { - EthereumAddress::from_str(request.from_asset.token_id.as_ref().ok_or(SwapperError::NotSupportedAsset)?).map_err(|_| { - SwapperError::InvalidAddress { - address: request.from_asset.token_id.clone().unwrap(), - } - })? + let quote_options = QuoteOptions { + swift: true, + mctp: false, + gasless: false, + only_direct: false, }; - if let Ok(wallet_addr) = EthereumAddress::from_str(&request.wallet_address) { - let mut trader_bytes = [0u8; 32]; - trader_bytes[12..].copy_from_slice(&wallet_addr.bytes); - order_params.trader.copy_from_slice(&trader_bytes); - } + let quote = mayan_relayer + .get_quote(quote_params, Some(quote_options)) + .await + .map_err(|e| SwapperError::ComputeQuoteError { + msg: format!("Mayan relayer quote error: {:?}", e), + })?; - // Set destination address - if let Ok(dest_addr) = EthereumAddress::from_str(&request.destination_address) { - let mut dest_bytes = [0u8; 32]; - dest_bytes[12..].copy_from_slice(&dest_addr.bytes); - order_params.dest_addr.copy_from_slice(&dest_bytes); - } + // TODO: adjust to find most effective quote + let most_effective_qoute = quote.into_iter().filter(|x| x.r#type == QuoteType::SWIFT.to_string()).last(); + + most_effective_qoute.ok_or(SwapperError::ComputeQuoteError { + msg: "Quote is not available".to_string(), + }) + } + + fn convert_amount_to_wei(&self, amount: f64, asset_id: &AssetId) -> Result { + // Retrieve asset information based on the provided AssetId + let asset = Asset::from_chain(asset_id.chain); - // Set token_out for the destination token - if let Some(token_id) = &request.to_asset.token_id { - if let Ok(token_addr) = EthereumAddress::from_str(token_id) { - let mut token_bytes = [0u8; 32]; - token_bytes[12..].copy_from_slice(&token_addr.bytes); - order_params.token_out.copy_from_slice(&token_bytes); - } + // Get the decimals for the asset + let decimals = asset.decimals; + + // Calculate the scaling factor (10^decimals) + let scaling_factor = 10f64.powi(decimals as i32); + + // Convert the amount to Wei (or the smallest unit) + let amount_in_wei = (amount * scaling_factor).round(); // `round` ensures correct conversion + + // Ensure the amount is within a valid range for integers + if amount_in_wei < 0.0 || amount_in_wei > (u64::MAX as f64) { + return Err(SwapperError::InvalidAmount); } - Ok(order_params) + // Convert the result to a string for return + Ok(format!("{:.0}", amount_in_wei)) } } @@ -188,72 +202,86 @@ impl GemSwapProvider for MayanSwiftProvider { async fn fetch_quote_data(&self, quote: &SwapQuote, provider: Arc, data: FetchQuoteData) -> Result { let request = "e.request; - let swift_address = Self::get_address_by_chain(quote.request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; - let swift_contract = MayanSwiftContract::new(swift_address.clone(), provider.clone(), quote.request.from_asset.chain); - let swift_order_params = self - .build_swift_order_params(request) - .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; - - let amount_in = quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?; - let data = if quote.request.from_asset.is_native() { + let mayan_quote = self.fetch_quote_from_request(&request, provider.clone()).await?; + let swift_address = if let Some(address) = &mayan_quote.swift_mayan_contract { + address.clone() + } else { + return Err(SwapperError::ComputeQuoteError { + msg: "No swift_mayan_contract in quote".to_string(), + }); + }; + let swift_contract = MayanSwift::new(swift_address.clone(), provider.clone(), request.from_asset.chain); + let swift_order_params = self.build_swift_order_params("e.request, &mayan_quote)?; + let forwarder = MayanForwarder::new(MAYAN_FORWARDER_CONTRACT.to_string(), provider.clone(), request.from_asset.chain); + + let swift_call_data = if request.from_asset.is_native() { swift_contract - .encode_create_order_with_eth(swift_order_params, amount_in) + .encode_create_order_with_eth(swift_order_params, quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?) .await .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? } else { swift_contract - .encode_create_order_with_token(request.from_asset.token_id.as_ref().unwrap(), amount_in, swift_order_params) + .encode_create_order_with_token( + request.from_asset.token_id.as_ref().ok_or(SwapperError::InvalidAddress { + address: request.from_asset.to_string(), + })?, + quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?, + swift_order_params, + ) .await .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? }; + let mut value = quote.from_value.clone(); + + let forwarder_call_data = if request.from_asset.is_native() { + forwarder + .encode_forward_eth_call(swift_address.as_str(), swift_call_data.clone()) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + } else { + todo!() + }; + Ok(SwapQuoteData { - to: swift_address, - value: quote.from_value.clone(), - data: data.encode_hex(), + to: MAYAN_FORWARDER_CONTRACT.to_string(), + value: value.clone(), + data: forwarder_call_data.encode_hex(), }) } async fn get_transaction_status(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { - let receipt_call = EthereumRpc::GetTransactionReceipt(transaction_hash.to_string()); - - let response = jsonrpc_call(&receipt_call, provider, &chain) - .await - .map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; - - Ok(result == "0x1") + todo!(); + // let receipt_call = EthereumRpc::GetTransactionReceipt(transaction_hash.to_string()); + // + // let response = jsonrpc_call(&receipt_call, provider, &chain) + // .await + // .map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; + // + // let result: serde_json::Value = response.extract_result().map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; + // + // if let Some(status_hex) = result.get("status").and_then(|s| s.as_str()) { + // let status = U256::from_str_radix(status_hex.trim_start_matches("0x"), 16).unwrap_or_else(|_| U256::zero()); + // Ok(!status.is_zero()) + // } else { + // Ok(false) + // } } - async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { // Validate chain support if !self.supported_chains().contains(&request.from_asset.chain) { return Err(SwapperError::NotSupportedChain); } - let swift_order_params = self - .build_swift_order_params(request) - .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; + let quote = self.fetch_quote_from_request(request, provider.clone()).await?; - // Check approvals if needed - let approval = self.check_approval(request, provider.clone()).await?; + if quote.r#type != QuoteType::SWIFT.to_string() { + return Err(SwapperError::ComputeQuoteError { + msg: "Quote type is not SWIFT".to_string(), + }); + } - // Get fee manager address for referral info - let swift_address = Self::get_address_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; - let swift_contract = MayanSwiftContract::new(swift_address.clone(), provider.clone(), request.from_asset.chain); - let amount_in = U256::from_str(&request.value).map_err(|_| SwapperError::InvalidAmount)?; - let estimated_gas = if request.from_asset.is_native() { - swift_contract - .estimate_create_order_with_eth(request.wallet_address.as_str(), swift_order_params.clone(), amount_in) - .await - .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? - } else { - swift_contract - .estimate_create_order_with_token(request.from_asset.token_id.as_ref().unwrap(), amount_in, swift_order_params.clone()) - .await - .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? - }; // Create route information let route = SwapRoute { route_type: "swift-order".to_string(), @@ -263,17 +291,19 @@ impl GemSwapProvider for MayanSwiftProvider { .clone() .unwrap_or_else(|| request.from_asset.chain.as_ref().to_string()), output: request.to_asset.token_id.clone().unwrap_or_else(|| request.to_asset.chain.as_ref().to_string()), - fee_tier: "0".to_string(), // MayanSwift doesn't use fee tiers - gas_estimate: Some(estimated_gas.to_string()), // TODO: check if this is correct + fee_tier: "0".to_string(), + gas_estimate: None, }; - let output_value = self.calculate_output_value(request, provider.clone(), &swift_order_params).await?; + let approval = self.check_approval(request, provider.clone()).await?; Ok(SwapQuote { from_value: request.value.clone(), - to_value: output_value, + to_value: self + .convert_amount_to_wei(quote.min_amount_out, &request.to_asset) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?, data: SwapProviderData { - provider: self.provider().clone(), + provider: self.provider(), routes: vec![route], }, approval, @@ -318,36 +348,4 @@ mod tests { assert!(chains.contains(&Chain::Optimism)); assert!(chains.contains(&Chain::Base)); } - - #[test] - fn test_address_parameters() { - let wallet = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; - let destination = "0x1234567890123456789012345678901234567890"; - - let request = SwapQuoteRequest { - from_asset: AssetId::from_chain(Chain::Base), - to_asset: AssetId::from_chain(Chain::Ethereum), - wallet_address: wallet.to_string(), - destination_address: destination.to_string(), - value: "292840000000000".to_string(), - mode: GemSwapMode::ExactIn, - options: None, - }; - - // Create provider and get params - let provider = Arc::new(MockProvider::new()); - let swift_provider = MayanSwiftProvider::new(); - - let params = tokio_test::block_on(swift_provider.build_swift_order_params(&request, provider)).unwrap(); - - // Verify trader address (wallet) - let wallet_addr = EthereumAddress::from_str(wallet).unwrap(); - assert_eq!(¶ms.trader[12..], &wallet_addr.bytes); - assert_eq!(¶ms.trader[..12], &[0u8; 12]); // First 12 bytes should be zero - - // Verify destination address - let dest_addr = EthereumAddress::from_str(destination).unwrap(); - assert_eq!(¶ms.dest_addr[12..], &dest_addr.bytes); - assert_eq!(¶ms.dest_addr[..12], &[0u8; 12]); - } } diff --git a/gemstone/src/swapper/mayan/mod.rs b/gemstone/src/swapper/mayan/mod.rs index 8283c3b2..efd51687 100644 --- a/gemstone/src/swapper/mayan/mod.rs +++ b/gemstone/src/swapper/mayan/mod.rs @@ -1,3 +1,5 @@ mod fee_manager; -pub mod mayan_swift_contract; +pub mod forwarder; +mod mayan_relayer; +pub mod mayan_swift; pub mod mayan_swift_provider; diff --git a/gemstone/tests/integration_test.rs b/gemstone/tests/integration_test.rs index d5394ca6..d73ba607 100644 --- a/gemstone/tests/integration_test.rs +++ b/gemstone/tests/integration_test.rs @@ -6,10 +6,7 @@ mod tests { network::{provider::AlienProvider, target::*, *}, swapper::{orca::Orca, *}, }; - use mayan::{ - mayan_swift_contract::{MayanSwiftContract, MayanSwiftContractError}, - mayan_swift_provider::MayanSwiftProvider, - }; + use mayan::mayan_swift_provider::MayanSwiftProvider; use primitives::{Asset, AssetId, Chain}; use reqwest::Client; use std::{collections::HashMap, sync::Arc}; @@ -119,29 +116,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_swift_get_fee_manager() -> Result<(), MayanSwiftContractError> { - const TEST_SWIFT_ADDRESS: &str = "0xC38e4e6A15593f908255214653d3D947CA1c2338"; - const TEST_FEE_MANAGER_ADDRESS: &str = "0xF93191d350117723DBEda5484a3b0996d285CECF"; - - // Setup Base chain node config - let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".into())]); - let network_provider = Arc::new(NativeProvider::new(node_config)); - - let swift_provider = MayanSwiftContract::new(TEST_SWIFT_ADDRESS.to_string(), network_provider, Chain::Base); - - // Get fee manager address - let fee_manager_address = swift_provider.get_fee_manager_address().await?; - println!("Fee Manager Address: {}", fee_manager_address); - - // Verify the address format - assert!(fee_manager_address.starts_with("0x")); - assert_eq!(fee_manager_address.len(), 42); // Standard Ethereum address length - assert_eq!(fee_manager_address, TEST_FEE_MANAGER_ADDRESS); - - Ok(()) - } - #[tokio::test] async fn test_mayan_swift_quote() -> Result<(), SwapperError> { const TEST_WALLET_ADDRESS: &str = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; @@ -157,15 +131,16 @@ mod tests { to_asset: AssetId::from_chain(Chain::Ethereum), wallet_address: TEST_WALLET_ADDRESS.to_string(), destination_address: TEST_WALLET_ADDRESS.to_string(), - value: "10000000000000".to_string(), + value: "100000000000000000".to_string(), mode: GemSwapMode::ExactIn, // Swap mode options: None, }; let quote = mayan_swift_provider.fetch_quote(&request, network_provider.clone()).await?; - assert_eq!(quote.from_value, "10000000000000"); - assert_eq!(quote.to_value, "10000000000000"); + assert_eq!(quote.from_value, "100000000000000000"); + // Expect the to_value to be + assert!(quote.to_value.parse::().unwrap() > 0); // Verify assert_eq!(quote.data.routes.len(), 1); @@ -180,32 +155,4 @@ mod tests { Ok(()) } - - #[tokio::test] - async fn test_mayan_swift_quote_data() -> Result<(), SwapperError> { - let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".into())]); - let network_provider = Arc::new(NativeProvider::new(node_config)); - - let mayan_swift_provider = MayanSwiftProvider::new(); - - // Create - let request = SwapQuoteRequest { - from_asset: AssetId::from_chain(Chain::Base), - to_asset: AssetId::from_chain(Chain::Ethereum), - wallet_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".to_string(), - destination_address: "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c".to_string(), - value: "10000000000000".to_string(), // 0.00001 ETH - mode: GemSwapMode::ExactIn, - options: None, - }; - - let quote = mayan_swift_provider.fetch_quote(&request, network_provider.clone()).await?; - let quote_data = mayan_swift_provider - .fetch_quote_data("e, network_provider.clone(), FetchQuoteData::None) - .await; - - assert!(quote_data.is_ok()); - - Ok(()) - } } From 9c9cef866103a2a9105de27117ddb5c3e2a4ab86 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 28 Nov 2024 14:19:54 +0200 Subject: [PATCH 03/15] fix: fixed min amount out and destination chain id --- gemstone/src/swapper/mayan/mayan_relayer.rs | 139 ++++++--- .../src/swapper/mayan/mayan_swift_provider.rs | 277 ++++++++++++++++-- 2 files changed, 346 insertions(+), 70 deletions(-) diff --git a/gemstone/src/swapper/mayan/mayan_relayer.rs b/gemstone/src/swapper/mayan/mayan_relayer.rs index a7edbfa3..14de2fa8 100644 --- a/gemstone/src/swapper/mayan/mayan_relayer.rs +++ b/gemstone/src/swapper/mayan/mayan_relayer.rs @@ -351,48 +351,103 @@ mod tests { fn test_quote_deserialization() { let json_data = r#" { - "type": "SWIFT", - "effectiveAmountIn": 0.01, - "expectedAmountOut": 0.002369675180011231, - "priceImpact": null, - "minAmountOut": 0.002345978428211119, - "minReceived": 0.002345978428211119, - "gasDrop": 0, - "price": 0.9996900030000001, - "swapRelayerFee": null, - "redeemRelayerFee": null, - "refundRelayerFee": null, - "solanaRelayerFee": null, - "refundRelayerFee64": "1110", - "cancelRelayerFee64": "299797", - "submitRelayerFee64": "0", - "solanaRelayerFee64": null, - "clientRelayerFeeSuccess": null, - "clientRelayerFeeRefund": 10.7424099907, - "eta": 1, - "etaSeconds": 20, - "clientEta": "20s", - "fromToken": { "name": "ETH", "symbol": "ETH", "mint": "", "contract": "0x0000000000000000000000000000000000000000", "chain_id": 8453, "w_chain_id": 30, "decimals": 18, "logo_uri": "", "coingecko_id": "eth", "real_origin_chain_id": null, "real_origin_contract_address": null, "supports_permit": false, "standard": "native" }, - "toToken": { "name": "ETH", "symbol": "ETH", "mint": "", "contract": "0x0000000000000000000000000000000000000000", "chain_id": 1, "w_chain_id": 2, "decimals": 18, "logo_uri": "", "coingecko_id": "eth", "real_origin_chain_id": null, "real_origin_contract_address": null, "supports_permit": false, "standard": "native" }, - "fromChain": "base", - "toChain": "ethereum", - "slippageBps": 100, - "bridgeFee": 0, - "suggestedPriorityFee": 0, - "onlyBridging": false, - "deadline64": "1732727937", - "referrerBps": 0, - "protocolBps": 3, - "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", - "swiftAuctionMode": 2, - "swiftInputContract": "0x0000000000000000000000000000000000000000", - "swiftInputDecimals": 18, - "gasless": false, - "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", - "sendTransactionCost": 0, - "maxUserGasDrop": 0.0007843624845837177, - "rentCost": 40000000 - }"#; + "maxUserGasDrop": 0.01, + "sendTransactionCost": 0, + "rentCost": "40000000", + "gasless": false, + "swiftAuctionMode": 2, + "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", + "minMiddleAmount": null, + "swiftInputContract": "0x0000000000000000000000000000000000000000", + "swiftInputDecimals": 18, + "slippageBps": 100, + "effectiveAmountIn": 0.1, + "expectedAmountOut": 0.09972757016582551, + "price": 0.9996900030000002, + "priceImpact": null, + "minAmountOut": 0.09873029446416727, + "minReceived": 0.09873029446416727, + "route": null, + "swapRelayerFee": null, + "swapRelayerFee64": null, + "redeemRelayerFee": null, + "redeemRelayerFee64": null, + "solanaRelayerFee": null, + "solanaRelayerFee64": null, + "refundRelayerFee": null, + "refundRelayerFee64": "1056", + "cancelRelayerFee64": "25", + "submitRelayerFee64": "0", + "deadline64": "1732777812", + "clientRelayerFeeSuccess": null, + "clientRelayerFeeRefund": 0.038947098479114796, + "fromToken": { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 8453, + "wChainId": 30, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "realOriginChainId": 30, + "supportsPermit": false, + "hasAuction": true + }, + "fromChain": "base", + "toToken": { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "8M6d63oL7dvMZ1gNbgGe3h8afMSWJEKEhtPTFM2u8h3c", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "wChainId": 24, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "realOriginChainId": 24, + "supportsPermit": false, + "hasAuction": true + }, + "toTokenPrice": 3602.87682508, + "toChain": "optimism", + "mintDecimals": null, + "gasDrop": 0, + "eta": 1, + "etaSeconds": 12, + "clientEta": "12s", + "bridgeFee": 0, + "suggestedPriorityFee": 0, + "type": "SWIFT", + "priceStat": { + "ratio": 0.9996907516582549, + "status": "GOOD" + }, + "referrerBps": 0, + "protocolBps": 3, + "onlyBridging": false, + "sourceSwapExpense": 0, + "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", + "meta": { + "advertisedDescription": "Cheapest and Fastest", + "advertisedTitle": "Best", + "icon": "https://cdn.mayan.finance/fast_icon.png", + "switchText": "Switch to the best route", + "title": "Best" + } + } + "#; let quote: Quote = serde_json::from_str(json_data).expect("Failed to deserialize Quote"); assert_eq!(quote.r#type, "SWIFT"); diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs index 444b29ba..11a24f9a 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -9,7 +9,6 @@ use gem_evm::{ jsonrpc::EthereumRpc, mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains}, }; -use num_bigint::RandomBits; use primitives::{Asset, AssetId, Chain}; use crate::{ @@ -67,37 +66,48 @@ impl MayanSwiftProvider { } fn build_swift_order_params(&self, request: &SwapQuoteRequest, quote: &Quote) -> Result { - let deadline = quote.deadline64.parse::().map_err(|_| SwapperError::InvalidRoute)?; + let deadline = quote.deadline64.parse::().map_err(|_| { + eprintln!("Failed to parse deadline: {}", quote.deadline64); + SwapperError::InvalidRoute + })?; - let trader_address = self.address_to_bytes32(&request.wallet_address)?; - let destination_address = self.address_to_bytes32(&request.destination_address)?; + let trader_address = self.address_to_bytes32(&request.wallet_address).map_err(|e| { + eprintln!("Failed to parse wallet_address: {}", request.wallet_address); + e + })?; + + let destination_address = self.address_to_bytes32(&request.destination_address).map_err(|e| { + eprintln!("Failed to parse destination_address: {}", request.destination_address); + e + })?; - // Handle the output token address let token_out = if let to_token_contract = "e.to_token.contract { - self.address_to_bytes32(to_token_contract)? + self.address_to_bytes32(to_token_contract).map_err(|e| { + eprintln!("Failed to parse to_token.contract: {}", to_token_contract); + e + })? } else { return Err(SwapperError::InvalidAddress { address: "Missing to_token contract address".to_string(), }); }; - // Calculate the minimum amount out in smallest unit - let min_amount_out = self.convert_amount_to_wei(quote.min_amount_out, &request.to_asset)?; + let asset = Asset::from_chain(request.to_asset.chain); + let to_asset_decimals = asset.decimals.to_string().parse::().unwrap_or(18); - // Calculate the gas drop in smallest unit - let gas_drop = self.convert_amount_to_wei(quote.gas_drop, &request.to_asset)?; + let min_amount_out = self.get_amount_of_fractional_amount(quote.min_amount_out, to_asset_decimals).map_err(|e| { + eprintln!("Failed to convert min_amount_out: {}", quote.min_amount_out); + e + })?; - let random_bytes = Self::generate_random_bytes32(); // TODO + let gas_drop = self.convert_amount_to_wei(quote.gas_drop, &request.to_asset).map_err(|e| { + eprintln!("Failed to convert gas_drop: {}", quote.gas_drop); + e + })?; - // Handle referrer address - // let referrer_address = if let Some(referrer) = &request.options { - // self.address_to_bytes32(referrer)? - // } else { - // [0u8; 32] - // }; + let dest_chain_id = quote.to_token.w_chain_id.unwrap(); + let random_bytes = Self::generate_random_bytes32(); - // Create the order params - // let params = OrderParams { trader: trader_address, token_out, @@ -105,10 +115,10 @@ impl MayanSwiftProvider { gas_drop: gas_drop.parse().map_err(|_| SwapperError::InvalidAmount)?, cancel_fee: quote.cancel_relayer_fee64.parse::().map_err(|_| SwapperError::InvalidAmount)?, refund_fee: quote.refund_relayer_fee64.parse::().map_err(|_| SwapperError::InvalidAmount)?, - deadline: quote.deadline64.parse().map_err(|_| SwapperError::InvalidRoute)?, + deadline, dest_addr: destination_address, - dest_chain_id: request.to_asset.chain.network_id().parse().map_err(|_| SwapperError::InvalidAmount)?, - referrer_addr: [0u8; 32], // Add referrer logic if applicable + dest_chain_id: dest_chain_id.to_string().parse().map_err(|_| SwapperError::InvalidAmount)?, + referrer_addr: [0u8; 32], referrer_bps: 0u8, auction_mode: quote.swift_auction_mode.unwrap_or(0), random: random_bytes, @@ -188,6 +198,37 @@ impl MayanSwiftProvider { // Convert the result to a string for return Ok(format!("{:.0}", amount_in_wei)) } + + fn get_amount_of_fractional_amount(&self, amount: f64, decimals: u8) -> Result { + if amount < 0.0 || !amount.is_finite() { + return Err(SwapperError::InvalidAmount); + } + + // Determine the cut factor (maximum of 8 or the provided decimals) + let cut_factor = std::cmp::min(8, decimals as i32); + + // Format the amount to cut_factor + 1 decimal places + let formatted_amount = format!("{:.precision$}", amount, precision = (cut_factor + 1) as usize); + + // Extract and truncate to cut_factor decimal places + let truncated_amount = if let Some((int_part, decimal_part)) = formatted_amount.split_once('.') { + let truncated_decimals = &decimal_part[..std::cmp::min(decimal_part.len(), cut_factor as usize)]; + format!("{}.{}", int_part, truncated_decimals) + } else { + formatted_amount + }; + + // Calculate the result scaled by 10^cut_factor + let scaled_amount = truncated_amount.parse::().map_err(|_| SwapperError::InvalidAmount)? * 10f64.powi(cut_factor); + + // Validate range + if scaled_amount < 0.0 || scaled_amount > (u64::MAX as f64) { + return Err(SwapperError::InvalidAmount); + } + + // Return the scaled amount as a string + Ok(format!("{:.0}", scaled_amount)) + } } #[async_trait] @@ -317,6 +358,7 @@ mod tests { use alloy_core::sol_types::SolValue; use alloy_primitives::U256; use primitives::AssetId; + use serde::{Deserialize, Deserializer, Serialize}; use crate::{ network::{AlienError, AlienTarget, Data}, @@ -325,14 +367,128 @@ mod tests { use super::*; - #[test] - fn test_eth_value_conversion() { - let decimal_str = "1000000000000000000"; // 1 ETH - let value = U256::from_str(decimal_str).unwrap(); + pub fn generate_mock_quote() -> Quote { + let json_data = r#" + { + "maxUserGasDrop": 0.01, + "sendTransactionCost": 0, + "rentCost": "40000000", + "gasless": false, + "swiftAuctionMode": 2, + "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", + "minMiddleAmount": null, + "swiftInputContract": "0x0000000000000000000000000000000000000000", + "swiftInputDecimals": 18, + "slippageBps": 100, + "effectiveAmountIn": 0.1, + "expectedAmountOut": 0.09972757016582551, + "price": 0.9996900030000002, + "priceImpact": null, + "minAmountOut": 0.09873029446416727, + "minReceived": 0.09873029446416727, + "route": null, + "swapRelayerFee": null, + "swapRelayerFee64": null, + "redeemRelayerFee": null, + "redeemRelayerFee64": null, + "solanaRelayerFee": null, + "solanaRelayerFee64": null, + "refundRelayerFee": null, + "refundRelayerFee64": "1056", + "cancelRelayerFee64": "25", + "submitRelayerFee64": "0", + "deadline64": "1732777812", + "clientRelayerFeeSuccess": null, + "clientRelayerFeeRefund": 0.038947098479114796, + "fromToken": { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 8453, + "wChainId": 30, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "realOriginChainId": 30, + "supportsPermit": false, + "hasAuction": true + }, + "fromChain": "base", + "toToken": { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "8M6d63oL7dvMZ1gNbgGe3h8afMSWJEKEhtPTFM2u8h3c", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "wChainId": 24, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "realOriginChainId": 24, + "supportsPermit": false, + "hasAuction": true + }, + "toTokenPrice": 3602.87682508, + "toChain": "optimism", + "mintDecimals": null, + "gasDrop": 0, + "eta": 1, + "etaSeconds": 12, + "clientEta": "12s", + "bridgeFee": 0, + "suggestedPriorityFee": 0, + "type": "SWIFT", + "priceStat": { + "ratio": 0.9996907516582549, + "status": "GOOD" + }, + "referrerBps": 0, + "protocolBps": 3, + "onlyBridging": false, + "sourceSwapExpense": 0, + "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", + "meta": { + "advertisedDescription": "Cheapest and Fastest", + "advertisedTitle": "Best", + "icon": "https://cdn.mayan.finance/fast_icon.png", + "switchText": "Switch to the best route", + "title": "Best" + } + } + "#; - let hex_value = format!("0x{}", value.to_string().encode_hex()); + let quote: Quote = serde_json::from_str(json_data).expect("Failed to deserialize Quote"); + quote + } - assert_eq!(hex_value, "0xde0b6b3a7640000"); + /// Generates a `SwapQuoteRequest` object using values directly. + pub fn generate_mock_request() -> SwapQuoteRequest { + SwapQuoteRequest { + wallet_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".to_string(), + destination_address: "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24".to_string(), + from_asset: AssetId { + chain: Chain::Base, + token_id: None, + }, + to_asset: AssetId { + chain: Chain::Optimism, + token_id: None, + }, + value: "1230000000000000000".to_string(), + mode: GemSwapMode::ExactIn, + options: None, // From JSON field "effectiveAmountIn" + } } #[test] @@ -348,4 +504,69 @@ mod tests { assert!(chains.contains(&Chain::Optimism)); assert!(chains.contains(&Chain::Base)); } + + #[test] + fn test_address_to_bytes32_valid() { + let provider = MayanSwiftProvider::new(); + let address = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; + let bytes32 = provider.address_to_bytes32(address).unwrap(); + let expected_bytes32 = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 85, 198, 171, 218, 94, 42, 82, 65, 170, 8, 72, 107, 213, 12, 247, 212, 117, 207, 36, + ]; + assert_eq!(bytes32, expected_bytes32); + } + + #[test] + fn test_address_to_bytes32_invalid() { + let provider = MayanSwiftProvider::new(); + let invalid_address = "invalid_address"; + let result = provider.address_to_bytes32(invalid_address); + assert!(result.is_err()); + } + + #[test] + fn test_convert_amount_to_wei_valid() { + let provider = MayanSwiftProvider::new(); + let asset = Asset::from_chain(Chain::Ethereum); + let amount = 1.23; // 1.23 ETH + let result = provider.convert_amount_to_wei(amount, &asset.id).unwrap(); + assert_eq!(result, "1230000000000000000"); // 1.23 ETH in Wei + } + + #[test] + fn test_convert_amount_to_wei_invalid() { + let provider = MayanSwiftProvider::new(); + let asset = Asset::from_chain(Chain::Ethereum); + let amount = -1.0; // Negative amount + let result = provider.convert_amount_to_wei(amount, &asset.id); + assert!(result.is_err()); + } + + #[test] + fn test_build_swift_order_params_valid() { + let provider = MayanSwiftProvider::new(); + let request = generate_mock_request(); + let quote = generate_mock_quote(); + + let result = provider.build_swift_order_params(&request, "e); + assert!(result.is_ok()); + } + + #[test] + fn test_get_amount_of_fractional_amount_valid() { + // Test with valid inputs and expected results + let test_cases = vec![ + (0.000075203, 18, 7520), // Regular case with precision truncation + (1.23456789, 8, 123456789), // Decimals less than 8 + (0.1, 6, 100000), // Simple rounding + (0.12345678, 8, 12345678), // Exact decimals + ]; + + for (amount, decimals, expected) in test_cases { + let provider = MayanSwiftProvider::new(); + let result = provider.get_amount_of_fractional_amount(amount, decimals); + assert!(result.is_ok(), "Failed for amount: {}", amount); + assert_eq!(result.unwrap(), expected.to_string()); + } + } } From 37daadd84dd18f4321326accead4338fc9641ba3 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Fri, 29 Nov 2024 19:42:37 +0200 Subject: [PATCH 04/15] feat: swap and forward eth implemented --- gemstone/src/swapper/mayan/forwarder.rs | 10 +-- gemstone/src/swapper/mayan/mayan_relayer.rs | 11 ++- .../src/swapper/mayan/mayan_swift_provider.rs | 87 +++++++++++++++---- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/gemstone/src/swapper/mayan/forwarder.rs b/gemstone/src/swapper/mayan/forwarder.rs index bec442a7..4e0b2b3e 100644 --- a/gemstone/src/swapper/mayan/forwarder.rs +++ b/gemstone/src/swapper/mayan/forwarder.rs @@ -25,15 +25,11 @@ pub enum MayanForwarderError { ABIError { msg: String }, } -pub struct MayanForwarder { - address: String, - provider: Arc, - chain: Chain, -} +pub struct MayanForwarder {} impl MayanForwarder { - pub fn new(address: String, provider: Arc, chain: Chain) -> Self { - Self { address, provider, chain } + pub fn new() -> Self { + Self {} } pub async fn encode_forward_eth_call(&self, mayan_protocol: &str, protocol_data: Vec) -> Result, MayanForwarderError> { diff --git a/gemstone/src/swapper/mayan/mayan_relayer.rs b/gemstone/src/swapper/mayan/mayan_relayer.rs index 14de2fa8..4bde0b7b 100644 --- a/gemstone/src/swapper/mayan/mayan_relayer.rs +++ b/gemstone/src/swapper/mayan/mayan_relayer.rs @@ -131,13 +131,22 @@ pub struct Quote { pub price_impact: Option, #[serde(rename = "minAmountOut")] pub min_amount_out: f64, + + #[serde(rename = "minMiddleAmount")] + pub min_middle_amount: Option, + + #[serde(rename = "evmSwapRouterAddress")] + pub evm_swap_router_address: Option, + + #[serde(rename = "evmSwapRouterCalldata")] + pub evm_swap_router_calldata: Option, #[serde(rename = "minReceived")] pub min_received: f64, #[serde(rename = "gasDrop")] pub gas_drop: f64, pub price: f64, #[serde(rename = "swapRelayerFee")] - pub swap_relayer_fee: Option, + pub swap_relayer_feed: Option, #[serde(rename = "redeemRelayerFee")] pub redeem_relayer_fee: Option, #[serde(rename = "refundRelayerFee")] diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs index 11a24f9a..345fde90 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -25,6 +25,8 @@ use super::{ mayan_swift::{KeyStruct, MayanSwift, MayanSwiftError, OrderParams, PermitParams}, }; +const MAYAN_ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; + #[derive(Debug)] pub struct MayanSwiftProvider {} @@ -92,13 +94,12 @@ impl MayanSwiftProvider { }); }; - let asset = Asset::from_chain(request.to_asset.chain); - let to_asset_decimals = asset.decimals.to_string().parse::().unwrap_or(18); - - let min_amount_out = self.get_amount_of_fractional_amount(quote.min_amount_out, to_asset_decimals).map_err(|e| { - eprintln!("Failed to convert min_amount_out: {}", quote.min_amount_out); - e - })?; + let min_amount_out = self + .get_amount_of_fractional_amount(quote.min_amount_out, quote.to_token.decimals) + .map_err(|e| { + eprintln!("Failed to convert min_amount_out: {}", quote.min_amount_out); + e + })?; let gas_drop = self.convert_amount_to_wei(quote.gas_drop, &request.to_asset).map_err(|e| { eprintln!("Failed to convert gas_drop: {}", quote.gas_drop); @@ -253,9 +254,9 @@ impl GemSwapProvider for MayanSwiftProvider { }; let swift_contract = MayanSwift::new(swift_address.clone(), provider.clone(), request.from_asset.chain); let swift_order_params = self.build_swift_order_params("e.request, &mayan_quote)?; - let forwarder = MayanForwarder::new(MAYAN_FORWARDER_CONTRACT.to_string(), provider.clone(), request.from_asset.chain); + let forwarder = MayanForwarder::new(); - let swift_call_data = if request.from_asset.is_native() { + let swift_call_data = if mayan_quote.swift_input_contract == MAYAN_ZERO_ADDRESS { swift_contract .encode_create_order_with_eth(swift_order_params, quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?) .await @@ -263,9 +264,7 @@ impl GemSwapProvider for MayanSwiftProvider { } else { swift_contract .encode_create_order_with_token( - request.from_asset.token_id.as_ref().ok_or(SwapperError::InvalidAddress { - address: request.from_asset.to_string(), - })?, + mayan_quote.swift_input_contract.as_str(), quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?, swift_order_params, ) @@ -274,14 +273,52 @@ impl GemSwapProvider for MayanSwiftProvider { }; let mut value = quote.from_value.clone(); - - let forwarder_call_data = if request.from_asset.is_native() { - forwarder - .encode_forward_eth_call(swift_address.as_str(), swift_call_data.clone()) - .await - .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + let forwarder_call_data = if mayan_quote.from_token.contract == mayan_quote.swift_input_contract { + if mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS { + forwarder + .encode_forward_eth_call(swift_address.as_str(), swift_call_data.clone()) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + } else { + todo!(); + } } else { - todo!() + let evm_swap_router_address = mayan_quote.evm_swap_router_address.clone().ok_or_else(|| SwapperError::ComputeQuoteError { + msg: "Missing evmSwapRouterAddress".to_string(), + })?; + let evm_swap_router_calldata = mayan_quote.evm_swap_router_calldata.clone().ok_or_else(|| SwapperError::ComputeQuoteError { + msg: "Missing evmSwapRouterCalldata".to_string(), + })?; + let min_middle_amount = mayan_quote.min_middle_amount.clone().ok_or_else(|| SwapperError::ComputeQuoteError { + msg: "Missing minMiddleAmount".to_string(), + })?; + + let token_in = mayan_quote.from_token.contract.clone(); + let formatted_min_middle_amount = self + .get_amount_of_fractional_amount(min_middle_amount, mayan_quote.swift_input_decimals) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; + let effective_amount_in = self + .get_amount_of_fractional_amount(mayan_quote.effective_amount_in, mayan_quote.from_token.decimals) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; + + if (mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS) { + forwarder + .encode_swap_and_forward_eth_call( + U256::from_str(quote.from_value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, + &evm_swap_router_address.as_str(), + hex::decode(evm_swap_router_calldata.trim_start_matches("0x")).map_err(|_| SwapperError::ABIError { + msg: "Failed to decode evm_swap_router_calldata hex string without prefix 0x ".to_string(), + })?, + &mayan_quote.swift_input_contract.as_str(), + U256::from_str(&formatted_min_middle_amount).map_err(|_| SwapperError::InvalidAmount)?, + &swift_address.as_str(), + swift_call_data, + ) + .await + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? + } else { + todo!(); + } }; Ok(SwapQuoteData { @@ -569,4 +606,16 @@ mod tests { assert_eq!(result.unwrap(), expected.to_string()); } } + + #[test] + fn test_decode_evm_calldata_valid() { + let calldata = "0x07ed2379000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd09000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000654874eb7f59c6f5b39931fc45dc45337c967c3000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000013db374b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004720000000000000000000000000000000000000000000004540004260003dc416003c01acae3d0173a93d819efdc832c7c4f153b06016452bbbe2900000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd0900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006749f4bcdef66c6c178087fd931514e99b04479e4d3d956c00020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000006280fa0775121000000000000000000000000000000000000000000000000000000006749f4bc00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000654874eb7f59c6f5b39931fc45dc45337c967c300000000000000000000000000000000000000000000000000005af3107a400000000000000000000000fc892ee4a97800000000000000000166d2f7025080000000000000000003f0a5341099c401550000000000002eb7b4329f0233e9100000000000000000000000000000000000000000000004ee7259d6914ae6c461bc00000000000000004563918244f400000000000000000000006a94d74f4300000000000000000000000000006749f462000000000000000000003b1dfde910000000000000000000000000000000000000000000000000000000000000000041923845af1a0f8e52538eb26d2d7a9d0e9fc833f801c72890cc3aec84fbc1b930696a280a0414c1cab407169bb90cf3c122f85383e9bf614e490c181bcca205b41b0000000000000000000000000000000000000000000000000000000000000000a0f2fa6b66833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000015775e5f000000000000000000000000004c421380a06c4eca27833589fcd6edb6e08f4c7c32d4f71b54bda02913111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000001ea79a4f"; + let without_prefix = calldata.trim_start_matches("0x"); + let decoded_calldata = hex::decode(without_prefix).unwrap(); + let encoded_calldata = hex::encode(&decoded_calldata); + let bytes = calldata.as_bytes(); + + assert_eq!(encoded_calldata, calldata); + assert_eq!(bytes, decoded_calldata); + } } From 1bd55198ca696b2200c001e183f2c1e893ce59ea Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Mon, 2 Dec 2024 15:47:20 +0200 Subject: [PATCH 05/15] feat: added referrer fee integration --- crates/gem_evm/src/mayan/swift/deployment.rs | 2 +- .../src/swapper/mayan/mayan_swift_provider.rs | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/crates/gem_evm/src/mayan/swift/deployment.rs b/crates/gem_evm/src/mayan/swift/deployment.rs index 364c762b..4cb0f8ac 100644 --- a/crates/gem_evm/src/mayan/swift/deployment.rs +++ b/crates/gem_evm/src/mayan/swift/deployment.rs @@ -5,7 +5,7 @@ use primitives::Chain; #[derive(Debug, Clone, PartialEq)] pub struct MayanSwiftDeployment { pub address: String, - pub wormhole_id: u16, + pub wormhole_id: u64, } pub fn get_swift_providers() -> HashMap { diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs index 345fde90..556b8481 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -7,11 +7,12 @@ use async_trait::async_trait; use gem_evm::{ address::EthereumAddress, jsonrpc::EthereumRpc, - mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains}, + mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains, get_swift_providers}, }; use primitives::{Asset, AssetId, Chain}; use crate::{ + config::swap_config::SwapReferralFee, network::{jsonrpc_call, AlienProvider}, swapper::{ ApprovalType, FetchQuoteData, GemSwapProvider, SwapProvider, SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteRequest, SwapRoute, SwapperError, @@ -45,6 +46,13 @@ impl MayanSwiftProvider { get_swift_deployment_by_chain(chain).map(|x| x.address) } + fn get_chain_by_wormhole_id(&self, wormhole_id: u64) -> Option { + get_swift_providers() + .into_iter() + .find(|(chain, deployment)| deployment.wormhole_id == wormhole_id) + .map(|(chain, _)| chain) + } + async fn check_approval(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { if request.from_asset.is_native() { return Ok(ApprovalType::None); @@ -63,11 +71,20 @@ impl MayanSwiftProvider { .map_err(|e| SwapperError::ABIError { msg: e.to_string() }) } - fn add_referrer(&self, request: &SwapQuoteRequest, order_params: &mut OrderParams) { - // TODO: implement if needed + fn to_native_wormhole_address(&self, address: &str, w_chain_id: u64) -> Result<[u8; 32], SwapperError> { + let chain = self.get_chain_by_wormhole_id(w_chain_id).ok_or(SwapperError::InvalidRoute)?; + + if chain == Chain::Solana { + todo!() + } else { + Ok(self.address_to_bytes32(address)?) + } } fn build_swift_order_params(&self, request: &SwapQuoteRequest, quote: &Quote) -> Result { + let dest_chain_id = quote.to_token.w_chain_id.unwrap(); + let from_chain_id = quote.from_token.w_chain_id.unwrap(); + let deadline = quote.deadline64.parse::().map_err(|_| { eprintln!("Failed to parse deadline: {}", quote.deadline64); SwapperError::InvalidRoute @@ -106,9 +123,10 @@ impl MayanSwiftProvider { e })?; - let dest_chain_id = quote.to_token.w_chain_id.unwrap(); let random_bytes = Self::generate_random_bytes32(); + let referrer = self.get_referrer(request)?; + let params = OrderParams { trader: trader_address, token_out, @@ -119,8 +137,12 @@ impl MayanSwiftProvider { deadline, dest_addr: destination_address, dest_chain_id: dest_chain_id.to_string().parse().map_err(|_| SwapperError::InvalidAmount)?, - referrer_addr: [0u8; 32], - referrer_bps: 0u8, + referrer_addr: self + .to_native_wormhole_address(referrer.address.as_str(), from_chain_id) + .map_err(|e| SwapperError::InvalidAddress { + address: referrer.address.clone(), + })?, + referrer_bps: referrer.bps.try_into().map_err(|_| SwapperError::InvalidAmount)?, auction_mode: quote.swift_auction_mode.unwrap_or(0), random: random_bytes, }; @@ -142,8 +164,28 @@ impl MayanSwiftProvider { Ok(bytes32) } + fn get_referrer(&self, request: &SwapQuoteRequest) -> Result { + if let Some(options) = &request.options { + if let Some(referrer) = &options.fee { + let evm_fee = &referrer.evm; + let solana_fee = &referrer.solana; + + if request.from_asset.chain == Chain::Solana { + return Ok(solana_fee.clone()); + } + + return Ok(evm_fee.clone()); + } + } + + Err(SwapperError::ComputeQuoteError { + msg: "Missing referrer".to_string(), + }) + } + async fn fetch_quote_from_request(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { let mayan_relayer = MayanRelayer::default_relayer(provider.clone()); + let referrer = self.get_referrer(request)?; let quote_params = QuoteParams { amount: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, from_token: request.from_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), @@ -152,8 +194,8 @@ impl MayanSwiftProvider { to_chain: request.to_asset.chain.clone(), slippage_bps: Some(100), gas_drop: None, - referrer: None, - referrer_bps: None, + referrer: Some(referrer.address), + referrer_bps: Some(referrer.bps), }; let quote_options = QuoteOptions { From b0abdfdebfed1e8a47ca016410a895c77bc55ef6 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Tue, 3 Dec 2024 08:40:53 +0200 Subject: [PATCH 06/15] feat: fetch erc20 swap quote --- crates/gem_evm/src/erc20.rs | 1 + gemstone/src/swapper/mayan/fee_manager.rs | 101 -------- gemstone/src/swapper/mayan/mayan_relayer.rs | 33 ++- gemstone/src/swapper/mayan/mayan_swift.rs | 235 +----------------- .../src/swapper/mayan/mayan_swift_provider.rs | 210 +++++++++------- gemstone/src/swapper/mayan/mod.rs | 1 - gemstone/src/swapper/mod.rs | 10 +- gemstone/tests/integration_test.rs | 27 +- 8 files changed, 167 insertions(+), 451 deletions(-) delete mode 100644 gemstone/src/swapper/mayan/fee_manager.rs diff --git a/crates/gem_evm/src/erc20.rs b/crates/gem_evm/src/erc20.rs index 1d34d9de..17cbe86f 100644 --- a/crates/gem_evm/src/erc20.rs +++ b/crates/gem_evm/src/erc20.rs @@ -3,5 +3,6 @@ use alloy_core::sol; sol! { interface IERC20 { function allowance(address owner, address spender) external view returns (uint256); + function decimals() external view returns (uint8); } } diff --git a/gemstone/src/swapper/mayan/fee_manager.rs b/gemstone/src/swapper/mayan/fee_manager.rs deleted file mode 100644 index 66a5d049..00000000 --- a/gemstone/src/swapper/mayan/fee_manager.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::{str::FromStr, sync::Arc}; - -use alloy_core::{ - hex::decode as HexDecode, - primitives::{Address, FixedBytes, U256, U8}, - sol_types::SolCall, -}; -use gem_evm::{ - address::EthereumAddress, - jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::fee_manager::IFeeManager, -}; -use primitives::Chain; -use thiserror::Error; - -use crate::network::{jsonrpc_call, AlienProvider}; - -#[derive(Debug, Error)] -pub enum FeeManagerError { - #[error("Only operator")] - OnlyOperator, - - #[error("Only next operator")] - OnlyNextOperator, - - #[error("Zero address")] - ZeroAddress, - - #[error("Call failed: {msg}")] - CallFailed { msg: String }, - - #[error("Invalid address: {address}")] - InvalidAddress { address: String }, - - #[error("ABI error: {msg}")] - ABIError { msg: String }, -} - -pub struct CalcProtocolBpsParams { - pub amount_in: u64, - pub token_in: EthereumAddress, - pub token_out: FixedBytes<32>, // bytes32 - pub dest_chain: u16, - pub referrer_bps: u8, -} - -pub struct SweepParams { - pub token: Option, // None for ETH, Some(address) for ERC20 - pub amount: U256, - pub to: EthereumAddress, -} - -#[derive(Debug)] -pub struct FeeManager { - address: String, -} - -impl FeeManager { - pub fn new(address: String) -> Self { - Self { address } - } - - pub async fn calc_protocol_bps( - &self, - sender: String, - chain: &Chain, - provider: Arc, - params: CalcProtocolBpsParams, - ) -> Result { - let token_in_address = Address::from_str(¶ms.token_in.to_checksum()).map_err(|_| FeeManagerError::InvalidAddress { - address: params.token_in.to_checksum(), - })?; - - let call_data = IFeeManager::calcProtocolBpsCall { - amountIn: params.amount_in, - tokenIn: token_in_address, - tokenOut: params.token_out, - destChain: params.dest_chain, - referrerBps: params.referrer_bps, - } - .abi_encode(); - - let calc_protocol_bps_call = EthereumRpc::Call(TransactionObject::new_call_with_from(&sender, &self.address, call_data), BlockParameter::Latest); - - let response = jsonrpc_call(&calc_protocol_bps_call, provider, chain) - .await - .map_err(|e| FeeManagerError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| FeeManagerError::CallFailed { msg: e.to_string() })?; - - let decoded = HexDecode(&result).map_err(|e| FeeManagerError::ABIError { - msg: format!("Failed to decode hex result: {}", e), - })?; - - let calculated_bps = IFeeManager::calcProtocolBpsCall::abi_decode_returns(&decoded, false).map_err(|e| FeeManagerError::ABIError { - msg: format!("Invalid calcProtocolBpsCall response: {}", e), - })?; - - Ok(U8::from(calculated_bps._0)) - } -} diff --git a/gemstone/src/swapper/mayan/mayan_relayer.rs b/gemstone/src/swapper/mayan/mayan_relayer.rs index 4bde0b7b..e5ff76f4 100644 --- a/gemstone/src/swapper/mayan/mayan_relayer.rs +++ b/gemstone/src/swapper/mayan/mayan_relayer.rs @@ -1,9 +1,22 @@ use std::sync::Arc; -use primitives::Chain; +use gem_evm::{ + erc20::IERC20, + jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, +}; +use primitives::{Asset, AssetId, Chain}; use serde::{Deserialize, Deserializer, Serialize}; -use crate::network::{AlienHttpMethod, AlienProvider, AlienTarget}; +use alloy_core::{ + hex::decode as HexDecode, + primitives::{Address, AddressError, U256}, + sol_types::SolCall, +}; + +use crate::{ + network::{jsonrpc_call, AlienHttpMethod, AlienProvider, AlienTarget}, + swapper::SwapperError, +}; const MAYAN_PROGRAM_ID: &str = "FC4eXxkyrMPTjiYUpp4EAnkmwMbQyZ6NDCh1kfLn6vsf"; pub const MAYAN_FORWARDER_CONTRACT: &str = "0x0654874eb7F59C6f5b39931FC45dC45337c967c3"; @@ -17,7 +30,7 @@ struct ApiError { #[derive(Debug, Clone, Serialize)] pub struct QuoteParams { - pub amount: u64, + pub amount: f64, pub from_token: String, pub from_chain: Chain, pub to_token: String, @@ -248,13 +261,13 @@ impl MayanRelayer { Self::new("https://price-api.mayan.finance/v3".to_string(), provider) } - fn convert_to_decimals(wei_amount: u64) -> f64 { - wei_amount as f64 / 1e18 - } - pub async fn get_quote(&self, params: QuoteParams, options: Option) -> Result, MayanRelayerError> { let options = options.unwrap_or_default(); - let amount_decimals = Self::convert_to_decimals(params.amount); + let from_chain = if params.from_chain == Chain::SmartChain { + "bsc".to_string() + } else { + params.from_chain.to_string() + }; let mut query_params = vec![ ("swift", options.swift.to_string()), @@ -263,9 +276,9 @@ impl MayanRelayer { ("onlyDirect", options.only_direct.to_string()), ("solanaProgram", MAYAN_PROGRAM_ID.to_string()), ("forwarderAddress", MAYAN_FORWARDER_CONTRACT.to_string()), - ("amountIn", amount_decimals.to_string()), + ("amountIn", params.amount.to_string()), ("fromToken", params.from_token), - ("fromChain", params.from_chain.to_string()), + ("fromChain", from_chain), ("toToken", params.to_token), ("toChain", params.to_chain.to_string()), // ("slippageBps", params.slippage_bps.map_or("auto".to_string(), |v| v.to_string())), diff --git a/gemstone/src/swapper/mayan/mayan_swift.rs b/gemstone/src/swapper/mayan/mayan_swift.rs index caab0166..d1041985 100644 --- a/gemstone/src/swapper/mayan/mayan_swift.rs +++ b/gemstone/src/swapper/mayan/mayan_swift.rs @@ -111,100 +111,9 @@ impl MayanSwift { Self { address, provider, chain } } - pub async fn get_fee_manager_address(&self) -> Result { - let call_data = IMayanSwift::feeManagerCall {}.abi_encode(); - let fee_manager_call = EthereumRpc::Call(TransactionObject::new_call(&self.address, call_data), BlockParameter::Latest); - - let response = jsonrpc_call(&fee_manager_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - let decoded = HexDecode(&result).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - let fee_manager = - IMayanSwift::feeManagerCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - let address = EthereumAddress::from_str(&fee_manager.feeManager.to_string()).map_err(|e| MayanSwiftError::InvalidResponse { - msg: format!("Failed to parse fee manager address: {}", e), - })?; - - Ok(address.to_checksum()) - } - - pub async fn create_order_with_eth(&self, from: &str, params: OrderParams, value: &str) -> Result { - let call_data = self - .encode_create_order_with_eth(params, U256::from_str(value).map_err(|_| MayanSwiftError::InvalidAmount)?) - .await?; - - let create_order_call = EthereumRpc::Call( - TransactionObject::new_call_with_value(from, &self.address, call_data, value), - BlockParameter::Latest, - ); - - let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - Ok(result) - } - - pub async fn create_order_with_token(&self, from: &str, token_in: &str, amount_in: &str, params: OrderParams) -> Result { - let call_data = self - .encode_create_order_with_token(token_in, U256::from_str(amount_in).map_err(|_| MayanSwiftError::InvalidAmount)?, params) - .await?; - - let create_order_call = EthereumRpc::Call(TransactionObject::new_call_with_from(from, &self.address, call_data), BlockParameter::Latest); - - let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - Ok(result) - } - - pub async fn estimate_create_order_with_eth(&self, from: &str, params: OrderParams, amount: U256) -> Result { - let call_data = self.encode_create_order_with_eth(params, amount).await?; - - // let value = encode_prefixed(amount.to_be_bytes_vec()); - let value = format!("0x{:x}", amount); - - let estimate_gas_call = EthereumRpc::EstimateGas(TransactionObject::new_call_with_value(from, &self.address, call_data, &value)); - - let response = jsonrpc_call(&estimate_gas_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let hex_str = result.trim_start_matches("0x"); - - Ok(U256::from_str_radix(hex_str, 16).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?) - } - - pub async fn estimate_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result { - let call_data = self.encode_create_order_with_token(token_in, amount, params).await?; - let estimate_gas_call = EthereumRpc::EstimateGas(TransactionObject::new_call_with_value(&self.address, token_in, call_data, &amount.to_string())); - - let response = jsonrpc_call(&estimate_gas_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let decoded = HexDecode(&result).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - Ok(U256::from_str(decoded.encode_hex().as_str()).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?) - } - pub async fn encode_create_order_with_eth(&self, params: OrderParams, amount: U256) -> Result, MayanSwiftError> { let call_data = IMayanSwift::createOrderWithEthCall { - params: self.convert_order_params(params), + params: params.to_contract_params(), } .abi_encode(); @@ -217,152 +126,12 @@ impl MayanSwift { msg: format!("Invalid token address: {}", e), })?, amountIn: amount, - params: self.convert_order_params(params), + params: params.to_contract_params(), } .abi_encode(); Ok(call_data) } - - pub async fn get_orders(&self, order_hashes: Vec<[u8; 32]>) -> Result, MayanSwiftError> { - let call_data = IMayanSwift::getOrdersCall { - orderHashes: order_hashes.into_iter().map(|x| x.into()).collect(), - } - .abi_encode(); - - let get_orders_call = EthereumRpc::Call(TransactionObject::new_call(&self.address, call_data), BlockParameter::Latest); - - let response = jsonrpc_call(&get_orders_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let decoded = HexDecode(&result).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - let orders = IMayanSwift::getOrdersCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftError::ABIError { msg: e.to_string() })?; - - Ok(orders - ._0 - .into_iter() - .map(|order| (order.status, order.amountIn.try_into().unwrap_or(0), order.destChainId)) - .collect()) - } - - pub async fn check_token_approval(&self, owner: &str, token: &str, amount: &str) -> Result { - // Encode allowance call for ERC20 token - let call_data = IERC20::allowanceCall { - owner: Address::from_str(owner).map_err(|e| MayanSwiftError::ABIError { - msg: format!("Invalid owner address: {}", e), - })?, - spender: Address::from_str(&self.address).map_err(|e| MayanSwiftError::ABIError { - msg: format!("Invalid spender address: {}", e), - })?, - } - .abi_encode(); - - // Create RPC call - let allowance_call = EthereumRpc::Call(TransactionObject::new_call(token, call_data), BlockParameter::Latest); - - // Execute the call - let response = jsonrpc_call(&allowance_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - // Decode the response - let decoded = hex::decode(result.trim_start_matches("0x")).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - let allowance = IERC20::allowanceCall::abi_decode_returns(&decoded, false).map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - // Convert amount string to U256 for comparison - let required_amount = U256::from_str(amount).map_err(|e| MayanSwiftError::ABIError { - msg: format!("Invalid amount: {}", e), - })?; - - // Compare allowance with required amount - Ok(if allowance._0 >= required_amount { - ApprovalType::Approve(ApprovalData { - token: token.into(), - spender: self.address.clone(), - value: amount.into(), - }) - } else { - ApprovalType::None - }) - } - - pub async fn encode_create_order_with_sig( - &self, - token_in: &str, - amount_in: U256, - params: OrderParams, - submission_fee: U256, - signed_order_hash: Vec, - permit_params: PermitParams, - ) -> Result, MayanSwiftError> { - let call_data = IMayanSwift::createOrderWithSigCall { - tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftError::ABIError { - msg: format!("Invalid token address: {}", e), - })?, - amountIn: amount_in, - params: self.convert_order_params(params), - submissionFee: submission_fee, - signedOrderHash: signed_order_hash.into(), - permitParams: self.convert_permit_params(permit_params), - } - .abi_encode(); - - Ok(call_data) - } - - pub async fn create_order_with_sig( - &self, - from: &str, - token_in: &str, - amount_in: &str, - params: OrderParams, - submission_fee: &str, - signed_order_hash: Vec, - permit_params: PermitParams, - ) -> Result { - let call_data = self - .encode_create_order_with_sig( - token_in, - U256::from_str(amount_in).map_err(|_| MayanSwiftError::InvalidAmount)?, - params, - U256::from_str(submission_fee).map_err(|_| MayanSwiftError::InvalidAmount)?, - signed_order_hash, - permit_params, - ) - .await?; - - let create_order_call = EthereumRpc::Call(TransactionObject::new_call_with_from(from, &self.address, call_data), BlockParameter::Latest); - - let response = jsonrpc_call(&create_order_call, self.provider.clone(), &self.chain) - .await - .map_err(|e| MayanSwiftError::CallFailed { msg: e.to_string() })?; - - let result: String = response.extract_result().map_err(|e| MayanSwiftError::InvalidResponse { msg: e.to_string() })?; - - Ok(result) - } - - pub fn convert_permit_params(&self, permit_params: PermitParams) -> IMayanSwift::PermitParams { - IMayanSwift::PermitParams { - value: U256::from_str(&permit_params.value).unwrap(), - deadline: U256::from(permit_params.deadline), - v: permit_params.v.into(), - r: permit_params.r.into(), - s: permit_params.s.into(), - } - } - - // Helper method to convert our native OrderParams to contract format - pub fn convert_order_params(&self, params: OrderParams) -> IMayanSwift::OrderParams { - params.to_contract_params() - } } #[cfg(test)] diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs index 556b8481..01c65381 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -1,12 +1,17 @@ use rand::Rng; use std::{str::FromStr, sync::Arc}; -use alloy_core::{hex::ToHexExt, primitives::Address}; -use alloy_primitives::{keccak256, U256}; +use alloy_core::{ + hex::{decode as HexDecode, ToHexExt}, + primitives::{Address, AddressError, U256}, + sol_types::SolCall, +}; + use async_trait::async_trait; use gem_evm::{ address::EthereumAddress, - jsonrpc::EthereumRpc, + erc20::IERC20, + jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains, get_swift_providers}, }; use primitives::{Asset, AssetId, Chain}; @@ -15,15 +20,15 @@ use crate::{ config::swap_config::SwapReferralFee, network::{jsonrpc_call, AlienProvider}, swapper::{ + approval::{check_approval, CheckApprovalType}, ApprovalType, FetchQuoteData, GemSwapProvider, SwapProvider, SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteRequest, SwapRoute, SwapperError, }, }; use super::{ - fee_manager::{CalcProtocolBpsParams, FeeManager}, forwarder::MayanForwarder, - mayan_relayer::{self, MayanRelayer, Quote, QuoteOptions, QuoteParams, QuoteType, MAYAN_FORWARDER_CONTRACT}, - mayan_swift::{KeyStruct, MayanSwift, MayanSwiftError, OrderParams, PermitParams}, + mayan_relayer::{MayanRelayer, Quote, QuoteOptions, QuoteParams, QuoteType, MAYAN_FORWARDER_CONTRACT}, + mayan_swift::{MayanSwift, MayanSwiftError, OrderParams}, }; const MAYAN_ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; @@ -49,26 +54,26 @@ impl MayanSwiftProvider { fn get_chain_by_wormhole_id(&self, wormhole_id: u64) -> Option { get_swift_providers() .into_iter() - .find(|(chain, deployment)| deployment.wormhole_id == wormhole_id) + .find(|(_, deployment)| deployment.wormhole_id == wormhole_id) .map(|(chain, _)| chain) } - async fn check_approval(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + async fn check_approval(&self, request: &SwapQuoteRequest, provider: Arc, quote: &Quote) -> Result { if request.from_asset.is_native() { return Ok(ApprovalType::None); } - let token_id = request.from_asset.token_id.as_ref().ok_or(SwapperError::NotSupportedAsset)?; - - let deployment = get_swift_deployment_by_chain(request.from_asset.chain).ok_or(SwapperError::NotSupportedChain)?; - - let swift_contract = MayanSwift::new(deployment.address, provider.clone(), request.from_asset.chain); - - let amount = &request.value; - swift_contract - .check_token_approval(&request.wallet_address, token_id, amount) - .await - .map_err(|e| SwapperError::ABIError { msg: e.to_string() }) + check_approval( + CheckApprovalType::ERC20( + request.wallet_address.clone(), + request.from_asset.token_id.clone().unwrap_or_default(), + MAYAN_FORWARDER_CONTRACT.to_string(), + U256::from_str(&request.value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, + ), + provider, + &request.from_asset.chain, + ) + .await } fn to_native_wormhole_address(&self, address: &str, w_chain_id: u64) -> Result<[u8; 32], SwapperError> { @@ -118,7 +123,8 @@ impl MayanSwiftProvider { e })?; - let gas_drop = self.convert_amount_to_wei(quote.gas_drop, &request.to_asset).map_err(|e| { + // TODO: check if we need to use to token or from token decimals + let gas_drop = self.convert_amount_to_wei(quote.gas_drop, quote.to_token.decimals.into()).map_err(|e| { eprintln!("Failed to convert gas_drop: {}", quote.gas_drop); e })?; @@ -137,12 +143,14 @@ impl MayanSwiftProvider { deadline, dest_addr: destination_address, dest_chain_id: dest_chain_id.to_string().parse().map_err(|_| SwapperError::InvalidAmount)?, - referrer_addr: self - .to_native_wormhole_address(referrer.address.as_str(), from_chain_id) - .map_err(|e| SwapperError::InvalidAddress { - address: referrer.address.clone(), - })?, - referrer_bps: referrer.bps.try_into().map_err(|_| SwapperError::InvalidAmount)?, + referrer_addr: referrer.clone().map_or([0u8; 32], |x| { + self.to_native_wormhole_address(x.address.as_str(), from_chain_id) + .map_err(|err| SwapperError::ComputeQuoteError { + msg: "Unable to get referrer wormhole address".to_string(), + }) + .unwrap() + }), + referrer_bps: referrer.map_or(0u8, |x| x.bps.try_into().map_err(|_| SwapperError::InvalidAmount).unwrap()), auction_mode: quote.swift_auction_mode.unwrap_or(0), random: random_bytes, }; @@ -164,38 +172,42 @@ impl MayanSwiftProvider { Ok(bytes32) } - fn get_referrer(&self, request: &SwapQuoteRequest) -> Result { + fn get_referrer(&self, request: &SwapQuoteRequest) -> Result, SwapperError> { if let Some(options) = &request.options { if let Some(referrer) = &options.fee { let evm_fee = &referrer.evm; let solana_fee = &referrer.solana; if request.from_asset.chain == Chain::Solana { - return Ok(solana_fee.clone()); + return Ok(Some(solana_fee.clone())); } - return Ok(evm_fee.clone()); + return Ok(Some(evm_fee.clone())); } } - Err(SwapperError::ComputeQuoteError { - msg: "Missing referrer".to_string(), - }) + Ok(None) } async fn fetch_quote_from_request(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + let asset_decimals = self.get_asset_decimals(request.from_asset.clone(), provider.clone()).await?; let mayan_relayer = MayanRelayer::default_relayer(provider.clone()); let referrer = self.get_referrer(request)?; let quote_params = QuoteParams { - amount: request.value.parse().map_err(|_| SwapperError::InvalidAmount)?, + amount: self.convert_to_decimals( + request.value.parse().map_err(|_| SwapperError::ComputeQuoteError { + msg: "Failed to convert request value to number".to_string(), + })?, + asset_decimals, + ), from_token: request.from_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), from_chain: request.from_asset.chain.clone(), to_token: request.to_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), to_chain: request.to_asset.chain.clone(), slippage_bps: Some(100), gas_drop: None, - referrer: Some(referrer.address), - referrer_bps: Some(referrer.bps), + referrer: referrer.clone().map(|x| x.address), + referrer_bps: referrer.map(|x| x.bps), }; let quote_options = QuoteOptions { @@ -220,13 +232,7 @@ impl MayanSwiftProvider { }) } - fn convert_amount_to_wei(&self, amount: f64, asset_id: &AssetId) -> Result { - // Retrieve asset information based on the provided AssetId - let asset = Asset::from_chain(asset_id.chain); - - // Get the decimals for the asset - let decimals = asset.decimals; - + fn convert_amount_to_wei(&self, amount: f64, decimals: u32) -> Result { // Calculate the scaling factor (10^decimals) let scaling_factor = 10f64.powi(decimals as i32); @@ -272,6 +278,39 @@ impl MayanSwiftProvider { // Return the scaled amount as a string Ok(format!("{:.0}", scaled_amount)) } + + async fn get_asset_decimals(&self, asset_id: AssetId, provider: Arc) -> Result { + let asset = Asset::from_chain(asset_id.chain); + + if asset_id.is_native() { + return Ok(asset.decimals as u32); + } + let address = asset_id.token_id.clone().unwrap(); + let decimals_data = IERC20::decimalsCall {}.abi_encode(); + let decimals_call = EthereumRpc::Call(TransactionObject::new_call(&address, decimals_data), BlockParameter::Latest); + + let response = jsonrpc_call(&decimals_call, provider.clone(), &asset_id.chain) + .await + .map_err(|err| SwapperError::ComputeQuoteError { + msg: format!("Failed to get ERC20 decimals: {}", err), + })?; + let result: String = response.take().map_err(|_| SwapperError::ComputeQuoteError { + msg: "Failed to get ERC20 decimals".to_string(), + })?; + let decoded = HexDecode(result).map_err(|_| SwapperError::ComputeQuoteError { + msg: "Failed to decode decimals return".to_string(), + })?; + let decimals_return = IERC20::decimalsCall::abi_decode_returns(&decoded, false).map_err(|_| SwapperError::ComputeQuoteError { + msg: "Failed to decode decimals return".to_string(), + })?; + + Ok(decimals_return._0.into()) + } + + fn convert_to_decimals(&self, wei_amount: u128, decimals: u32) -> f64 { + let divisor = 10_u64.pow(decimals); + wei_amount as f64 / divisor as f64 + } } #[async_trait] @@ -284,6 +323,44 @@ impl GemSwapProvider for MayanSwiftProvider { get_swift_deployment_chains() } + async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { + // Validate chain support + if !self.supported_chains().contains(&request.from_asset.chain) { + return Err(SwapperError::NotSupportedChain); + } + + let quote = self.fetch_quote_from_request(request, provider.clone()).await?; + + if quote.r#type != QuoteType::SWIFT.to_string() { + return Err(SwapperError::ComputeQuoteError { + msg: "Quote type is not SWIFT".to_string(), + }); + } + + // Create route information + let route = SwapRoute { + route_data: "swift-order".to_string(), + input: request.from_asset.clone(), + output: request.to_asset.clone(), + gas_estimate: None, + }; + + let approval = self.check_approval(request, provider.clone(), "e).await?; + + Ok(SwapQuote { + from_value: request.value.clone(), + to_value: self + .convert_amount_to_wei(quote.min_amount_out, quote.to_token.decimals.into()) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?, + data: SwapProviderData { + provider: self.provider(), + routes: vec![route], + }, + approval, + request: request.clone(), + }) + } + async fn fetch_quote_data(&self, quote: &SwapQuote, provider: Arc, data: FetchQuoteData) -> Result { let request = "e.request; let mayan_quote = self.fetch_quote_from_request(&request, provider.clone()).await?; @@ -314,7 +391,7 @@ impl GemSwapProvider for MayanSwiftProvider { .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? }; - let mut value = quote.from_value.clone(); + let value = quote.from_value.clone(); let forwarder_call_data = if mayan_quote.from_token.contract == mayan_quote.swift_input_contract { if mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS { forwarder @@ -387,49 +464,6 @@ impl GemSwapProvider for MayanSwiftProvider { // Ok(false) // } } - - async fn fetch_quote(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { - // Validate chain support - if !self.supported_chains().contains(&request.from_asset.chain) { - return Err(SwapperError::NotSupportedChain); - } - - let quote = self.fetch_quote_from_request(request, provider.clone()).await?; - - if quote.r#type != QuoteType::SWIFT.to_string() { - return Err(SwapperError::ComputeQuoteError { - msg: "Quote type is not SWIFT".to_string(), - }); - } - - // Create route information - let route = SwapRoute { - route_type: "swift-order".to_string(), - input: request - .from_asset - .token_id - .clone() - .unwrap_or_else(|| request.from_asset.chain.as_ref().to_string()), - output: request.to_asset.token_id.clone().unwrap_or_else(|| request.to_asset.chain.as_ref().to_string()), - fee_tier: "0".to_string(), - gas_estimate: None, - }; - - let approval = self.check_approval(request, provider.clone()).await?; - - Ok(SwapQuote { - from_value: request.value.clone(), - to_value: self - .convert_amount_to_wei(quote.min_amount_out, &request.to_asset) - .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?, - data: SwapProviderData { - provider: self.provider(), - routes: vec![route], - }, - approval, - request: request.clone(), - }) - } } #[cfg(test)] @@ -608,7 +642,7 @@ mod tests { let provider = MayanSwiftProvider::new(); let asset = Asset::from_chain(Chain::Ethereum); let amount = 1.23; // 1.23 ETH - let result = provider.convert_amount_to_wei(amount, &asset.id).unwrap(); + let result = provider.convert_amount_to_wei(amount, 18).unwrap(); assert_eq!(result, "1230000000000000000"); // 1.23 ETH in Wei } @@ -617,7 +651,7 @@ mod tests { let provider = MayanSwiftProvider::new(); let asset = Asset::from_chain(Chain::Ethereum); let amount = -1.0; // Negative amount - let result = provider.convert_amount_to_wei(amount, &asset.id); + let result = provider.convert_amount_to_wei(amount, 18); assert!(result.is_err()); } diff --git a/gemstone/src/swapper/mayan/mod.rs b/gemstone/src/swapper/mayan/mod.rs index efd51687..f677108f 100644 --- a/gemstone/src/swapper/mayan/mod.rs +++ b/gemstone/src/swapper/mayan/mod.rs @@ -1,4 +1,3 @@ -mod fee_manager; pub mod forwarder; mod mayan_relayer; pub mod mayan_swift; diff --git a/gemstone/src/swapper/mod.rs b/gemstone/src/swapper/mod.rs index 455e7fba..a0bce809 100644 --- a/gemstone/src/swapper/mod.rs +++ b/gemstone/src/swapper/mod.rs @@ -7,8 +7,8 @@ mod approval; mod custom_types; mod permit2_data; -pub mod mayan; pub mod jupiter; +pub mod mayan; pub mod models; pub mod orca; pub mod slippage; @@ -41,11 +41,11 @@ impl GemSwapper { Self { rpc_provider, swappers: vec![ - Box::new(universal_router::UniswapV3::new_uniswap()), - Box::new(universal_router::UniswapV3::new_pancakeswap()), - Box::new(thorchain::ThorChain::default()), + // Box::new(universal_router::UniswapV3::new_uniswap()), + // Box::new(universal_router::UniswapV3::new_pancakeswap()), + // Box::new(thorchain::ThorChain::default()), + // Box::new(jupiter::Jupiter::default()), Box::new(mayan::mayan_swift_provider::MayanSwiftProvider::new()), - Box::new(jupiter::Jupiter::default()), ], } } diff --git a/gemstone/tests/integration_test.rs b/gemstone/tests/integration_test.rs index 990ffd00..ea136467 100644 --- a/gemstone/tests/integration_test.rs +++ b/gemstone/tests/integration_test.rs @@ -13,12 +13,12 @@ mod tests { #[derive(Debug)] pub struct NativeProvider { - pub node_config: HashMap, + pub node_config: HashMap, pub client: Client, } impl NativeProvider { - pub fn new(node_config: HashMap) -> Self { + pub fn new(node_config: HashMap) -> Self { Self { node_config, client: Client::new(), @@ -28,7 +28,7 @@ mod tests { #[async_trait] impl AlienProvider for NativeProvider { - fn get_endpoint(&self, chain: String) -> Result { + fn get_endpoint(&self, chain: Chain) -> Result { Ok(self .node_config .get(&chain) @@ -95,7 +95,7 @@ mod tests { #[tokio::test] async fn test_orca_get_quote_by_input() -> Result<(), SwapperError> { - let node_config = HashMap::from([(Chain::Solana.to_string(), "https://solana-rpc.publicnode.com".into())]); + let node_config = HashMap::from([(Chain::Solana, "https://solana-rpc.publicnode.com".into())]); let swap_provider: Box = Box::new(Orca::default()); let network_provider = Arc::new(NativeProvider::new(node_config)); @@ -120,34 +120,35 @@ mod tests { async fn test_mayan_swift_quote() -> Result<(), SwapperError> { const TEST_WALLET_ADDRESS: &str = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; - let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".into())]); + let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".to_string())]); let network_provider = Arc::new(NativeProvider::new(node_config)); let mayan_swift_provider = MayanSwiftProvider::new(); // Create a swap quote request let request = SwapQuoteRequest { - from_asset: AssetId::from_chain(Chain::Base), - to_asset: AssetId::from_chain(Chain::Ethereum), + from_asset: AssetId::from(Chain::Base, Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".to_string())), + to_asset: AssetId::from_chain(Chain::Optimism), wallet_address: TEST_WALLET_ADDRESS.to_string(), destination_address: TEST_WALLET_ADDRESS.to_string(), - value: "100000000000000000".to_string(), + value: "9000000".to_string(), mode: GemSwapMode::ExactIn, // Swap mode options: None, }; let quote = mayan_swift_provider.fetch_quote(&request, network_provider.clone()).await?; - assert_eq!(quote.from_value, "100000000000000000"); + assert_eq!(quote.from_value, "9000000"); // Expect the to_value to be assert!(quote.to_value.parse::().unwrap() > 0); // Verify assert_eq!(quote.data.routes.len(), 1); - assert_eq!(quote.data.routes[0].route_type, "swift-order"); - assert_eq!(quote.data.routes[0].input, "base"); - assert_eq!(quote.data.routes[0].output, "ethereum"); - assert_eq!(quote.data.routes[0].fee_tier, "0"); + assert_eq!( + quote.data.routes[0].input, + AssetId::from(Chain::Base, Some("0x4200000000000000000000000000000000000042".to_string())), + ); + assert_eq!(quote.data.routes[0].output, AssetId::from_chain(Chain::Optimism)); // assert!(quote.data.routes[0].gas_estimate.is_some(); // Verify From a1749d6464a510faafc358a6a3c2b2d952c26a31 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Tue, 3 Dec 2024 09:06:21 +0200 Subject: [PATCH 07/15] feat: erc20 swap using mayan swift --- gemstone/src/swapper/mayan/forwarder.rs | 62 +++++++++++++++++++ gemstone/src/swapper/mayan/mayan_swift.rs | 26 +++++++- .../src/swapper/mayan/mayan_swift_provider.rs | 37 +++++++++-- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/gemstone/src/swapper/mayan/forwarder.rs b/gemstone/src/swapper/mayan/forwarder.rs index 4e0b2b3e..4881dcfc 100644 --- a/gemstone/src/swapper/mayan/forwarder.rs +++ b/gemstone/src/swapper/mayan/forwarder.rs @@ -13,6 +13,8 @@ use thiserror::Error; use crate::network::{jsonrpc_call, AlienProvider}; +use super::mayan_swift::MayanSwiftPermit; + #[derive(Debug, Error)] pub enum MayanForwarderError { #[error("Unsupported protocol")] @@ -44,6 +46,30 @@ impl MayanForwarder { Ok(call_data) } + pub fn encode_forward_erc20_call( + &self, + token_in: &str, + amount_in: U256, + permit: Option, + mayan_protocol: &str, + protocol_data: Vec, + ) -> Result, MayanForwarderError> { + let call_data = IMayanForwarder::forwardERC20Call { + tokenIn: Address::from_str(token_in).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid token address: {}", e), + })?, + amountIn: amount_in, + permitParams: permit.map_or(MayanSwiftPermit::zero().to_contract_params(), |p| p.to_contract_params()), + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid protocol address: {}", e), + })?, + protocolData: protocol_data.into(), + } + .abi_encode(); + + Ok(call_data) + } + pub async fn encode_swap_and_forward_eth_call( &self, amount_in: U256, @@ -73,4 +99,40 @@ impl MayanForwarder { Ok(call_data) } + + pub fn encode_swap_and_forward_erc20_call( + &self, + token_in: &str, + amount_in: U256, + permit: Option, + swap_protocol: &str, + swap_data: Vec, + middle_token: &str, + min_middle_amount: U256, + mayan_protocol: &str, + mayan_data: Vec, + ) -> Result, MayanForwarderError> { + let call_data = IMayanForwarder::swapAndForwardERC20Call { + tokenIn: Address::from_str(token_in).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid token address: {}", e), + })?, + amountIn: amount_in, + permitParams: permit.map_or(MayanSwiftPermit::zero().to_contract_params(), |p| p.to_contract_params()), + swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid swap protocol address: {}", e), + })?, + swapData: swap_data.into(), + middleToken: Address::from_str(middle_token).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid middle token address: {}", e), + })?, + minMiddleAmount: min_middle_amount, + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + msg: format!("Invalid mayan protocol address: {}", e), + })?, + mayanData: mayan_data.into(), + } + .abi_encode(); + + Ok(call_data) + } } diff --git a/gemstone/src/swapper/mayan/mayan_swift.rs b/gemstone/src/swapper/mayan/mayan_swift.rs index d1041985..1b0e4aaf 100644 --- a/gemstone/src/swapper/mayan/mayan_swift.rs +++ b/gemstone/src/swapper/mayan/mayan_swift.rs @@ -12,7 +12,7 @@ use gem_evm::{ address::EthereumAddress, erc20::IERC20, jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::swift::swift::IMayanSwift, + mayan::{forwarder::IMayanForwarder::PermitParams, swift::swift::IMayanSwift}, }; use primitives::Chain; use std::{str::FromStr, sync::Arc}; @@ -78,7 +78,7 @@ impl OrderParams { } #[derive(Debug)] -pub struct PermitParams { +pub struct MayanSwiftPermit { pub value: String, pub deadline: u64, pub v: u8, @@ -106,6 +106,28 @@ impl KeyStruct { } } +impl MayanSwiftPermit { + pub fn zero() -> Self { + Self { + value: "0".to_string(), + deadline: 0, + v: 0, + r: [0u8; 32], + s: [0u8; 32], + } + } + + pub fn to_contract_params(&self) -> PermitParams { + PermitParams { + value: U256::from_str(&self.value).unwrap(), + deadline: U256::from(self.deadline), + v: self.v.into(), + r: self.r.into(), + s: self.s.into(), + } + } +} + impl MayanSwift { pub fn new(address: String, provider: Arc, chain: Chain) -> Self { Self { address, provider, chain } diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/mayan_swift_provider.rs index 01c65381..8f63c5c8 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/mayan_swift_provider.rs @@ -391,7 +391,11 @@ impl GemSwapProvider for MayanSwiftProvider { .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? }; - let value = quote.from_value.clone(); + let mut value = quote.from_value.clone(); + let effective_amount_in = self + .get_amount_of_fractional_amount(mayan_quote.effective_amount_in, mayan_quote.from_token.decimals) + .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; + let forwarder_call_data = if mayan_quote.from_token.contract == mayan_quote.swift_input_contract { if mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS { forwarder @@ -399,7 +403,16 @@ impl GemSwapProvider for MayanSwiftProvider { .await .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? } else { - todo!(); + value = "0".to_string(); + forwarder + .encode_forward_erc20_call( + mayan_quote.swift_input_contract.as_str(), + U256::from_str(effective_amount_in.as_str()).unwrap(), + None, + swift_address.as_str(), + swift_call_data.clone(), + ) + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? } } else { let evm_swap_router_address = mayan_quote.evm_swap_router_address.clone().ok_or_else(|| SwapperError::ComputeQuoteError { @@ -416,9 +429,6 @@ impl GemSwapProvider for MayanSwiftProvider { let formatted_min_middle_amount = self .get_amount_of_fractional_amount(min_middle_amount, mayan_quote.swift_input_decimals) .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; - let effective_amount_in = self - .get_amount_of_fractional_amount(mayan_quote.effective_amount_in, mayan_quote.from_token.decimals) - .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; if (mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS) { forwarder @@ -436,7 +446,22 @@ impl GemSwapProvider for MayanSwiftProvider { .await .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? } else { - todo!(); + value = "0".to_string(); + forwarder + .encode_swap_and_forward_erc20_call( + token_in.as_str(), + U256::from_str(quote.from_value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, + None, + &evm_swap_router_address.as_str(), + hex::decode(evm_swap_router_calldata.trim_start_matches("0x")).map_err(|_| SwapperError::ABIError { + msg: "Failed to decode evm_swap_router_calldata hex string without prefix 0x ".to_string(), + })?, + &mayan_quote.swift_input_contract.as_str(), + U256::from_str(&formatted_min_middle_amount).map_err(|_| SwapperError::InvalidAmount)?, + &swift_address.as_str(), + swift_call_data, + ) + .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? } }; From 32d137ba87b7aedd8a1c3cc5d9f595d99adfdcdf Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 5 Dec 2024 19:12:18 +0200 Subject: [PATCH 08/15] chore: code refactoring --- crates/gem_evm/src/mayan/fee_manager.rs | 69 --- crates/gem_evm/src/mayan/forwarder.rs | 35 -- crates/gem_evm/src/mayan/mod.rs | 1 - crates/gem_evm/src/mayan/swift/swift.rs | 77 --- gemstone/src/swapper/mayan/constants.rs | 3 + gemstone/src/swapper/mayan/forwarder.rs | 55 +- gemstone/src/swapper/mayan/mayan_relayer.rs | 521 ------------------ gemstone/src/swapper/mayan/mayan_swift.rs | 203 ------- gemstone/src/swapper/mayan/mod.rs | 10 +- gemstone/src/swapper/mayan/models.rs | 7 + gemstone/src/swapper/mayan/relayer.rs | 313 +++++++++++ gemstone/src/swapper/mayan/swift.rs | 122 ++++ ...an_swift_provider.rs => swift_provider.rs} | 261 ++------- .../swapper/mayan/tests/quote_response.json | 97 ++++ .../mayan/tests/quote_token_response.json | 18 + gemstone/src/swapper/mod.rs | 10 +- gemstone/tests/integration_test.rs | 8 +- 17 files changed, 640 insertions(+), 1170 deletions(-) delete mode 100644 crates/gem_evm/src/mayan/fee_manager.rs create mode 100644 gemstone/src/swapper/mayan/constants.rs delete mode 100644 gemstone/src/swapper/mayan/mayan_relayer.rs delete mode 100644 gemstone/src/swapper/mayan/mayan_swift.rs create mode 100644 gemstone/src/swapper/mayan/models.rs create mode 100644 gemstone/src/swapper/mayan/relayer.rs create mode 100644 gemstone/src/swapper/mayan/swift.rs rename gemstone/src/swapper/mayan/{mayan_swift_provider.rs => swift_provider.rs} (62%) create mode 100644 gemstone/src/swapper/mayan/tests/quote_response.json create mode 100644 gemstone/src/swapper/mayan/tests/quote_token_response.json diff --git a/crates/gem_evm/src/mayan/fee_manager.rs b/crates/gem_evm/src/mayan/fee_manager.rs deleted file mode 100644 index 95c17d60..00000000 --- a/crates/gem_evm/src/mayan/fee_manager.rs +++ /dev/null @@ -1,69 +0,0 @@ -use alloy_core::sol; -use alloy_primitives::{Address, U256}; - -sol! { - /// @notice Fee Manager interface for managing protocol fees and treasury operations - #[derive(Debug, PartialEq)] - interface IFeeManager { - /// @notice Calculates the protocol fee in basis points - /// @param amountIn The input amount for the swap - /// @param tokenIn The input token address - /// @param tokenOut The output token identifier - /// @param destChain The destination chain identifier - /// @param referrerBps The referrer's basis points - /// @return The protocol fee in basis points - function calcProtocolBps( - uint64 amountIn, - address tokenIn, - bytes32 tokenOut, - uint16 destChain, - uint8 referrerBps - ) external view returns (uint8); - - /// @notice Returns the current fee collector address - /// @return The address of the fee collector (treasury or contract) - function feeCollector() external view returns (address); - - /// @notice Changes the operator to a new address - /// @param nextOperator The address of the new operator - function changeOperator(address nextOperator) external; - - /// @notice Allows the next operator to claim the operator role - function claimOperator() external; - - /// @notice Sweeps ERC20 tokens from the contract - /// @param token The token address to sweep - /// @param amount The amount to sweep - /// @param to The recipient address - function sweepToken(address token, uint256 amount, address to) external; - - /// @notice Sweeps ETH from the contract - /// @param amount The amount of ETH to sweep - /// @param to The recipient address - function sweepEth(uint256 amount, address payable to) external; - - /// @notice Sets the base fee in basis points - /// @param baseBps The new base fee in basis points - function setBaseBps(uint8 baseBps) external; - - /// @notice Sets the treasury address - /// @param treasury The new treasury address - function setTreasury(address treasury) external; - } - - /// @notice Fee Manager contract state and events - #[derive(Debug, PartialEq)] - contract FeeManager { - /// @notice The current operator address - address public operator; - - /// @notice The next operator address - address public nextOperator; - - /// @notice The base fee in basis points - uint8 public baseBps; - - /// @notice The treasury address - address public treasury; - } -} diff --git a/crates/gem_evm/src/mayan/forwarder.rs b/crates/gem_evm/src/mayan/forwarder.rs index 3cf617aa..0f73f1bd 100644 --- a/crates/gem_evm/src/mayan/forwarder.rs +++ b/crates/gem_evm/src/mayan/forwarder.rs @@ -4,17 +4,6 @@ sol! { /// @title MayanForwarder Interface #[derive(Debug, PartialEq)] interface IMayanForwarder { - /// @notice Guardian address - function guardian() external view returns (address); - - /// @notice Next guardian address - function nextGuardian() external view returns (address); - - /// @notice Check if protocol is supported for swaps - function swapProtocols(address protocol) external view returns (bool); - - /// @notice Check if protocol is supported for Mayan operations - function mayanProtocols(address protocol) external view returns (bool); /// @notice Forward ETH to Mayan protocol function forwardEth(address mayanProtocol, bytes calldata protocolData) external payable; @@ -52,30 +41,6 @@ sol! { bytes calldata mayanData ) external payable; - /// @notice Rescue ERC20 tokens - function rescueToken(address token, uint256 amount, address to) external; - - /// @notice Rescue ETH - function rescueEth(uint256 amount, address payable to) external; - - /// @notice Change guardian - function changeGuardian(address newGuardian) external; - - /// @notice Claim guardian role - function claimGuardian() external; - - /// @notice Set swap protocol status - function setSwapProtocol(address swapProtocol, bool enabled) external; - - /// @notice Set Mayan protocol status - function setMayanProtocol(address mayanProtocol, bool enabled) external; - - /// Events - event ForwardedEth(address mayanProtocol, bytes protocolData); - event ForwardedERC20(address token, uint256 amount, address mayanProtocol, bytes protocolData); - event SwapAndForwardedEth(uint256 amountIn, address swapProtocol, address middleToken, uint256 middleAmount, address mayanProtocol, bytes mayanData); - event SwapAndForwardedERC20(address tokenIn, uint256 amountIn, address swapProtocol, address middleToken, uint256 middleAmount, address mayanProtocol, bytes mayanData); - /// Structs struct PermitParams { uint256 value; diff --git a/crates/gem_evm/src/mayan/mod.rs b/crates/gem_evm/src/mayan/mod.rs index 6968936a..29ee1422 100644 --- a/crates/gem_evm/src/mayan/mod.rs +++ b/crates/gem_evm/src/mayan/mod.rs @@ -1,3 +1,2 @@ -pub mod fee_manager; pub mod forwarder; pub mod swift; diff --git a/crates/gem_evm/src/mayan/swift/swift.rs b/crates/gem_evm/src/mayan/swift/swift.rs index 6d070182..06bc0b21 100644 --- a/crates/gem_evm/src/mayan/swift/swift.rs +++ b/crates/gem_evm/src/mayan/swift/swift.rs @@ -1,60 +1,9 @@ use alloy_core::sol; -// First define the enums used in the contract -sol! { - #[derive(Debug)] - enum Status { - CREATED, - FULFILLED, - UNLOCKED, - CANCELED, - REFUNDED - } - - enum Action { - NONE, - FULFILL, - UNLOCK, - REFUND, - BATCH_UNLOCK - } - - enum AuctionMode { - NONE, - BYPASS, - ENGLISH - } -} - -// Now define the main contract interface sol! { /// @title MayanSwift Cross-Chain Swap Contract #[derive(Debug)] contract IMayanSwift{ - // Events - event OrderCreated(bytes32 indexed key); - event OrderFulfilled(bytes32 indexed key, uint64 sequence, uint256 netAmount); - event OrderUnlocked(bytes32 indexed key); - event OrderCanceled(bytes32 indexed key, uint64 sequence); - event OrderRefunded(bytes32 indexed key, uint256 netAmount); - - // Storage - address public immutable wormhole; - uint16 public immutable auctionChainId; - bytes32 public immutable auctionAddr; - bytes32 public immutable solanaEmitter; - address public feeManager; - uint8 public consistencyLevel; - address public guardian; - address public nextGuardian; - bool public paused; - - struct Order { - uint8 status; // Status enum - uint64 amountIn; - uint16 destChainId; - } - struct OrderParams { bytes32 trader; bytes32 tokenOut; @@ -79,33 +28,7 @@ sol! { bytes32 s; } - struct KeyStruct { - OrderParams params; - bytes32 tokenIn; - uint16 chainId; - uint16 protocolBps; - } - - // State changing functions - function setPause(bool _pause) external; - function setFeeManager(address _feeManager) external; - function setConsistencyLevel(uint8 _consistencyLevel) external; - function changeGuardian(address newGuardian) external; - function claimGuardian() external; - - // External functions function createOrderWithEth(OrderParams memory params) external payable returns (bytes32 orderHash); function createOrderWithToken(address tokenIn, uint256 amountIn, OrderParams memory params) external returns (bytes32 orderHash); - function createOrderWithSig( - address tokenIn, - uint256 amountIn, - OrderParams memory params, - uint256 submissionFee, - bytes calldata signedOrderHash, - PermitParams calldata permitParams - ) external returns (bytes32 orderHash); - - // View functions - function getOrders(bytes32[] memory orderHashes) external view returns (Order[] memory); } } diff --git a/gemstone/src/swapper/mayan/constants.rs b/gemstone/src/swapper/mayan/constants.rs new file mode 100644 index 00000000..67a11361 --- /dev/null +++ b/gemstone/src/swapper/mayan/constants.rs @@ -0,0 +1,3 @@ +pub const MAYAN_PROGRAM_ID: &str = "FC4eXxkyrMPTjiYUpp4EAnkmwMbQyZ6NDCh1kfLn6vsf"; +pub const MAYAN_FORWARDER_CONTRACT: &str = "0x0654874eb7F59C6f5b39931FC45dC45337c967c3"; +pub const MAYAN_ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; diff --git a/gemstone/src/swapper/mayan/forwarder.rs b/gemstone/src/swapper/mayan/forwarder.rs index 4881dcfc..1d05c2e3 100644 --- a/gemstone/src/swapper/mayan/forwarder.rs +++ b/gemstone/src/swapper/mayan/forwarder.rs @@ -1,31 +1,12 @@ -use std::{str::FromStr, sync::Arc}; +use std::str::FromStr; use alloy_core::{ primitives::{Address, U256}, - sol_types::{SolCall, SolValue}, + sol_types::SolCall, }; -use gem_evm::{ - jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::forwarder::IMayanForwarder, -}; -use primitives::Chain; -use thiserror::Error; - -use crate::network::{jsonrpc_call, AlienProvider}; +use gem_evm::mayan::forwarder::IMayanForwarder; -use super::mayan_swift::MayanSwiftPermit; - -#[derive(Debug, Error)] -pub enum MayanForwarderError { - #[error("Unsupported protocol")] - UnsupportedProtocol, - #[error("Call failed: {msg}")] - CallFailed { msg: String }, - #[error("Invalid response")] - InvalidResponse, - #[error("ABI error: {msg}")] - ABIError { msg: String }, -} +use super::{models::MayanError, swift::MayanSwiftPermit}; pub struct MayanForwarder {} @@ -34,9 +15,9 @@ impl MayanForwarder { Self {} } - pub async fn encode_forward_eth_call(&self, mayan_protocol: &str, protocol_data: Vec) -> Result, MayanForwarderError> { + pub async fn encode_forward_eth_call(&self, mayan_protocol: &str, protocol_data: Vec) -> Result, MayanError> { let call_data = IMayanForwarder::forwardEthCall { - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { msg: format!("Invalid protocol address: {}", e), })?, protocolData: protocol_data.into(), @@ -53,14 +34,14 @@ impl MayanForwarder { permit: Option, mayan_protocol: &str, protocol_data: Vec, - ) -> Result, MayanForwarderError> { + ) -> Result, MayanError> { let call_data = IMayanForwarder::forwardERC20Call { - tokenIn: Address::from_str(token_in).map_err(|e| MayanForwarderError::ABIError { + tokenIn: Address::from_str(token_in).map_err(|e| MayanError::ABIError { msg: format!("Invalid token address: {}", e), })?, amountIn: amount_in, permitParams: permit.map_or(MayanSwiftPermit::zero().to_contract_params(), |p| p.to_contract_params()), - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { msg: format!("Invalid protocol address: {}", e), })?, protocolData: protocol_data.into(), @@ -79,18 +60,18 @@ impl MayanForwarder { min_middle_amount: U256, mayan_protocol: &str, mayan_data: Vec, - ) -> Result, MayanForwarderError> { + ) -> Result, MayanError> { let call_data = IMayanForwarder::swapAndForwardEthCall { amountIn: amount_in, - swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanForwarderError::ABIError { + swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanError::ABIError { msg: format!("Invalid swap protocol address: {}", e), })?, swapData: swap_data.into(), - middleToken: Address::from_str(middle_token).map_err(|e| MayanForwarderError::ABIError { + middleToken: Address::from_str(middle_token).map_err(|e| MayanError::ABIError { msg: format!("Invalid middle token address: {}", e), })?, minMiddleAmount: min_middle_amount, - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { msg: format!("Invalid mayan protocol address: {}", e), })?, mayanData: mayan_data.into(), @@ -111,22 +92,22 @@ impl MayanForwarder { min_middle_amount: U256, mayan_protocol: &str, mayan_data: Vec, - ) -> Result, MayanForwarderError> { + ) -> Result, MayanError> { let call_data = IMayanForwarder::swapAndForwardERC20Call { - tokenIn: Address::from_str(token_in).map_err(|e| MayanForwarderError::ABIError { + tokenIn: Address::from_str(token_in).map_err(|e| MayanError::ABIError { msg: format!("Invalid token address: {}", e), })?, amountIn: amount_in, permitParams: permit.map_or(MayanSwiftPermit::zero().to_contract_params(), |p| p.to_contract_params()), - swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanForwarderError::ABIError { + swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanError::ABIError { msg: format!("Invalid swap protocol address: {}", e), })?, swapData: swap_data.into(), - middleToken: Address::from_str(middle_token).map_err(|e| MayanForwarderError::ABIError { + middleToken: Address::from_str(middle_token).map_err(|e| MayanError::ABIError { msg: format!("Invalid middle token address: {}", e), })?, minMiddleAmount: min_middle_amount, - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { msg: format!("Invalid mayan protocol address: {}", e), })?, mayanData: mayan_data.into(), diff --git a/gemstone/src/swapper/mayan/mayan_relayer.rs b/gemstone/src/swapper/mayan/mayan_relayer.rs deleted file mode 100644 index e5ff76f4..00000000 --- a/gemstone/src/swapper/mayan/mayan_relayer.rs +++ /dev/null @@ -1,521 +0,0 @@ -use std::sync::Arc; - -use gem_evm::{ - erc20::IERC20, - jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, -}; -use primitives::{Asset, AssetId, Chain}; -use serde::{Deserialize, Deserializer, Serialize}; - -use alloy_core::{ - hex::decode as HexDecode, - primitives::{Address, AddressError, U256}, - sol_types::SolCall, -}; - -use crate::{ - network::{jsonrpc_call, AlienHttpMethod, AlienProvider, AlienTarget}, - swapper::SwapperError, -}; - -const MAYAN_PROGRAM_ID: &str = "FC4eXxkyrMPTjiYUpp4EAnkmwMbQyZ6NDCh1kfLn6vsf"; -pub const MAYAN_FORWARDER_CONTRACT: &str = "0x0654874eb7F59C6f5b39931FC45dC45337c967c3"; -const SDK_VERSION: &str = "9_7_0"; - -#[derive(Debug, Deserialize)] -struct ApiError { - code: String, - msg: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct QuoteParams { - pub amount: f64, - pub from_token: String, - pub from_chain: Chain, - pub to_token: String, - pub to_chain: Chain, - #[serde(skip_serializing_if = "Option::is_none")] - pub slippage_bps: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub gas_drop: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub referrer: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub referrer_bps: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct QuoteOptions { - #[serde(default = "default_true")] - pub swift: bool, - #[serde(default = "default_true")] - pub mctp: bool, - #[serde(default = "default_false")] - pub gasless: bool, - #[serde(default = "default_false")] - pub only_direct: bool, -} - -fn deserialize_string_or_u64<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - use serde::de::{Error, Unexpected}; - let value: Option = Option::deserialize(deserializer)?; - if let Some(s) = value { - s.parse::() - .map(Some) - .map_err(|_| Error::invalid_value(Unexpected::Str(&s), &"a valid u64")) - // Convert string to u64 - } else { - Ok(None) - } -} - -impl Default for QuoteOptions { - fn default() -> Self { - Self { - swift: true, - mctp: true, - gasless: false, - only_direct: false, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct Token { - pub name: String, - pub standard: String, - pub symbol: String, - pub mint: String, - pub verified: bool, // Added - pub contract: String, - #[serde(rename = "wrappedAddress")] - pub wrapped_address: Option, // Added - #[serde(rename = "chainId")] - pub chain_id: Option, - #[serde(rename = "wChainId")] - pub w_chain_id: Option, - pub decimals: u8, - #[serde(rename = "logoURI")] - pub logo_uri: String, - #[serde(rename = "coingeckoId")] - pub coingecko_id: String, - #[serde(rename = "realOriginChainId")] - pub real_origin_chain_id: Option, - #[serde(rename = "realOriginContractAddress")] - pub real_origin_contract_address: Option, - #[serde(rename = "supportsPermit")] - pub supports_permit: bool, - #[serde(rename = "hasAuction")] - pub has_auction: bool, // Added -} - -#[derive(Debug, PartialEq)] -pub enum QuoteType { - SWIFT, - MCTP, - SWAP, - WH, -} - -impl ToString for QuoteType { - fn to_string(&self) -> String { - match self { - QuoteType::SWIFT => "SWIFT".to_string(), - QuoteType::MCTP => "MCTP".to_string(), - QuoteType::SWAP => "SWAP".to_string(), - QuoteType::WH => "WH".to_string(), - } - } -} - -#[derive(Debug, Deserialize)] -pub struct Quote { - #[serde(rename = "type")] - pub r#type: String, - #[serde(rename = "effectiveAmountIn")] - pub effective_amount_in: f64, - #[serde(rename = "expectedAmountOut")] - pub expected_amount_out: f64, - #[serde(rename = "priceImpact")] - pub price_impact: Option, - #[serde(rename = "minAmountOut")] - pub min_amount_out: f64, - - #[serde(rename = "minMiddleAmount")] - pub min_middle_amount: Option, - - #[serde(rename = "evmSwapRouterAddress")] - pub evm_swap_router_address: Option, - - #[serde(rename = "evmSwapRouterCalldata")] - pub evm_swap_router_calldata: Option, - #[serde(rename = "minReceived")] - pub min_received: f64, - #[serde(rename = "gasDrop")] - pub gas_drop: f64, - pub price: f64, - #[serde(rename = "swapRelayerFee")] - pub swap_relayer_feed: Option, - #[serde(rename = "redeemRelayerFee")] - pub redeem_relayer_fee: Option, - #[serde(rename = "refundRelayerFee")] - pub refund_relayer_fee: Option, - #[serde(rename = "solanaRelayerFee")] - pub solana_relayer_fee: Option, - #[serde(rename = "refundRelayerFee64")] - pub refund_relayer_fee64: String, - #[serde(rename = "cancelRelayerFee64")] - pub cancel_relayer_fee64: String, - #[serde(rename = "submitRelayerFee64")] - pub submit_relayer_fee64: String, - #[serde(rename = "solanaRelayerFee64")] - pub solana_relayer_fee64: Option, - #[serde(rename = "clientRelayerFeeSuccess")] - pub client_relayer_fee_success: Option, - #[serde(rename = "clientRelayerFeeRefund")] - pub client_relayer_fee_refund: Option, - pub eta: u64, - #[serde(rename = "etaSeconds")] - pub eta_seconds: u64, - #[serde(rename = "clientEta")] - pub client_eta: String, - #[serde(rename = "fromToken")] - pub from_token: Token, - #[serde(rename = "toToken")] - pub to_token: Token, - #[serde(rename = "fromChain")] - pub from_chain: String, - #[serde(rename = "toChain")] - pub to_chain: String, - #[serde(rename = "slippageBps")] - pub slippage_bps: u32, - #[serde(rename = "bridgeFee")] - pub bridge_fee: f64, - #[serde(rename = "suggestedPriorityFee")] - pub suggested_priority_fee: f64, - #[serde(rename = "onlyBridging")] - pub only_bridging: bool, - #[serde(rename = "deadline64")] - pub deadline64: String, - #[serde(rename = "referrerBps")] - pub referrer_bps: Option, - #[serde(rename = "protocolBps")] - pub protocol_bps: Option, - #[serde(rename = "swiftMayanContract")] - pub swift_mayan_contract: Option, - #[serde(rename = "swiftAuctionMode")] - pub swift_auction_mode: Option, - #[serde(rename = "swiftInputContract")] - pub swift_input_contract: String, - #[serde(rename = "swiftInputDecimals")] - pub swift_input_decimals: u8, - pub gasless: bool, - pub relayer: String, - #[serde(rename = "sendTransactionCost")] - pub send_transaction_cost: f64, - #[serde(rename = "maxUserGasDrop")] - pub max_user_gas_drop: f64, - - #[serde(rename = "rentCost", deserialize_with = "deserialize_string_or_u64")] - pub rent_cost: Option, -} - -#[derive(Debug, Deserialize)] -struct QuoteResponse { - quotes: Vec, - - #[serde(rename = "minimumSdkVersion")] - pub minimum_sdk_version: Vec, -} - -#[derive(Debug, thiserror::Error)] -pub enum MayanRelayerError { - #[error("Network error: {0}")] - NetworkError(String), - #[error("Invalid response: {0}")] - InvalidResponse(String), - #[error("Route not found")] - RouteNotFound, - #[error("SDK version not supported")] - SdkVersionNotSupported, - #[error("Invalid parameters: {0}")] - InvalidParameters(String), -} - -#[derive(Debug)] -pub struct MayanRelayer { - url: String, - provider: Arc, -} - -impl MayanRelayer { - pub fn new(url: String, provider: Arc) -> Self { - Self { url, provider } - } - - pub fn default_relayer(provider: Arc) -> Self { - Self::new("https://price-api.mayan.finance/v3".to_string(), provider) - } - - pub async fn get_quote(&self, params: QuoteParams, options: Option) -> Result, MayanRelayerError> { - let options = options.unwrap_or_default(); - let from_chain = if params.from_chain == Chain::SmartChain { - "bsc".to_string() - } else { - params.from_chain.to_string() - }; - - let mut query_params = vec![ - ("swift", options.swift.to_string()), - ("mctp", options.mctp.to_string()), - ("gasless", options.gasless.to_string()), - ("onlyDirect", options.only_direct.to_string()), - ("solanaProgram", MAYAN_PROGRAM_ID.to_string()), - ("forwarderAddress", MAYAN_FORWARDER_CONTRACT.to_string()), - ("amountIn", params.amount.to_string()), - ("fromToken", params.from_token), - ("fromChain", from_chain), - ("toToken", params.to_token), - ("toChain", params.to_chain.to_string()), - // ("slippageBps", params.slippage_bps.map_or("auto".to_string(), |v| v.to_string())), - // ("gasDrop", params.gas_drop.unwrap_or(0).to_string()), - ("sdkVersion", "9_7_0".to_string()), - ]; - - if let Some(slippage) = params.slippage_bps { - query_params.push(("slippageBps", slippage.to_string())); - } - if let Some(gas_drop) = params.gas_drop { - query_params.push(("gasDrop", gas_drop.to_string())); - } - if let Some(referrer) = params.referrer { - query_params.push(("referrer", referrer)); - } - if let Some(referrer_bps) = params.referrer_bps { - query_params.push(("referrerBps", referrer_bps.to_string())); - } - - let query = serde_urlencoded::to_string(&query_params).map_err(|e| MayanRelayerError::InvalidParameters(e.to_string()))?; - - let url = format!("{}/quote?{}", self.url, query); - - let target = AlienTarget { - url, - method: AlienHttpMethod::Get, - headers: None, - body: None, - }; - - let data = self - .provider - .request(target) - .await - .map_err(|err| MayanRelayerError::NetworkError(err.to_string()))?; - - let quote_response = serde_json::from_slice::(&data); - match quote_response { - Ok(response) => { - if !self.check_sdk_version(response.minimum_sdk_version) { - return Err(MayanRelayerError::SdkVersionNotSupported); - } - - Ok(response.quotes) - } - Err(err) => { - if let Ok(api_error) = serde_json::from_slice::(&data) { - return Err(MayanRelayerError::InvalidResponse(api_error.msg)); - } - Err(MayanRelayerError::NetworkError(err.to_string())) - } - } - } - - fn check_sdk_version(&self, minimum_version: Vec) -> bool { - let sdk_version = SDK_VERSION.split('_').filter_map(|x| x.parse::().ok()).collect::>(); - - // Major version check - if sdk_version[0] < minimum_version[0] { - return false; - } - if sdk_version[0] > minimum_version[0] { - return true; - } - - // Minor version check - if sdk_version[1] < minimum_version[1] { - return false; - } - if sdk_version[1] > minimum_version[1] { - return true; - } - - if sdk_version[2] >= minimum_version[2] { - return true; - } - - false - } -} - -#[cfg(test)] -mod tests { - use std::{sync::Arc, time::Duration}; - - use crate::network::mock::AlienProviderMock; - - use super::*; - - #[test] - fn test_quote_deserialization() { - let json_data = r#" - { - "maxUserGasDrop": 0.01, - "sendTransactionCost": 0, - "rentCost": "40000000", - "gasless": false, - "swiftAuctionMode": 2, - "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", - "minMiddleAmount": null, - "swiftInputContract": "0x0000000000000000000000000000000000000000", - "swiftInputDecimals": 18, - "slippageBps": 100, - "effectiveAmountIn": 0.1, - "expectedAmountOut": 0.09972757016582551, - "price": 0.9996900030000002, - "priceImpact": null, - "minAmountOut": 0.09873029446416727, - "minReceived": 0.09873029446416727, - "route": null, - "swapRelayerFee": null, - "swapRelayerFee64": null, - "redeemRelayerFee": null, - "redeemRelayerFee64": null, - "solanaRelayerFee": null, - "solanaRelayerFee64": null, - "refundRelayerFee": null, - "refundRelayerFee64": "1056", - "cancelRelayerFee64": "25", - "submitRelayerFee64": "0", - "deadline64": "1732777812", - "clientRelayerFeeSuccess": null, - "clientRelayerFeeRefund": 0.038947098479114796, - "fromToken": { - "name": "ETH", - "standard": "native", - "symbol": "ETH", - "mint": "", - "verified": true, - "contract": "0x0000000000000000000000000000000000000000", - "wrappedAddress": "0x4200000000000000000000000000000000000006", - "chainId": 8453, - "wChainId": 30, - "decimals": 18, - "logoURI": "https://statics.mayan.finance/eth.png", - "coingeckoId": "weth", - "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", - "realOriginContractAddress": "0x4200000000000000000000000000000000000006", - "realOriginChainId": 30, - "supportsPermit": false, - "hasAuction": true - }, - "fromChain": "base", - "toToken": { - "name": "ETH", - "standard": "native", - "symbol": "ETH", - "mint": "8M6d63oL7dvMZ1gNbgGe3h8afMSWJEKEhtPTFM2u8h3c", - "verified": true, - "contract": "0x0000000000000000000000000000000000000000", - "wrappedAddress": "0x4200000000000000000000000000000000000006", - "chainId": 10, - "wChainId": 24, - "decimals": 18, - "logoURI": "https://statics.mayan.finance/eth.png", - "coingeckoId": "weth", - "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", - "realOriginContractAddress": "0x4200000000000000000000000000000000000006", - "realOriginChainId": 24, - "supportsPermit": false, - "hasAuction": true - }, - "toTokenPrice": 3602.87682508, - "toChain": "optimism", - "mintDecimals": null, - "gasDrop": 0, - "eta": 1, - "etaSeconds": 12, - "clientEta": "12s", - "bridgeFee": 0, - "suggestedPriorityFee": 0, - "type": "SWIFT", - "priceStat": { - "ratio": 0.9996907516582549, - "status": "GOOD" - }, - "referrerBps": 0, - "protocolBps": 3, - "onlyBridging": false, - "sourceSwapExpense": 0, - "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", - "meta": { - "advertisedDescription": "Cheapest and Fastest", - "advertisedTitle": "Best", - "icon": "https://cdn.mayan.finance/fast_icon.png", - "switchText": "Switch to the best route", - "title": "Best" - } - } - "#; - - let quote: Quote = serde_json::from_str(json_data).expect("Failed to deserialize Quote"); - assert_eq!(quote.r#type, "SWIFT"); - assert!(quote.price_impact.is_none()); - assert_eq!(quote.swift_input_decimals, 18); - } - - #[test] - fn test_token_deserialization() { - let json_data = r#" - { - "name": "ETH", - "standard": "native", - "symbol": "ETH", - "mint": "", - "verified": true, - "contract": "0x0000000000000000000000000000000000000000", - "wrappedAddress": "0x4200000000000000000000000000000000000006", - "chainId": 8453, - "wChainId": 30, - "decimals": 18, - "logoURI": "https://statics.mayan.finance/eth.png", - "coingeckoId": "weth", - "realOriginChainId": 30, - "realOriginContractAddress": "0x4200000000000000000000000000000000000006", - "supportsPermit": false, - "hasAuction": true - }"#; - - let token: Token = serde_json::from_str(json_data).expect("Failed to deserialize Token"); - assert_eq!(token.name, "ETH"); - assert!(token.verified); - assert_eq!(token.chain_id, Some(8453)); - assert_eq!(token.has_auction, true); - assert_eq!(token.wrapped_address.unwrap(), "0x4200000000000000000000000000000000000006"); - } - - #[test] - fn test_quote_response_deserialization() { - let json_data = r#" - { - "quotes": [], - "minimumSdkVersion": [7, 0, 0] - }"#; - - let response: QuoteResponse = serde_json::from_str(json_data).expect("Failed to deserialize QuoteResponse"); - assert_eq!(response.minimum_sdk_version, vec![7, 0, 0]); - } -} diff --git a/gemstone/src/swapper/mayan/mayan_swift.rs b/gemstone/src/swapper/mayan/mayan_swift.rs deleted file mode 100644 index 1b0e4aaf..00000000 --- a/gemstone/src/swapper/mayan/mayan_swift.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::{ - network::{jsonrpc_call, AlienProvider}, - swapper::{ApprovalData, ApprovalType}, -}; -use alloy_core::{ - hex::{decode as HexDecode, encode_prefixed, ToHexExt}, - primitives::{Address, FixedBytes, U256, U8}, - sol_types::{SolCall, SolValue}, -}; - -use gem_evm::{ - address::EthereumAddress, - erc20::IERC20, - jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::{forwarder::IMayanForwarder::PermitParams, swift::swift::IMayanSwift}, -}; -use primitives::Chain; -use std::{str::FromStr, sync::Arc}; -use thiserror::Error; - -pub struct MayanSwift { - address: String, - provider: Arc, - chain: Chain, -} - -#[derive(Error, Debug)] -pub enum MayanSwiftError { - #[error("Call failed: {msg}")] - CallFailed { msg: String }, - - #[error("Invalid response: {msg}")] - InvalidResponse { msg: String }, - - #[error("ABI error: {msg}")] - ABIError { msg: String }, - - #[error("Invalid amount")] - InvalidAmount, -} - -// Parameter structs with native types -#[derive(Debug, Clone)] -pub struct OrderParams { - pub trader: [u8; 32], - pub token_out: [u8; 32], - pub min_amount_out: u64, - pub gas_drop: u64, - pub cancel_fee: u64, - pub refund_fee: u64, - pub deadline: u64, - pub dest_addr: [u8; 32], - pub dest_chain_id: u16, - pub referrer_addr: [u8; 32], - pub referrer_bps: u8, - pub auction_mode: u8, - pub random: [u8; 32], -} - -impl OrderParams { - pub fn to_contract_params(&self) -> IMayanSwift::OrderParams { - IMayanSwift::OrderParams { - trader: self.trader.into(), - tokenOut: self.token_out.into(), - minAmountOut: self.min_amount_out, - gasDrop: self.gas_drop, - cancelFee: self.cancel_fee, - refundFee: self.refund_fee, - deadline: self.deadline, - destAddr: self.dest_addr.into(), - destChainId: self.dest_chain_id, - referrerAddr: self.referrer_addr.into(), - referrerBps: self.referrer_bps, - auctionMode: self.auction_mode, - random: self.random.into(), - } - } -} - -#[derive(Debug)] -pub struct MayanSwiftPermit { - pub value: String, - pub deadline: u64, - pub v: u8, - pub r: [u8; 32], - pub s: [u8; 32], -} - -#[derive(Debug, Clone)] -pub struct KeyStruct { - pub params: OrderParams, - pub token_in: [u8; 32], - pub chain_id: u16, - pub protocol_bps: u16, -} - -impl KeyStruct { - pub fn abi_encode(&self) -> Vec { - let key = IMayanSwift::KeyStruct { - params: self.params.to_contract_params(), - tokenIn: self.token_in.into(), - chainId: self.chain_id, - protocolBps: self.protocol_bps, - }; - key.abi_encode() - } -} - -impl MayanSwiftPermit { - pub fn zero() -> Self { - Self { - value: "0".to_string(), - deadline: 0, - v: 0, - r: [0u8; 32], - s: [0u8; 32], - } - } - - pub fn to_contract_params(&self) -> PermitParams { - PermitParams { - value: U256::from_str(&self.value).unwrap(), - deadline: U256::from(self.deadline), - v: self.v.into(), - r: self.r.into(), - s: self.s.into(), - } - } -} - -impl MayanSwift { - pub fn new(address: String, provider: Arc, chain: Chain) -> Self { - Self { address, provider, chain } - } - - pub async fn encode_create_order_with_eth(&self, params: OrderParams, amount: U256) -> Result, MayanSwiftError> { - let call_data = IMayanSwift::createOrderWithEthCall { - params: params.to_contract_params(), - } - .abi_encode(); - - Ok(call_data) - } - - pub async fn encode_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result, MayanSwiftError> { - let call_data = IMayanSwift::createOrderWithTokenCall { - tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftError::ABIError { - msg: format!("Invalid token address: {}", e), - })?, - amountIn: amount, - params: params.to_contract_params(), - } - .abi_encode(); - - Ok(call_data) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{future::pending, time::Duration}; - - use async_std::future::timeout; - use async_trait::async_trait; - - use crate::network::{mock::AlienProviderWarp, AlienError, AlienTarget, Data}; - - #[derive(Debug)] - pub struct AlienProviderMock { - pub response: String, - pub timeout: Duration, - } - - #[async_trait] - impl AlienProvider for AlienProviderMock { - async fn request(&self, _target: AlienTarget) -> Result { - let responses = self.batch_request(vec![_target]).await; - responses.map(|responses| responses.first().unwrap().clone()) - } - - async fn batch_request(&self, _targets: Vec) -> Result, AlienError> { - let never = pending::<()>(); - let _ = timeout(self.timeout, never).await; - Ok(vec![self.response.as_bytes().to_vec()]) - } - - fn get_endpoint(&self, _chain: Chain) -> Result { - Ok(String::from("http://localhost:8080")) - } - } - - // #[test] - // fn test_encode_amount_hex() { - // let amount = U256::from(100); - // let mock_provider = AlienProviderMock { - // response: String::from("0x0000000000000000000000000000000000000000000000000000000000000064"), - // timeout: Duration::from_millis(100), - // }; - // let encoded = MayanSwiftContract::new("0x1234567890abcdef".into(), Arc::new(mock_provider), Chain::Ethereum).encode_amount_hex(amount); - // assert_eq!(encoded, "0000000000000000000000000000000000000000000000000000000000000064"); - // } -} diff --git a/gemstone/src/swapper/mayan/mod.rs b/gemstone/src/swapper/mayan/mod.rs index f677108f..4659c604 100644 --- a/gemstone/src/swapper/mayan/mod.rs +++ b/gemstone/src/swapper/mayan/mod.rs @@ -1,4 +1,6 @@ -pub mod forwarder; -mod mayan_relayer; -pub mod mayan_swift; -pub mod mayan_swift_provider; +mod constants; +mod forwarder; +mod models; +mod relayer; +mod swift; +pub mod swift_provider; diff --git a/gemstone/src/swapper/mayan/models.rs b/gemstone/src/swapper/mayan/models.rs new file mode 100644 index 00000000..0043aad1 --- /dev/null +++ b/gemstone/src/swapper/mayan/models.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MayanError { + #[error("ABI Error: {msg}")] + ABIError { msg: String }, +} diff --git a/gemstone/src/swapper/mayan/relayer.rs b/gemstone/src/swapper/mayan/relayer.rs new file mode 100644 index 00000000..77706a9a --- /dev/null +++ b/gemstone/src/swapper/mayan/relayer.rs @@ -0,0 +1,313 @@ +use std::{fmt::Display, sync::Arc}; + +use primitives::Chain; +use serde::{Deserialize, Serialize}; + +use crate::network::{AlienHttpMethod, AlienProvider, AlienTarget}; + +use super::constants::{MAYAN_FORWARDER_CONTRACT, MAYAN_PROGRAM_ID}; + +const SDK_VERSION: &str = "9_7_0"; + +#[derive(Debug, Deserialize)] +struct ApiError { + code: String, + msg: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct QuoteParams { + pub amount: f64, + pub from_token: String, + pub from_chain: Chain, + pub to_token: String, + pub to_chain: Chain, + #[serde(skip_serializing_if = "Option::is_none")] + pub slippage_bps: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_drop: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer_bps: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct QuoteOptions { + #[serde(default = "default_true")] + pub swift: bool, + #[serde(default = "default_true")] + pub mctp: bool, + #[serde(default = "default_false")] + pub gasless: bool, + #[serde(default = "default_false")] + pub only_direct: bool, +} + +impl Default for QuoteOptions { + fn default() -> Self { + Self { + swift: true, + mctp: true, + gasless: false, + only_direct: false, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] +pub struct Token { + pub name: String, + pub standard: String, + pub symbol: String, + pub mint: String, + pub verified: bool, + pub contract: String, + pub wrapped_address: Option, + pub chain_id: Option, + pub w_chain_id: Option, + pub decimals: u8, + + #[serde(rename = "logoURI")] + pub logo_uri: String, + pub coingecko_id: String, + pub real_origin_chain_id: Option, + pub real_origin_contract_address: Option, + pub supports_permit: bool, + pub has_auction: bool, +} + +#[derive(Debug, PartialEq)] +#[allow(dead_code)] +pub enum QuoteType { + Swift, + Mctp, // TODO: do we want to support all types? + Swap, + WH, +} + +impl Display for QuoteType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + QuoteType::Swift => write!(f, "SWIFT"), + QuoteType::Mctp => write!(f, "MCTP"), + QuoteType::Swap => write!(f, "SWAP"), + QuoteType::WH => write!(f, "WH"), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] +pub struct Quote { + #[serde(rename = "type")] + pub r#type: String, + pub effective_amount_in: f64, + pub expected_amount_out: f64, + pub price_impact: Option, + pub min_amount_out: f64, + pub min_middle_amount: Option, + pub evm_swap_router_address: Option, + pub evm_swap_router_calldata: Option, + pub min_received: f64, + pub gas_drop: f64, + pub price: f64, + pub swap_relayer_feed: Option, + pub redeem_relayer_fee: Option, + pub refund_relayer_fee: Option, + pub solana_relayer_fee: Option, + pub refund_relayer_fee64: String, + pub cancel_relayer_fee64: String, + pub from_token: Token, + pub to_token: Token, + pub from_chain: String, + pub to_chain: String, + pub slippage_bps: u32, + pub bridge_fee: f64, + pub suggested_priority_fee: f64, + pub only_bridging: bool, + pub deadline64: String, + pub referrer_bps: Option, + pub protocol_bps: Option, + pub swift_mayan_contract: Option, + pub swift_auction_mode: Option, + pub swift_input_contract: String, + pub swift_input_decimals: u8, + pub gasless: bool, + pub relayer: String, + pub send_transaction_cost: f64, + pub max_user_gas_drop: f64, +} + +#[derive(Debug, Deserialize)] +struct QuoteResponse { + quotes: Vec, + + #[serde(rename = "minimumSdkVersion")] + pub minimum_sdk_version: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum MayanRelayerError { + #[error("Network error: {0}")] + NetworkError(String), + #[error("Invalid response: {0}")] + InvalidResponse(String), + #[error("SDK version not supported")] + SdkVersionNotSupported, + #[error("Invalid parameters: {0}")] + InvalidParameters(String), +} + +#[derive(Debug)] +pub struct MayanRelayer { + url: String, + provider: Arc, +} + +impl MayanRelayer { + pub fn new(url: String, provider: Arc) -> Self { + Self { url, provider } + } + + pub fn default_relayer(provider: Arc) -> Self { + Self::new("https://price-api.mayan.finance/v3".to_string(), provider) + } + + pub async fn get_quote(&self, params: QuoteParams, options: Option) -> Result, MayanRelayerError> { + let options = options.unwrap_or_default(); + let from_chain = if params.from_chain == Chain::SmartChain { + "bsc".to_string() + } else { + params.from_chain.to_string() + }; + + let mut query_params = vec![ + ("swift", options.swift.to_string()), + ("mctp", options.mctp.to_string()), + ("gasless", options.gasless.to_string()), + ("onlyDirect", options.only_direct.to_string()), + ("solanaProgram", MAYAN_PROGRAM_ID.to_string()), + ("forwarderAddress", MAYAN_FORWARDER_CONTRACT.to_string()), + ("amountIn", params.amount.to_string()), + ("fromToken", params.from_token), + ("fromChain", from_chain), + ("toToken", params.to_token), + ("toChain", params.to_chain.to_string()), + // ("slippageBps", params.slippage_bps.map_or("auto".to_string(), |v| v.to_string())), + // ("gasDrop", params.gas_drop.unwrap_or(0).to_string()), + ("sdkVersion", "9_7_0".to_string()), + ]; + + if let Some(slippage) = params.slippage_bps { + query_params.push(("slippageBps", slippage.to_string())); + } + if let Some(gas_drop) = params.gas_drop { + query_params.push(("gasDrop", gas_drop.to_string())); + } + if let Some(referrer) = params.referrer { + query_params.push(("referrer", referrer)); + } + if let Some(referrer_bps) = params.referrer_bps { + query_params.push(("referrerBps", referrer_bps.to_string())); + } + + let query = serde_urlencoded::to_string(&query_params).map_err(|e| MayanRelayerError::InvalidParameters(e.to_string()))?; + + let url = format!("{}/quote?{}", self.url, query); + + let target = AlienTarget { + url, + method: AlienHttpMethod::Get, + headers: None, + body: None, + }; + + let data = self + .provider + .request(target) + .await + .map_err(|err| MayanRelayerError::NetworkError(err.to_string()))?; + + let quote_response = serde_json::from_slice::(&data); + match quote_response { + Ok(response) => { + if !self.check_sdk_version(response.minimum_sdk_version) { + return Err(MayanRelayerError::SdkVersionNotSupported); + } + + Ok(response.quotes) + } + Err(err) => { + if let Ok(api_error) = serde_json::from_slice::(&data) { + return Err(MayanRelayerError::InvalidResponse(api_error.msg)); + } + Err(MayanRelayerError::NetworkError(err.to_string())) + } + } + } + + fn check_sdk_version(&self, minimum_version: Vec) -> bool { + let sdk_version = SDK_VERSION.split('_').filter_map(|x| x.parse::().ok()).collect::>(); + + // Major version check + if sdk_version[0] < minimum_version[0] { + return false; + } + if sdk_version[0] > minimum_version[0] { + return true; + } + + // Minor version check + if sdk_version[1] < minimum_version[1] { + return false; + } + if sdk_version[1] > minimum_version[1] { + return true; + } + + if sdk_version[2] >= minimum_version[2] { + return true; + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quote_deserialization() { + let data = include_str!("tests/quote_response.json"); + let quote: Quote = serde_json::from_str(data).expect("Failed to deserialize Quote"); + assert_eq!(quote.r#type, "SWIFT"); + assert_eq!(quote.swift_input_decimals, 18); + } + + #[test] + fn test_token_deserialization() { + let data = include_str!("tests/quote_token_response.json"); + + let token: Token = serde_json::from_str(data).expect("Failed to deserialize Token"); + assert_eq!(token.name, "ETH"); + assert!(token.verified); + assert_eq!(token.chain_id, Some(8453)); + assert_eq!(token.wrapped_address.unwrap(), "0x4200000000000000000000000000000000000006"); + } + + #[test] + fn test_quote_response_deserialization() { + let json_data = r#"{ + "quotes": [], + "minimumSdkVersion": [7, 0, 0] + }"#; + + let response: QuoteResponse = serde_json::from_str(json_data).expect("Failed to deserialize QuoteResponse"); + assert_eq!(response.minimum_sdk_version, vec![7, 0, 0]); + } +} diff --git a/gemstone/src/swapper/mayan/swift.rs b/gemstone/src/swapper/mayan/swift.rs new file mode 100644 index 00000000..c4982543 --- /dev/null +++ b/gemstone/src/swapper/mayan/swift.rs @@ -0,0 +1,122 @@ +use alloy_core::{ + primitives::{Address, U256}, + sol_types::SolCall, +}; + +use gem_evm::mayan::{forwarder::IMayanForwarder::PermitParams, swift::swift::IMayanSwift}; +use std::str::FromStr; +use thiserror::Error; + +pub struct MayanSwift {} + +#[derive(Error, Debug)] +pub enum MayanSwiftError { + #[error("Call failed: {msg}")] + CallFailed { msg: String }, + + #[error("Invalid response: {msg}")] + InvalidResponse { msg: String }, + + #[error("ABI error: {msg}")] + ABIError { msg: String }, + + #[error("Invalid amount")] + InvalidAmount, +} + +// Parameter structs with native types +#[derive(Debug, Clone)] +pub struct OrderParams { + pub trader: [u8; 32], + pub token_out: [u8; 32], + pub min_amount_out: u64, + pub gas_drop: u64, + pub cancel_fee: u64, + pub refund_fee: u64, + pub deadline: u64, + pub dest_addr: [u8; 32], + pub dest_chain_id: u16, + pub referrer_addr: [u8; 32], + pub referrer_bps: u8, + pub auction_mode: u8, + pub random: [u8; 32], +} + +impl OrderParams { + pub fn to_contract_params(&self) -> IMayanSwift::OrderParams { + IMayanSwift::OrderParams { + trader: self.trader.into(), + tokenOut: self.token_out.into(), + minAmountOut: self.min_amount_out, + gasDrop: self.gas_drop, + cancelFee: self.cancel_fee, + refundFee: self.refund_fee, + deadline: self.deadline, + destAddr: self.dest_addr.into(), + destChainId: self.dest_chain_id, + referrerAddr: self.referrer_addr.into(), + referrerBps: self.referrer_bps, + auctionMode: self.auction_mode, + random: self.random.into(), + } + } +} + +#[derive(Debug)] +pub struct MayanSwiftPermit { + pub value: String, + pub deadline: u64, + pub v: u8, + pub r: [u8; 32], + pub s: [u8; 32], +} + +impl MayanSwiftPermit { + pub fn zero() -> Self { + Self { + value: "0".to_string(), + deadline: 0, + v: 0, + r: [0u8; 32], + s: [0u8; 32], + } + } + + pub fn to_contract_params(&self) -> PermitParams { + PermitParams { + value: U256::from_str(&self.value).unwrap(), + deadline: U256::from(self.deadline), + v: self.v, + r: self.r.into(), + s: self.s.into(), + } + } +} + +impl MayanSwift { + pub fn new() -> Self { + Self {} + } + + pub async fn encode_create_order_with_eth(&self, params: OrderParams) -> Result, MayanSwiftError> { + let call_data = IMayanSwift::createOrderWithEthCall { + params: params.to_contract_params(), + } + .abi_encode(); + + Ok(call_data) + } + + pub async fn encode_create_order_with_token(&self, token_in: &str, amount: U256, params: OrderParams) -> Result, MayanSwiftError> { + let call_data = IMayanSwift::createOrderWithTokenCall { + tokenIn: Address::from_str(token_in).map_err(|e| MayanSwiftError::ABIError { + msg: format!("Invalid token address: {}", e), + })?, + amountIn: amount, + params: params.to_contract_params(), + } + .abi_encode(); + + Ok(call_data) + } +} diff --git a/gemstone/src/swapper/mayan/mayan_swift_provider.rs b/gemstone/src/swapper/mayan/swift_provider.rs similarity index 62% rename from gemstone/src/swapper/mayan/mayan_swift_provider.rs rename to gemstone/src/swapper/mayan/swift_provider.rs index 8f63c5c8..d9b1f617 100644 --- a/gemstone/src/swapper/mayan/mayan_swift_provider.rs +++ b/gemstone/src/swapper/mayan/swift_provider.rs @@ -3,7 +3,7 @@ use std::{str::FromStr, sync::Arc}; use alloy_core::{ hex::{decode as HexDecode, ToHexExt}, - primitives::{Address, AddressError, U256}, + primitives::U256, sol_types::SolCall, }; @@ -12,7 +12,7 @@ use gem_evm::{ address::EthereumAddress, erc20::IERC20, jsonrpc::{BlockParameter, EthereumRpc, TransactionObject}, - mayan::swift::deployment::{get_swift_deployment_by_chain, get_swift_deployment_chains, get_swift_providers}, + mayan::swift::deployment::{get_swift_deployment_chains, get_swift_providers}, }; use primitives::{Asset, AssetId, Chain}; @@ -26,13 +26,12 @@ use crate::{ }; use super::{ + constants::{MAYAN_FORWARDER_CONTRACT, MAYAN_ZERO_ADDRESS}, forwarder::MayanForwarder, - mayan_relayer::{MayanRelayer, Quote, QuoteOptions, QuoteParams, QuoteType, MAYAN_FORWARDER_CONTRACT}, - mayan_swift::{MayanSwift, MayanSwiftError, OrderParams}, + relayer::{MayanRelayer, Quote, QuoteOptions, QuoteParams, QuoteType}, + swift::{MayanSwift, MayanSwiftError, OrderParams}, }; -const MAYAN_ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; - #[derive(Debug)] pub struct MayanSwiftProvider {} @@ -47,10 +46,6 @@ impl MayanSwiftProvider { Self {} } - fn get_address_by_chain(chain: Chain) -> Option { - get_swift_deployment_by_chain(chain).map(|x| x.address) - } - fn get_chain_by_wormhole_id(&self, wormhole_id: u64) -> Option { get_swift_providers() .into_iter() @@ -58,7 +53,7 @@ impl MayanSwiftProvider { .map(|(chain, _)| chain) } - async fn check_approval(&self, request: &SwapQuoteRequest, provider: Arc, quote: &Quote) -> Result { + async fn check_approval(&self, request: &SwapQuoteRequest, provider: Arc) -> Result { if request.from_asset.is_native() { return Ok(ApprovalType::None); } @@ -68,7 +63,7 @@ impl MayanSwiftProvider { request.wallet_address.clone(), request.from_asset.token_id.clone().unwrap_or_default(), MAYAN_FORWARDER_CONTRACT.to_string(), - U256::from_str(&request.value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, + U256::from_str(request.value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, ), provider, &request.from_asset.chain, @@ -90,39 +85,15 @@ impl MayanSwiftProvider { let dest_chain_id = quote.to_token.w_chain_id.unwrap(); let from_chain_id = quote.from_token.w_chain_id.unwrap(); - let deadline = quote.deadline64.parse::().map_err(|_| { - eprintln!("Failed to parse deadline: {}", quote.deadline64); - SwapperError::InvalidRoute - })?; - - let trader_address = self.address_to_bytes32(&request.wallet_address).map_err(|e| { - eprintln!("Failed to parse wallet_address: {}", request.wallet_address); - e + let deadline = quote.deadline64.parse::().map_err(|_| SwapperError::ComputeQuoteError { + msg: "Failed to parse deadline".to_string(), })?; - let destination_address = self.address_to_bytes32(&request.destination_address).map_err(|e| { - eprintln!("Failed to parse destination_address: {}", request.destination_address); - e - })?; - - let token_out = if let to_token_contract = "e.to_token.contract { - self.address_to_bytes32(to_token_contract).map_err(|e| { - eprintln!("Failed to parse to_token.contract: {}", to_token_contract); - e - })? - } else { - return Err(SwapperError::InvalidAddress { - address: "Missing to_token contract address".to_string(), - }); - }; - - let min_amount_out = self - .get_amount_of_fractional_amount(quote.min_amount_out, quote.to_token.decimals) - .map_err(|e| { - eprintln!("Failed to convert min_amount_out: {}", quote.min_amount_out); - e - })?; + let trader_address = self.address_to_bytes32(&request.wallet_address)?; + let destination_address = self.address_to_bytes32(&request.destination_address)?; + let token_out = self.address_to_bytes32("e.to_token.contract)?; + let min_amount_out = self.get_amount_of_fractional_amount(quote.min_amount_out, quote.to_token.decimals)?; // TODO: check if we need to use to token or from token decimals let gas_drop = self.convert_amount_to_wei(quote.gas_drop, quote.to_token.decimals.into()).map_err(|e| { eprintln!("Failed to convert gas_drop: {}", quote.gas_drop); @@ -145,7 +116,7 @@ impl MayanSwiftProvider { dest_chain_id: dest_chain_id.to_string().parse().map_err(|_| SwapperError::InvalidAmount)?, referrer_addr: referrer.clone().map_or([0u8; 32], |x| { self.to_native_wormhole_address(x.address.as_str(), from_chain_id) - .map_err(|err| SwapperError::ComputeQuoteError { + .map_err(|_| SwapperError::ComputeQuoteError { msg: "Unable to get referrer wormhole address".to_string(), }) .unwrap() @@ -201,9 +172,9 @@ impl MayanSwiftProvider { asset_decimals, ), from_token: request.from_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), - from_chain: request.from_asset.chain.clone(), + from_chain: request.from_asset.chain, to_token: request.to_asset.token_id.clone().unwrap_or(EthereumAddress::zero().to_checksum()), - to_chain: request.to_asset.chain.clone(), + to_chain: request.to_asset.chain, slippage_bps: Some(100), gas_drop: None, referrer: referrer.clone().map(|x| x.address), @@ -225,7 +196,7 @@ impl MayanSwiftProvider { })?; // TODO: adjust to find most effective quote - let most_effective_qoute = quote.into_iter().filter(|x| x.r#type == QuoteType::SWIFT.to_string()).last(); + let most_effective_qoute = quote.into_iter().filter(|x| x.r#type == QuoteType::Swift.to_string()).last(); most_effective_qoute.ok_or(SwapperError::ComputeQuoteError { msg: "Quote is not available".to_string(), @@ -331,7 +302,7 @@ impl GemSwapProvider for MayanSwiftProvider { let quote = self.fetch_quote_from_request(request, provider.clone()).await?; - if quote.r#type != QuoteType::SWIFT.to_string() { + if quote.r#type != QuoteType::Swift.to_string() { return Err(SwapperError::ComputeQuoteError { msg: "Quote type is not SWIFT".to_string(), }); @@ -345,7 +316,7 @@ impl GemSwapProvider for MayanSwiftProvider { gas_estimate: None, }; - let approval = self.check_approval(request, provider.clone(), "e).await?; + let approval = self.check_approval(request, provider.clone()).await?; Ok(SwapQuote { from_value: request.value.clone(), @@ -361,23 +332,19 @@ impl GemSwapProvider for MayanSwiftProvider { }) } - async fn fetch_quote_data(&self, quote: &SwapQuote, provider: Arc, data: FetchQuoteData) -> Result { + async fn fetch_quote_data(&self, quote: &SwapQuote, provider: Arc, _data: FetchQuoteData) -> Result { let request = "e.request; - let mayan_quote = self.fetch_quote_from_request(&request, provider.clone()).await?; - let swift_address = if let Some(address) = &mayan_quote.swift_mayan_contract { - address.clone() - } else { - return Err(SwapperError::ComputeQuoteError { - msg: "No swift_mayan_contract in quote".to_string(), - }); - }; - let swift_contract = MayanSwift::new(swift_address.clone(), provider.clone(), request.from_asset.chain); + let mayan_quote = self.fetch_quote_from_request(request, provider.clone()).await?; + let swift_address = mayan_quote.swift_mayan_contract.clone().ok_or(SwapperError::ComputeQuoteError { + msg: "No swift_mayan_contract in quote".to_string(), + })?; + let swift_contract = MayanSwift::new(); let swift_order_params = self.build_swift_order_params("e.request, &mayan_quote)?; let forwarder = MayanForwarder::new(); let swift_call_data = if mayan_quote.swift_input_contract == MAYAN_ZERO_ADDRESS { swift_contract - .encode_create_order_with_eth(swift_order_params, quote.from_value.parse().map_err(|_| SwapperError::InvalidAmount)?) + .encode_create_order_with_eth(swift_order_params) .await .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? } else { @@ -421,7 +388,7 @@ impl GemSwapProvider for MayanSwiftProvider { let evm_swap_router_calldata = mayan_quote.evm_swap_router_calldata.clone().ok_or_else(|| SwapperError::ComputeQuoteError { msg: "Missing evmSwapRouterCalldata".to_string(), })?; - let min_middle_amount = mayan_quote.min_middle_amount.clone().ok_or_else(|| SwapperError::ComputeQuoteError { + let min_middle_amount = mayan_quote.min_middle_amount.ok_or_else(|| SwapperError::ComputeQuoteError { msg: "Missing minMiddleAmount".to_string(), })?; @@ -430,17 +397,21 @@ impl GemSwapProvider for MayanSwiftProvider { .get_amount_of_fractional_amount(min_middle_amount, mayan_quote.swift_input_decimals) .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?; - if (mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS) { + let amount_in = U256::from_str(quote.from_value.as_str()).map_err(|_| SwapperError::InvalidAmount)?; + let swap_data = hex::decode(evm_swap_router_calldata.trim_start_matches("0x")).map_err(|_| SwapperError::ABIError { + msg: "Failed to decode evm_swap_router_calldata hex string without prefix 0x ".to_string(), + })?; + let min_middle_amount = U256::from_str(&formatted_min_middle_amount).map_err(|_| SwapperError::InvalidAmount)?; + + if mayan_quote.from_token.contract == MAYAN_ZERO_ADDRESS { forwarder .encode_swap_and_forward_eth_call( - U256::from_str(quote.from_value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, - &evm_swap_router_address.as_str(), - hex::decode(evm_swap_router_calldata.trim_start_matches("0x")).map_err(|_| SwapperError::ABIError { - msg: "Failed to decode evm_swap_router_calldata hex string without prefix 0x ".to_string(), - })?, - &mayan_quote.swift_input_contract.as_str(), - U256::from_str(&formatted_min_middle_amount).map_err(|_| SwapperError::InvalidAmount)?, - &swift_address.as_str(), + amount_in, + evm_swap_router_address.as_str(), + swap_data, + mayan_quote.swift_input_contract.as_str(), + min_middle_amount, + swift_address.as_str(), swift_call_data, ) .await @@ -450,15 +421,13 @@ impl GemSwapProvider for MayanSwiftProvider { forwarder .encode_swap_and_forward_erc20_call( token_in.as_str(), - U256::from_str(quote.from_value.as_str()).map_err(|_| SwapperError::InvalidAmount)?, + amount_in, None, - &evm_swap_router_address.as_str(), - hex::decode(evm_swap_router_calldata.trim_start_matches("0x")).map_err(|_| SwapperError::ABIError { - msg: "Failed to decode evm_swap_router_calldata hex string without prefix 0x ".to_string(), - })?, - &mayan_quote.swift_input_contract.as_str(), - U256::from_str(&formatted_min_middle_amount).map_err(|_| SwapperError::InvalidAmount)?, - &swift_address.as_str(), + evm_swap_router_address.as_str(), + swap_data, + mayan_quote.swift_input_contract.as_str(), + min_middle_amount, + swift_address.as_str(), swift_call_data, ) .map_err(|e| SwapperError::ABIError { msg: e.to_string() })? @@ -474,139 +443,21 @@ impl GemSwapProvider for MayanSwiftProvider { async fn get_transaction_status(&self, chain: Chain, transaction_hash: &str, provider: Arc) -> Result { todo!(); - // let receipt_call = EthereumRpc::GetTransactionReceipt(transaction_hash.to_string()); - // - // let response = jsonrpc_call(&receipt_call, provider, &chain) - // .await - // .map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; - // - // let result: serde_json::Value = response.extract_result().map_err(|e| SwapperError::NetworkError { msg: e.to_string() })?; - // - // if let Some(status_hex) = result.get("status").and_then(|s| s.as_str()) { - // let status = U256::from_str_radix(status_hex.trim_start_matches("0x"), 16).unwrap_or_else(|_| U256::zero()); - // Ok(!status.is_zero()) - // } else { - // Ok(false) - // } } } #[cfg(test)] mod tests { - use alloy_core::sol_types::SolValue; - use alloy_primitives::U256; use primitives::AssetId; - use serde::{Deserialize, Deserializer, Serialize}; - use crate::{ - network::{AlienError, AlienTarget, Data}, - swapper::GemSwapMode, - }; + use crate::swapper::GemSwapMode; use super::*; pub fn generate_mock_quote() -> Quote { - let json_data = r#" - { - "maxUserGasDrop": 0.01, - "sendTransactionCost": 0, - "rentCost": "40000000", - "gasless": false, - "swiftAuctionMode": 2, - "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", - "minMiddleAmount": null, - "swiftInputContract": "0x0000000000000000000000000000000000000000", - "swiftInputDecimals": 18, - "slippageBps": 100, - "effectiveAmountIn": 0.1, - "expectedAmountOut": 0.09972757016582551, - "price": 0.9996900030000002, - "priceImpact": null, - "minAmountOut": 0.09873029446416727, - "minReceived": 0.09873029446416727, - "route": null, - "swapRelayerFee": null, - "swapRelayerFee64": null, - "redeemRelayerFee": null, - "redeemRelayerFee64": null, - "solanaRelayerFee": null, - "solanaRelayerFee64": null, - "refundRelayerFee": null, - "refundRelayerFee64": "1056", - "cancelRelayerFee64": "25", - "submitRelayerFee64": "0", - "deadline64": "1732777812", - "clientRelayerFeeSuccess": null, - "clientRelayerFeeRefund": 0.038947098479114796, - "fromToken": { - "name": "ETH", - "standard": "native", - "symbol": "ETH", - "mint": "", - "verified": true, - "contract": "0x0000000000000000000000000000000000000000", - "wrappedAddress": "0x4200000000000000000000000000000000000006", - "chainId": 8453, - "wChainId": 30, - "decimals": 18, - "logoURI": "https://statics.mayan.finance/eth.png", - "coingeckoId": "weth", - "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", - "realOriginContractAddress": "0x4200000000000000000000000000000000000006", - "realOriginChainId": 30, - "supportsPermit": false, - "hasAuction": true - }, - "fromChain": "base", - "toToken": { - "name": "ETH", - "standard": "native", - "symbol": "ETH", - "mint": "8M6d63oL7dvMZ1gNbgGe3h8afMSWJEKEhtPTFM2u8h3c", - "verified": true, - "contract": "0x0000000000000000000000000000000000000000", - "wrappedAddress": "0x4200000000000000000000000000000000000006", - "chainId": 10, - "wChainId": 24, - "decimals": 18, - "logoURI": "https://statics.mayan.finance/eth.png", - "coingeckoId": "weth", - "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", - "realOriginContractAddress": "0x4200000000000000000000000000000000000006", - "realOriginChainId": 24, - "supportsPermit": false, - "hasAuction": true - }, - "toTokenPrice": 3602.87682508, - "toChain": "optimism", - "mintDecimals": null, - "gasDrop": 0, - "eta": 1, - "etaSeconds": 12, - "clientEta": "12s", - "bridgeFee": 0, - "suggestedPriorityFee": 0, - "type": "SWIFT", - "priceStat": { - "ratio": 0.9996907516582549, - "status": "GOOD" - }, - "referrerBps": 0, - "protocolBps": 3, - "onlyBridging": false, - "sourceSwapExpense": 0, - "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", - "meta": { - "advertisedDescription": "Cheapest and Fastest", - "advertisedTitle": "Best", - "icon": "https://cdn.mayan.finance/fast_icon.png", - "switchText": "Switch to the best route", - "title": "Best" - } - } - "#; + let data = include_str!("tests/quote_response.json"); - let quote: Quote = serde_json::from_str(json_data).expect("Failed to deserialize Quote"); + let quote: Quote = serde_json::from_str(data).expect("Failed to deserialize Quote"); quote } @@ -665,7 +516,6 @@ mod tests { #[test] fn test_convert_amount_to_wei_valid() { let provider = MayanSwiftProvider::new(); - let asset = Asset::from_chain(Chain::Ethereum); let amount = 1.23; // 1.23 ETH let result = provider.convert_amount_to_wei(amount, 18).unwrap(); assert_eq!(result, "1230000000000000000"); // 1.23 ETH in Wei @@ -674,7 +524,6 @@ mod tests { #[test] fn test_convert_amount_to_wei_invalid() { let provider = MayanSwiftProvider::new(); - let asset = Asset::from_chain(Chain::Ethereum); let amount = -1.0; // Negative amount let result = provider.convert_amount_to_wei(amount, 18); assert!(result.is_err()); @@ -707,16 +556,4 @@ mod tests { assert_eq!(result.unwrap(), expected.to_string()); } } - - #[test] - fn test_decode_evm_calldata_valid() { - let calldata = "0x07ed2379000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd09000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000654874eb7f59c6f5b39931fc45dc45337c967c3000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000013db374b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004720000000000000000000000000000000000000000000004540004260003dc416003c01acae3d0173a93d819efdc832c7c4f153b06016452bbbe2900000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e37e799d5077682fa0a244d46e5649f71457bd0900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006749f4bcdef66c6c178087fd931514e99b04479e4d3d956c00020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000016345785d8a000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000006280fa0775121000000000000000000000000000000000000000000000000000000006749f4bc00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000654874eb7f59c6f5b39931fc45dc45337c967c300000000000000000000000000000000000000000000000000005af3107a400000000000000000000000fc892ee4a97800000000000000000166d2f7025080000000000000000003f0a5341099c401550000000000002eb7b4329f0233e9100000000000000000000000000000000000000000000004ee7259d6914ae6c461bc00000000000000004563918244f400000000000000000000006a94d74f4300000000000000000000000000006749f462000000000000000000003b1dfde910000000000000000000000000000000000000000000000000000000000000000041923845af1a0f8e52538eb26d2d7a9d0e9fc833f801c72890cc3aec84fbc1b930696a280a0414c1cab407169bb90cf3c122f85383e9bf614e490c181bcca205b41b0000000000000000000000000000000000000000000000000000000000000000a0f2fa6b66833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000015775e5f000000000000000000000000004c421380a06c4eca27833589fcd6edb6e08f4c7c32d4f71b54bda02913111111125421ca6dc452d289314280a0f8842a6500000000000000000000000000001ea79a4f"; - let without_prefix = calldata.trim_start_matches("0x"); - let decoded_calldata = hex::decode(without_prefix).unwrap(); - let encoded_calldata = hex::encode(&decoded_calldata); - let bytes = calldata.as_bytes(); - - assert_eq!(encoded_calldata, calldata); - assert_eq!(bytes, decoded_calldata); - } } diff --git a/gemstone/src/swapper/mayan/tests/quote_response.json b/gemstone/src/swapper/mayan/tests/quote_response.json new file mode 100644 index 00000000..700f2b35 --- /dev/null +++ b/gemstone/src/swapper/mayan/tests/quote_response.json @@ -0,0 +1,97 @@ +{ + "maxUserGasDrop": 0.01, + "sendTransactionCost": 0, + "rentCost": "40000000", + "gasless": false, + "swiftAuctionMode": 2, + "swiftMayanContract": "0xC38e4e6A15593f908255214653d3D947CA1c2338", + "minMiddleAmount": null, + "swiftInputContract": "0x0000000000000000000000000000000000000000", + "swiftInputDecimals": 18, + "slippageBps": 100, + "effectiveAmountIn": 0.1, + "expectedAmountOut": 0.09972757016582551, + "price": 0.9996900030000002, + "priceImpact": null, + "minAmountOut": 0.09873029446416727, + "minReceived": 0.09873029446416727, + "route": null, + "swapRelayerFee": null, + "swapRelayerFee64": null, + "redeemRelayerFee": null, + "redeemRelayerFee64": null, + "solanaRelayerFee": null, + "solanaRelayerFee64": null, + "refundRelayerFee": null, + "refundRelayerFee64": "1056", + "cancelRelayerFee64": "25", + "submitRelayerFee64": "0", + "deadline64": "1732777812", + "clientRelayerFeeSuccess": null, + "clientRelayerFeeRefund": 0.038947098479114796, + "fromToken": { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 8453, + "wChainId": 30, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "realOriginChainId": 30, + "supportsPermit": false, + "hasAuction": true + }, + "fromChain": "base", + "toToken": { + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "8M6d63oL7dvMZ1gNbgGe3h8afMSWJEKEhtPTFM2u8h3c", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 10, + "wChainId": 24, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "pythUsdPriceId": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "realOriginChainId": 24, + "supportsPermit": false, + "hasAuction": true + }, + "toTokenPrice": 3602.87682508, + "toChain": "optimism", + "mintDecimals": null, + "gasDrop": 0, + "eta": 1, + "etaSeconds": 12, + "clientEta": "12s", + "bridgeFee": 0, + "suggestedPriorityFee": 0, + "type": "SWIFT", + "priceStat": { + "ratio": 0.9996907516582549, + "status": "GOOD" + }, + "referrerBps": 0, + "protocolBps": 3, + "onlyBridging": false, + "sourceSwapExpense": 0, + "relayer": "7dm9am6Qx7cH64RB99Mzf7ZsLbEfmXM7ihXXCvMiT2X1", + "meta": { + "advertisedDescription": "Cheapest and Fastest", + "advertisedTitle": "Best", + "icon": "https://cdn.mayan.finance/fast_icon.png", + "switchText": "Switch to the best route", + "title": "Best" + } +} diff --git a/gemstone/src/swapper/mayan/tests/quote_token_response.json b/gemstone/src/swapper/mayan/tests/quote_token_response.json new file mode 100644 index 00000000..601cdfef --- /dev/null +++ b/gemstone/src/swapper/mayan/tests/quote_token_response.json @@ -0,0 +1,18 @@ +{ + "name": "ETH", + "standard": "native", + "symbol": "ETH", + "mint": "", + "verified": true, + "contract": "0x0000000000000000000000000000000000000000", + "wrappedAddress": "0x4200000000000000000000000000000000000006", + "chainId": 8453, + "wChainId": 30, + "decimals": 18, + "logoURI": "https://statics.mayan.finance/eth.png", + "coingeckoId": "weth", + "realOriginChainId": 30, + "realOriginContractAddress": "0x4200000000000000000000000000000000000006", + "supportsPermit": false, + "hasAuction": true +} diff --git a/gemstone/src/swapper/mod.rs b/gemstone/src/swapper/mod.rs index a0bce809..ea9406b2 100644 --- a/gemstone/src/swapper/mod.rs +++ b/gemstone/src/swapper/mod.rs @@ -41,11 +41,11 @@ impl GemSwapper { Self { rpc_provider, swappers: vec![ - // Box::new(universal_router::UniswapV3::new_uniswap()), - // Box::new(universal_router::UniswapV3::new_pancakeswap()), - // Box::new(thorchain::ThorChain::default()), - // Box::new(jupiter::Jupiter::default()), - Box::new(mayan::mayan_swift_provider::MayanSwiftProvider::new()), + Box::new(universal_router::UniswapV3::new_uniswap()), + Box::new(universal_router::UniswapV3::new_pancakeswap()), + Box::new(thorchain::ThorChain::default()), + Box::new(jupiter::Jupiter::default()), + Box::new(mayan::swift_provider::MayanSwiftProvider::new()), ], } } diff --git a/gemstone/tests/integration_test.rs b/gemstone/tests/integration_test.rs index ea136467..3b6ed8eb 100644 --- a/gemstone/tests/integration_test.rs +++ b/gemstone/tests/integration_test.rs @@ -6,7 +6,7 @@ mod tests { network::{provider::AlienProvider, target::*, *}, swapper::{orca::Orca, *}, }; - use mayan::mayan_swift_provider::MayanSwiftProvider; + use mayan::swift_provider::MayanSwiftProvider; use primitives::{Asset, AssetId, Chain}; use reqwest::Client; use std::{collections::HashMap, sync::Arc}; @@ -146,13 +146,9 @@ mod tests { assert_eq!(quote.data.routes.len(), 1); assert_eq!( quote.data.routes[0].input, - AssetId::from(Chain::Base, Some("0x4200000000000000000000000000000000000042".to_string())), + AssetId::from(Chain::Base, Some("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".to_string())), ); assert_eq!(quote.data.routes[0].output, AssetId::from_chain(Chain::Optimism)); - // assert!(quote.data.routes[0].gas_estimate.is_some(); - - // Verify - // assert_eq!(quote.approval, ApprovalType::None); Ok(()) } From a312f8e8ff72004ce476ea7b201f8e1e4a7de168 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 5 Dec 2024 19:13:18 +0200 Subject: [PATCH 09/15] chore: post merge fix --- gemstone/src/network/mock.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/gemstone/src/network/mock.rs b/gemstone/src/network/mock.rs index 36e7df1f..0447ce27 100644 --- a/gemstone/src/network/mock.rs +++ b/gemstone/src/network/mock.rs @@ -2,8 +2,6 @@ use async_trait::async_trait; use primitives::Chain; use super::{AlienError, AlienProvider, AlienTarget, Data}; -use async_trait::async_trait; -use primitives::Chain; use std::{fmt::Debug, sync::Arc, time::Duration}; #[derive(Debug, uniffi::Object)] From 389892d596bc4864a24247651f3b0f24e57a4ddb Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 5 Dec 2024 19:15:08 +0200 Subject: [PATCH 10/15] chore: refactored forwarder errros --- gemstone/src/swapper/mayan/forwarder.rs | 37 +++++++++++++++---------- gemstone/src/swapper/mayan/mod.rs | 1 - gemstone/src/swapper/mayan/models.rs | 7 ----- 3 files changed, 22 insertions(+), 23 deletions(-) delete mode 100644 gemstone/src/swapper/mayan/models.rs diff --git a/gemstone/src/swapper/mayan/forwarder.rs b/gemstone/src/swapper/mayan/forwarder.rs index 1d05c2e3..3ef5b266 100644 --- a/gemstone/src/swapper/mayan/forwarder.rs +++ b/gemstone/src/swapper/mayan/forwarder.rs @@ -5,8 +5,15 @@ use alloy_core::{ sol_types::SolCall, }; use gem_evm::mayan::forwarder::IMayanForwarder; +use thiserror::Error; -use super::{models::MayanError, swift::MayanSwiftPermit}; +use super::swift::MayanSwiftPermit; + +#[derive(Debug, Error)] +pub enum MayanForwarderError { + #[error("ABI Error: {msg}")] + ABIError { msg: String }, +} pub struct MayanForwarder {} @@ -15,9 +22,9 @@ impl MayanForwarder { Self {} } - pub async fn encode_forward_eth_call(&self, mayan_protocol: &str, protocol_data: Vec) -> Result, MayanError> { + pub async fn encode_forward_eth_call(&self, mayan_protocol: &str, protocol_data: Vec) -> Result, MayanForwarderError> { let call_data = IMayanForwarder::forwardEthCall { - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid protocol address: {}", e), })?, protocolData: protocol_data.into(), @@ -34,14 +41,14 @@ impl MayanForwarder { permit: Option, mayan_protocol: &str, protocol_data: Vec, - ) -> Result, MayanError> { + ) -> Result, MayanForwarderError> { let call_data = IMayanForwarder::forwardERC20Call { - tokenIn: Address::from_str(token_in).map_err(|e| MayanError::ABIError { + tokenIn: Address::from_str(token_in).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid token address: {}", e), })?, amountIn: amount_in, permitParams: permit.map_or(MayanSwiftPermit::zero().to_contract_params(), |p| p.to_contract_params()), - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid protocol address: {}", e), })?, protocolData: protocol_data.into(), @@ -60,18 +67,18 @@ impl MayanForwarder { min_middle_amount: U256, mayan_protocol: &str, mayan_data: Vec, - ) -> Result, MayanError> { + ) -> Result, MayanForwarderError> { let call_data = IMayanForwarder::swapAndForwardEthCall { amountIn: amount_in, - swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanError::ABIError { + swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid swap protocol address: {}", e), })?, swapData: swap_data.into(), - middleToken: Address::from_str(middle_token).map_err(|e| MayanError::ABIError { + middleToken: Address::from_str(middle_token).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid middle token address: {}", e), })?, minMiddleAmount: min_middle_amount, - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid mayan protocol address: {}", e), })?, mayanData: mayan_data.into(), @@ -92,22 +99,22 @@ impl MayanForwarder { min_middle_amount: U256, mayan_protocol: &str, mayan_data: Vec, - ) -> Result, MayanError> { + ) -> Result, MayanForwarderError> { let call_data = IMayanForwarder::swapAndForwardERC20Call { - tokenIn: Address::from_str(token_in).map_err(|e| MayanError::ABIError { + tokenIn: Address::from_str(token_in).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid token address: {}", e), })?, amountIn: amount_in, permitParams: permit.map_or(MayanSwiftPermit::zero().to_contract_params(), |p| p.to_contract_params()), - swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanError::ABIError { + swapProtocol: Address::from_str(swap_protocol).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid swap protocol address: {}", e), })?, swapData: swap_data.into(), - middleToken: Address::from_str(middle_token).map_err(|e| MayanError::ABIError { + middleToken: Address::from_str(middle_token).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid middle token address: {}", e), })?, minMiddleAmount: min_middle_amount, - mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanError::ABIError { + mayanProtocol: Address::from_str(mayan_protocol).map_err(|e| MayanForwarderError::ABIError { msg: format!("Invalid mayan protocol address: {}", e), })?, mayanData: mayan_data.into(), diff --git a/gemstone/src/swapper/mayan/mod.rs b/gemstone/src/swapper/mayan/mod.rs index 4659c604..2bf23ae0 100644 --- a/gemstone/src/swapper/mayan/mod.rs +++ b/gemstone/src/swapper/mayan/mod.rs @@ -1,6 +1,5 @@ mod constants; mod forwarder; -mod models; mod relayer; mod swift; pub mod swift_provider; diff --git a/gemstone/src/swapper/mayan/models.rs b/gemstone/src/swapper/mayan/models.rs deleted file mode 100644 index 0043aad1..00000000 --- a/gemstone/src/swapper/mayan/models.rs +++ /dev/null @@ -1,7 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum MayanError { - #[error("ABI Error: {msg}")] - ABIError { msg: String }, -} From 004270d000ad2165bc013fcd9dac9c5e6db56794 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 5 Dec 2024 19:34:09 +0200 Subject: [PATCH 11/15] chore: imporved wormhole id --- crates/gem_evm/src/mayan/swift/deployment.rs | 150 +++++++++++-------- gemstone/src/swapper/mayan/swift_provider.rs | 2 +- 2 files changed, 90 insertions(+), 62 deletions(-) diff --git a/crates/gem_evm/src/mayan/swift/deployment.rs b/crates/gem_evm/src/mayan/swift/deployment.rs index 4cb0f8ac..2e97cde2 100644 --- a/crates/gem_evm/src/mayan/swift/deployment.rs +++ b/crates/gem_evm/src/mayan/swift/deployment.rs @@ -2,65 +2,75 @@ use std::collections::HashMap; use primitives::Chain; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum MayanSwiftDeploymentWormholeId { + Solana = 1, + Ethereum = 2, + SmartChain = 4, + Polygon = 5, + Arbitrum = 23, + Optimism = 24, + Base = 30, +} + #[derive(Debug, Clone, PartialEq)] pub struct MayanSwiftDeployment { pub address: String, - pub wormhole_id: u64, + pub wormhole_id: MayanSwiftDeploymentWormholeId, } pub fn get_swift_providers() -> HashMap { - let mut map = HashMap::new(); - map.insert( - Chain::Solana, - MayanSwiftDeployment { - address: "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY".to_string(), - wormhole_id: 1, - }, - ); - map.insert( - Chain::Ethereum, - MayanSwiftDeployment { - address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), - wormhole_id: 2, - }, - ); - map.insert( - Chain::SmartChain, - MayanSwiftDeployment { - address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), - wormhole_id: 4, - }, - ); - map.insert( - Chain::Polygon, - MayanSwiftDeployment { - address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), - wormhole_id: 5, - }, - ); - map.insert( - Chain::Arbitrum, - MayanSwiftDeployment { - address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), - wormhole_id: 23, - }, - ); - map.insert( - Chain::Optimism, - MayanSwiftDeployment { - address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), - wormhole_id: 24, - }, - ); - map.insert( - Chain::Base, - MayanSwiftDeployment { - address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), - wormhole_id: 30, - }, - ); - - map + HashMap::from([ + ( + Chain::Solana, + MayanSwiftDeployment { + address: "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::Solana, + }, + ), + ( + Chain::Ethereum, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::Ethereum, + }, + ), + ( + Chain::SmartChain, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::SmartChain, + }, + ), + ( + Chain::Polygon, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::Polygon, + }, + ), + ( + Chain::Arbitrum, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::Arbitrum, + }, + ), + ( + Chain::Optimism, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::Optimism, + }, + ), + ( + Chain::Base, + MayanSwiftDeployment { + address: "0xC38e4e6A15593f908255214653d3D947CA1c2338".to_string(), + wormhole_id: MayanSwiftDeploymentWormholeId::Base, + }, + ), + ]) } pub fn get_swift_deployment_chains() -> Vec { @@ -68,7 +78,7 @@ pub fn get_swift_deployment_chains() -> Vec { } pub fn get_swift_deployment_by_chain(chain: Chain) -> Option { - get_swift_providers().get(&chain).map(|x| x.clone()) + get_swift_providers().get(&chain).cloned() } #[cfg(test)] @@ -82,7 +92,7 @@ mod tests { get_swift_deployment_by_chain(Chain::Solana), Some(MayanSwiftDeployment { address: "BLZRi6frs4X4DNLw56V4EXai1b6QVESN1BhHBTYM9VcY".to_string(), - wormhole_id: 1, + wormhole_id: MayanSwiftDeploymentWormholeId::Solana, }) ); @@ -105,12 +115,30 @@ mod tests { #[test] fn test_chain_ids() { // Verify chain IDs match the provided table, and that they are in order - assert_eq!(get_swift_deployment_by_chain(Chain::Solana).map(|x| x.wormhole_id), Some(1)); - assert_eq!(get_swift_deployment_by_chain(Chain::Ethereum).map(|x| x.wormhole_id), Some(2)); - assert_eq!(get_swift_deployment_by_chain(Chain::SmartChain).map(|x| x.wormhole_id), Some(4)); - assert_eq!(get_swift_deployment_by_chain(Chain::Polygon).map(|x| x.wormhole_id), Some(5)); + assert_eq!( + get_swift_deployment_by_chain(Chain::Solana).map(|x| x.wormhole_id), + Some(MayanSwiftDeploymentWormholeId::Solana) + ); + assert_eq!( + get_swift_deployment_by_chain(Chain::Ethereum).map(|x| x.wormhole_id), + Some(MayanSwiftDeploymentWormholeId::Ethereum) + ); + assert_eq!( + get_swift_deployment_by_chain(Chain::SmartChain).map(|x| x.wormhole_id), + Some(MayanSwiftDeploymentWormholeId::SmartChain) + ); + assert_eq!( + get_swift_deployment_by_chain(Chain::Polygon).map(|x| x.wormhole_id), + Some(MayanSwiftDeploymentWormholeId::Polygon) + ); // assert_eq!(get_swift_deployment_by_chain(Chain::Avalanche).map(|x| x.wormhole_id), Some(6)); - assert_eq!(get_swift_deployment_by_chain(Chain::Arbitrum).map(|x| x.wormhole_id), Some(23)); - assert_eq!(get_swift_deployment_by_chain(Chain::Optimism).map(|x| x.wormhole_id), Some(24)); + assert_eq!( + get_swift_deployment_by_chain(Chain::Arbitrum).map(|x| x.wormhole_id), + Some(MayanSwiftDeploymentWormholeId::Arbitrum) + ); + assert_eq!( + get_swift_deployment_by_chain(Chain::Optimism).map(|x| x.wormhole_id), + Some(MayanSwiftDeploymentWormholeId::Optimism) + ); } } diff --git a/gemstone/src/swapper/mayan/swift_provider.rs b/gemstone/src/swapper/mayan/swift_provider.rs index d9b1f617..f6c87bb2 100644 --- a/gemstone/src/swapper/mayan/swift_provider.rs +++ b/gemstone/src/swapper/mayan/swift_provider.rs @@ -49,7 +49,7 @@ impl MayanSwiftProvider { fn get_chain_by_wormhole_id(&self, wormhole_id: u64) -> Option { get_swift_providers() .into_iter() - .find(|(_, deployment)| deployment.wormhole_id == wormhole_id) + .find(|(_, deployment)| deployment.wormhole_id.clone() as u64 == wormhole_id) .map(|(chain, _)| chain) } From 18be7e28626f9cba78e614f912df6f8ba166d84a Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Thu, 5 Dec 2024 19:37:16 +0200 Subject: [PATCH 12/15] chore: removed unused defaults --- gemstone/src/swapper/mayan/relayer.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gemstone/src/swapper/mayan/relayer.rs b/gemstone/src/swapper/mayan/relayer.rs index 77706a9a..b68498fd 100644 --- a/gemstone/src/swapper/mayan/relayer.rs +++ b/gemstone/src/swapper/mayan/relayer.rs @@ -34,13 +34,9 @@ pub struct QuoteParams { #[derive(Debug, Clone, Serialize)] pub struct QuoteOptions { - #[serde(default = "default_true")] pub swift: bool, - #[serde(default = "default_true")] pub mctp: bool, - #[serde(default = "default_false")] pub gasless: bool, - #[serde(default = "default_false")] pub only_direct: bool, } From 46dd9bfb6d229b91e27de0e2950aca7eea1a3d19 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Mon, 9 Dec 2024 12:31:52 +0200 Subject: [PATCH 13/15] chore: cargo formatting --- Cargo.toml | 58 ++++++++++++++++++++++----------------------- gemstone/Cargo.toml | 6 ++--- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b46ef46..9a511532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,42 +10,42 @@ documentation = "https://github.com/gemwalletcom" [workspace] resolver = "2" members = [ - "apps/api", - "apps/daemon", - "apps/parser", - "apps/setup", + "apps/api", + "apps/daemon", + "apps/parser", + "apps/setup", - "bin/img-downloader", - "bin/generate", + "bin/img-downloader", + "bin/generate", - "bin/uniffi-bindgen", - "gemstone", + "bin/uniffi-bindgen", + "gemstone", - "crates/primitives", - "crates/blockchain", - "crates/fiat", - "crates/cacher", - "crates/name_resolver", - "crates/api_connector", - "crates/settings", - "crates/settings_chain", - "crates/pricer", - "crates/chain_primitives", + "crates/primitives", + "crates/blockchain", + "crates/fiat", + "crates/cacher", + "crates/name_resolver", + "crates/api_connector", + "crates/settings", + "crates/settings_chain", + "crates/pricer", + "crates/chain_primitives", - "crates/security_*", - "crates/gem_*", + "crates/security_*", + "crates/gem_*", - "crates/localizer", - "crates/job_runner", + "crates/localizer", + "crates/job_runner", ] default-members = [ - "apps/api", - "apps/daemon", - "apps/parser", - "apps/setup", - "bin/generate", - "gemstone", + "apps/api", + "apps/daemon", + "apps/parser", + "apps/setup", + "bin/generate", + "gemstone", ] [workspace.dependencies] @@ -117,5 +117,5 @@ rust-embed = { version = "8.5.0" } # numbers rusty-money = { git = "https://github.com/varunsrin/rusty_money.git", rev = "bbc0150", features = [ - "iso", + "iso", ] } diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 1a80d2e3..33412efd 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -5,9 +5,9 @@ version = "0.1.1" [lib] crate-type = [ - "staticlib", # iOS - "rlib", # for Other crate - "cdylib", # Android + "staticlib", # iOS + "rlib", # for Other crate + "cdylib", # Android ] name = "gemstone" From 7ab8096745ba99b374b6776def5c44d7d3493b38 Mon Sep 17 00:00:00 2001 From: Ivan Kuchmenko Date: Mon, 9 Dec 2024 13:29:10 +0200 Subject: [PATCH 14/15] chore: post merge --- gemstone/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 90125509..7fe17e9b 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -38,6 +38,7 @@ url.workspace = true num-bigint.workspace = true futures.workspace = true borsh.workspace = true +rand.workspace = true orca_whirlpools_core = "1.0.0" [build-dependencies] From a882c7765e6bdc785399780853565b787476d3e6 Mon Sep 17 00:00:00 2001 From: Ichigo Date: Tue, 17 Dec 2024 09:35:39 +0200 Subject: [PATCH 15/15] chore: updated to main --- gemstone/src/swapper/mayan/swift_provider.rs | 78 ++++++++++---------- gemstone/src/swapper/mod.rs | 2 +- gemstone/src/swapper/models.rs | 1 + gemstone/tests/integration_test.rs | 14 +++- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/gemstone/src/swapper/mayan/swift_provider.rs b/gemstone/src/swapper/mayan/swift_provider.rs index f6c87bb2..33d8489c 100644 --- a/gemstone/src/swapper/mayan/swift_provider.rs +++ b/gemstone/src/swapper/mayan/swift_provider.rs @@ -3,7 +3,10 @@ use std::{str::FromStr, sync::Arc}; use alloy_core::{ hex::{decode as HexDecode, ToHexExt}, - primitives::U256, + primitives::{ + utils::{parse_units, Unit}, + U256, + }, sol_types::SolCall, }; @@ -32,7 +35,7 @@ use super::{ swift::{MayanSwift, MayanSwiftError, OrderParams}, }; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct MayanSwiftProvider {} impl From for SwapperError { @@ -42,10 +45,6 @@ impl From for SwapperError { } impl MayanSwiftProvider { - pub fn new() -> Self { - Self {} - } - fn get_chain_by_wormhole_id(&self, wormhole_id: u64) -> Option { get_swift_providers() .into_iter() @@ -144,17 +143,15 @@ impl MayanSwiftProvider { } fn get_referrer(&self, request: &SwapQuoteRequest) -> Result, SwapperError> { - if let Some(options) = &request.options { - if let Some(referrer) = &options.fee { - let evm_fee = &referrer.evm; - let solana_fee = &referrer.solana; - - if request.from_asset.chain == Chain::Solana { - return Ok(Some(solana_fee.clone())); - } + if let Some(referrer) = &request.options.fee { + let evm_fee = &referrer.evm; + let solana_fee = &referrer.solana; - return Ok(Some(evm_fee.clone())); + if request.from_asset.chain == Chain::Solana { + return Ok(Some(solana_fee.clone())); } + + return Ok(Some(evm_fee.clone())); } Ok(None) @@ -203,20 +200,18 @@ impl MayanSwiftProvider { }) } - fn convert_amount_to_wei(&self, amount: f64, decimals: u32) -> Result { - // Calculate the scaling factor (10^decimals) - let scaling_factor = 10f64.powi(decimals as i32); - - // Convert the amount to Wei (or the smallest unit) - let amount_in_wei = (amount * scaling_factor).round(); // `round` ensures correct conversion - - // Ensure the amount is within a valid range for integers - if amount_in_wei < 0.0 || amount_in_wei > (u64::MAX as f64) { - return Err(SwapperError::InvalidAmount); + fn convert_amount_to_wei(&self, amount: f64, decimals: u8) -> Result { + if amount < 0.0 { + return Err(SwapperError::ComputeQuoteError { + msg: "Cannot convert negative amount".to_string(), + }); } - // Convert the result to a string for return - Ok(format!("{:.0}", amount_in_wei)) + let parsed = parse_units(amount.to_string().as_str(), decimals).map_err(|_| SwapperError::ComputeQuoteError { + msg: "Invalid conversion amount to decimals".to_string(), + })?; + + Ok(parsed.to_string()) } fn get_amount_of_fractional_amount(&self, amount: f64, decimals: u8) -> Result { @@ -321,10 +316,11 @@ impl GemSwapProvider for MayanSwiftProvider { Ok(SwapQuote { from_value: request.value.clone(), to_value: self - .convert_amount_to_wei(quote.min_amount_out, quote.to_token.decimals.into()) + .convert_amount_to_wei(quote.min_amount_out, quote.to_token.decimals) .map_err(|e| SwapperError::ComputeQuoteError { msg: e.to_string() })?, data: SwapProviderData { provider: self.provider(), + suggested_slippage_bps: Some(quote.slippage_bps), routes: vec![route], }, approval, @@ -450,7 +446,7 @@ impl GemSwapProvider for MayanSwiftProvider { mod tests { use primitives::AssetId; - use crate::swapper::GemSwapMode; + use crate::swapper::{GemSwapMode, GemSwapOptions}; use super::*; @@ -476,13 +472,17 @@ mod tests { }, value: "1230000000000000000".to_string(), mode: GemSwapMode::ExactIn, - options: None, // From JSON field "effectiveAmountIn" + options: GemSwapOptions { + slippage_bps: 12, + fee: None, + preferred_providers: vec![], + }, } } #[test] fn test_supported_chains() { - let provider = MayanSwiftProvider::new(); + let provider = MayanSwiftProvider::default(); let chains = provider.supported_chains(); assert!(chains.contains(&Chain::Solana)); @@ -496,7 +496,7 @@ mod tests { #[test] fn test_address_to_bytes32_valid() { - let provider = MayanSwiftProvider::new(); + let provider = MayanSwiftProvider::default(); let address = "0x0655c6AbdA5e2a5241aa08486bd50Cf7d475CF24"; let bytes32 = provider.address_to_bytes32(address).unwrap(); let expected_bytes32 = [ @@ -507,7 +507,7 @@ mod tests { #[test] fn test_address_to_bytes32_invalid() { - let provider = MayanSwiftProvider::new(); + let provider = MayanSwiftProvider::default(); let invalid_address = "invalid_address"; let result = provider.address_to_bytes32(invalid_address); assert!(result.is_err()); @@ -515,23 +515,23 @@ mod tests { #[test] fn test_convert_amount_to_wei_valid() { - let provider = MayanSwiftProvider::new(); - let amount = 1.23; // 1.23 ETH + let provider = MayanSwiftProvider::default(); + let amount = 1.23; let result = provider.convert_amount_to_wei(amount, 18).unwrap(); assert_eq!(result, "1230000000000000000"); // 1.23 ETH in Wei } #[test] fn test_convert_amount_to_wei_invalid() { - let provider = MayanSwiftProvider::new(); - let amount = -1.0; // Negative amount + let provider = MayanSwiftProvider::default(); + let amount = -1.0; let result = provider.convert_amount_to_wei(amount, 18); assert!(result.is_err()); } #[test] fn test_build_swift_order_params_valid() { - let provider = MayanSwiftProvider::new(); + let provider = MayanSwiftProvider::default(); let request = generate_mock_request(); let quote = generate_mock_quote(); @@ -550,7 +550,7 @@ mod tests { ]; for (amount, decimals, expected) in test_cases { - let provider = MayanSwiftProvider::new(); + let provider = MayanSwiftProvider::default(); let result = provider.get_amount_of_fractional_amount(amount, decimals); assert!(result.is_ok(), "Failed for amount: {}", amount); assert_eq!(result.unwrap(), expected.to_string()); diff --git a/gemstone/src/swapper/mod.rs b/gemstone/src/swapper/mod.rs index 26803424..b7efad1e 100644 --- a/gemstone/src/swapper/mod.rs +++ b/gemstone/src/swapper/mod.rs @@ -61,7 +61,7 @@ impl GemSwapper { Box::new(universal_router::UniswapV3::new_pancakeswap()), Box::new(thorchain::ThorChain::default()), Box::new(jupiter::Jupiter::default()), - Box::new(mayan::swift_provider::MayanSwiftProvider::new()), + Box::new(mayan::swift_provider::MayanSwiftProvider::default()), Box::new(pancakeswap_aptos::PancakeSwapAptos::default()), ], } diff --git a/gemstone/src/swapper/models.rs b/gemstone/src/swapper/models.rs index 58d897d4..05614822 100644 --- a/gemstone/src/swapper/models.rs +++ b/gemstone/src/swapper/models.rs @@ -90,6 +90,7 @@ impl SwapProvider { Self::Thorchain => SwapProviderType::CrossChain, Self::Orca => SwapProviderType::OnChain, Self::Jupiter => SwapProviderType::OnChain, + Self::MayanSwift => SwapProviderType::CrossChain, } } } diff --git a/gemstone/tests/integration_test.rs b/gemstone/tests/integration_test.rs index 3b6ed8eb..b105837e 100644 --- a/gemstone/tests/integration_test.rs +++ b/gemstone/tests/integration_test.rs @@ -106,7 +106,11 @@ mod tests { destination_address: "G7B17AigRCGvwnxFc5U8zY5T3NBGduLzT7KYApNU2VdR".into(), value: "1000000".into(), mode: GemSwapMode::ExactIn, - options: None, + options: GemSwapOptions { + slippage_bps: 10, + fee: None, + preferred_providers: vec![], + }, }; let quote = swap_provider.fetch_quote(&request, network_provider.clone()).await?; @@ -123,7 +127,7 @@ mod tests { let node_config = HashMap::from([(Chain::Base, "https://mainnet.base.org".to_string())]); let network_provider = Arc::new(NativeProvider::new(node_config)); - let mayan_swift_provider = MayanSwiftProvider::new(); + let mayan_swift_provider = MayanSwiftProvider::default(); // Create a swap quote request let request = SwapQuoteRequest { @@ -133,7 +137,11 @@ mod tests { destination_address: TEST_WALLET_ADDRESS.to_string(), value: "9000000".to_string(), mode: GemSwapMode::ExactIn, // Swap mode - options: None, + options: GemSwapOptions { + slippage_bps: 10, + fee: None, + preferred_providers: vec![], + }, }; let quote = mayan_swift_provider.fetch_quote(&request, network_provider.clone()).await?;