diff --git a/Cargo.lock b/Cargo.lock index b7a61105e..3d6614459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7466,6 +7466,7 @@ dependencies = [ "base64", "bcs", "bigdecimal", + "borsh", "bs58", "chrono", "futures", diff --git a/crates/gem_evm/src/across/contracts/spoke_pool.rs b/crates/gem_evm/src/across/contracts/spoke_pool.rs index 15f0966e5..4d3457177 100644 --- a/crates/gem_evm/src/across/contracts/spoke_pool.rs +++ b/crates/gem_evm/src/across/contracts/spoke_pool.rs @@ -1,7 +1,7 @@ use alloy_sol_types::sol; // https://docs.across.to/reference/selected-contract-functions -// https://github.com/across-protocol/contracts/blob/master/contracts/interfaces/SpokePoolInterface.sol +// https://github.com/across-protocol/contracts/blob/master/contracts/interfaces/V3SpokePoolInterface.sol sol! { // Contains structs and functions used by SpokePool contracts to facilitate universal settlement. interface V3SpokePoolInterface { @@ -10,16 +10,16 @@ sol! { // replay attacks on other chains. If any portion of this data differs, the relay is considered to be // completely distinct. struct V3RelayData { - // The address that made the deposit on the origin chain. - address depositor; - // The recipient address on the destination chain. - address recipient; + // The bytes32 that made the deposit on the origin chain. + bytes32 depositor; + // The recipient bytes32 on the destination chain. + bytes32 recipient; // This is the exclusive relayer who can fill the deposit before the exclusivity deadline. - address exclusiveRelayer; + bytes32 exclusiveRelayer; // Token that is deposited on origin chain by depositor. - address inputToken; + bytes32 inputToken; // Token that is received on destination chain by recipient. - address outputToken; + bytes32 outputToken; // The amount of input token deposited by depositor. uint256 inputAmount; // The amount of output token to be received by recipient. @@ -27,7 +27,7 @@ sol! { // Origin chain id. uint256 originChainId; // The id uniquely identifying this deposit on the origin chain. - uint32 depositId; + uint256 depositId; // The timestamp on the destination chain after which this deposit can no longer be filled. uint32 fillDeadline; // The timestamp on the destination chain after which any relayer can fill the deposit. @@ -38,21 +38,25 @@ sol! { function getCurrentTime() public view virtual returns (uint256); - function depositV3( - address depositor, - address recipient, - address inputToken, - address outputToken, + function deposit( + bytes32 depositor, + bytes32 recipient, + bytes32 inputToken, + bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, - address exclusiveRelayer, + bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes calldata message ) external payable; - function fillV3Relay(V3RelayData calldata relayData, uint256 repaymentChainId) external; + function fillRelay( + V3RelayData calldata relayData, + uint256 repaymentChainId, + bytes32 repaymentAddress + ) external; } } diff --git a/crates/gem_evm/src/across/deployment.rs b/crates/gem_evm/src/across/deployment.rs index 3103f7a1a..7e7662f98 100644 --- a/crates/gem_evm/src/across/deployment.rs +++ b/crates/gem_evm/src/across/deployment.rs @@ -2,16 +2,18 @@ use super::fees::CapitalCostConfig; use crate::ether_conv::EtherConv; use alloy_primitives::map::HashSet; use num_bigint::BigInt; -use primitives::{AssetId, Chain, asset_constants::*}; +use primitives::{AssetId, Chain, ChainType, asset_constants::*}; use std::{collections::HashMap, vec}; pub const ACROSS_CONFIG_STORE: &str = "0x3B03509645713718B78951126E0A6de6f10043f5"; pub const ACROSS_HUBPOOL: &str = "0xc186fA914353c44b2E33eBE05f21846F1048bEda"; pub const MULTICALL_HANDLER: &str = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; +static SOLANA_CHAIN_ID: u64 = 34268394551451_u64; /// https://docs.across.to/developer-docs/developers/contract-addresses pub struct AcrossDeployment { - pub chain_id: u32, + pub chain_id: u64, + pub chain_type: ChainType, pub spoke_pool: &'static str, } @@ -23,68 +25,92 @@ pub struct AssetMapping { impl AcrossDeployment { pub fn deployment_by_chain(chain: &Chain) -> Option { - let chain_id: u32 = chain.network_id().parse().unwrap(); + let chain_id: u64 = if chain.chain_type() == ChainType::Solana { + SOLANA_CHAIN_ID + } else { + chain.network_id().parse().unwrap() + }; match chain { Chain::Ethereum => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5", }), Chain::Arbitrum => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0xe35e9842fceaca96570b734083f4a58e8f7c5f2a", }), Chain::Base => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", }), Chain::Blast => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x2D509190Ed0172ba588407D4c2df918F955Cc6E1", }), Chain::Linea => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75", }), Chain::Optimism => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x6f26Bf09B1C792e3228e5467807a900A503c0281", }), Chain::Polygon => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096", }), Chain::World => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", }), Chain::ZkSync => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0xE0B015E54d54fc84a6cB9B666099c46adE9335FF", }), Chain::Ink => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0xeF684C38F94F48775959ECf2012D7E864ffb9dd4", }), Chain::Unichain => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", }), Chain::Monad => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0xd2ecb3afe598b746F8123CaE365a598DA831A449", }), Chain::SmartChain => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x4e8E101924eDE233C13e2D8622DC8aED2872d505", }), Chain::Hyperliquid => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x35E63eA3eb0fb7A3bc543C71FB66412e1F6B0E04", }), Chain::Plasma => Some(Self { chain_id, + chain_type: ChainType::Ethereum, spoke_pool: "0x50039fAEfebef707cFD94D6d462fE6D10B39207a", }), + Chain::Solana => Some(Self { + chain_id: SOLANA_CHAIN_ID, + chain_type: ChainType::Solana, + spoke_pool: "DLv3NggMiSaef97YCkew5xKUHDh13tVGZ7tydt3ZeAru", + }), _ => None, } } @@ -92,15 +118,15 @@ impl AcrossDeployment { pub fn multicall_handler(&self) -> String { match self.chain_id { // Linea - 59144 => "0x1015c58894961F4F7Dd7D68ba033e28Ed3ee1cDB".into(), + 59144_u64 => "0x1015c58894961F4F7Dd7D68ba033e28Ed3ee1cDB".into(), // zkSync - 324 => "0x863859ef502F0Ee9676626ED5B418037252eFeb2".into(), + 324_u64 => "0x863859ef502F0Ee9676626ED5B418037252eFeb2".into(), // SmartChain - 56 => "0xAC537C12fE8f544D712d71ED4376a502EEa944d7".into(), + 56_u64 => "0xAC537C12fE8f544D712d71ED4376a502EEa944d7".into(), // Monad - 143 => "0xeC41F75c686e376Ab2a4F18bde263ab5822c4511".into(), + 143_u64 => "0xeC41F75c686e376Ab2a4F18bde263ab5822c4511".into(), // HyperEvm | Plasma - 999 | 9745 => "0x5E7840E06fAcCb6d1c3b5F5E0d1d3d07F2829bba".into(), + 999_u64 | 9745_u64 => "0x5E7840E06fAcCb6d1c3b5F5E0d1d3d07F2829bba".into(), _ => MULTICALL_HANDLER.into(), } } @@ -162,6 +188,7 @@ impl AcrossDeployment { (Chain::Monad, vec![USDC_MONAD_ASSET_ID.into(), USDT_MONAD_ASSET_ID.into()]), (Chain::SmartChain, vec![ETH_SMARTCHAIN_ASSET_ID.into()]), (Chain::Plasma, vec![USDT_PLASMA_ASSET_ID.into()]), + (Chain::Solana, vec![USDC_SOLANA_ASSET_ID.into()]), ]) } @@ -205,6 +232,7 @@ impl AcrossDeployment { USDC_UNICHAIN_ASSET_ID.into(), USDC_HYPEREVM_ASSET_ID.into(), USDC_MONAD_ASSET_ID.into(), + USDC_SOLANA_ASSET_ID.into(), ]), }, // USDC on BSC decimals are 18 diff --git a/crates/gem_evm/src/chainlink/contract.rs b/crates/gem_evm/src/chainlink/contract.rs index c7b4f1e4b..ef553f184 100644 --- a/crates/gem_evm/src/chainlink/contract.rs +++ b/crates/gem_evm/src/chainlink/contract.rs @@ -9,3 +9,4 @@ sol! { pub const CHAINLINK_ETH_USD_FEED: &str = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"; pub const CHAINLINK_MON_USD_FEED: &str = "0xBcD78f76005B7515837af6b50c7C52BCf73822fb"; +pub const CHAINLINK_SOL_USD_FEED: &str = "0x4ffC43a60e009B551865A93d232E33Fce9f01507"; diff --git a/crates/gem_solana/src/jsonrpc.rs b/crates/gem_solana/src/jsonrpc.rs index eb3b2e82c..43ae0cee1 100644 --- a/crates/gem_solana/src/jsonrpc.rs +++ b/crates/gem_solana/src/jsonrpc.rs @@ -13,6 +13,7 @@ pub enum SolanaRpc { GetMultipleAccounts(Vec), GetEpochInfo, GetLatestBlockhash, + GetRecentPrioritizationFees, } impl Display for SolanaRpc { @@ -23,6 +24,7 @@ impl Display for SolanaRpc { SolanaRpc::GetMultipleAccounts(_) => write!(f, "getMultipleAccounts"), SolanaRpc::GetEpochInfo => write!(f, "getEpochInfo"), SolanaRpc::GetLatestBlockhash => write!(f, "getLatestBlockhash"), + SolanaRpc::GetRecentPrioritizationFees => write!(f, "getRecentPrioritizationFees"), } } } @@ -43,7 +45,7 @@ impl JsonRpcRequestConvert for SolanaRpc { Value::Array(accounts.iter().map(|x| serde_json::to_value(x).unwrap()).collect()), serde_json::to_value(default_config).unwrap(), ], - SolanaRpc::GetEpochInfo | SolanaRpc::GetLatestBlockhash => vec![], + SolanaRpc::GetEpochInfo | SolanaRpc::GetLatestBlockhash | SolanaRpc::GetRecentPrioritizationFees => vec![], }; JsonRpcRequest::new(id, &method, params.into()) diff --git a/crates/swapper/Cargo.toml b/crates/swapper/Cargo.toml index 59a8d9e50..6ad8b783b 100644 --- a/crates/swapper/Cargo.toml +++ b/crates/swapper/Cargo.toml @@ -34,6 +34,7 @@ number_formatter = { path = "../number_formatter" } reqwest = { workspace = true, optional = true } bcs.workspace = true +borsh.workspace = true sui-types = { workspace = true } sui-transaction-builder = { workspace = true } diff --git a/crates/swapper/src/across/mod.rs b/crates/swapper/src/across/mod.rs index 54a247b0d..074af7922 100644 --- a/crates/swapper/src/across/mod.rs +++ b/crates/swapper/src/across/mod.rs @@ -3,6 +3,8 @@ pub use provider::Across; pub mod api; pub mod config_store; pub mod hubpool; +pub mod models; +pub mod solana; const DEFAULT_FILL_TIMEOUT: u32 = 60 * 60 * 6; // 6 hours const DEFAULT_DEPOSIT_GAS_LIMIT: u64 = 180_000; // gwei diff --git a/crates/swapper/src/across/models.rs b/crates/swapper/src/across/models.rs new file mode 100644 index 000000000..7204216f2 --- /dev/null +++ b/crates/swapper/src/across/models.rs @@ -0,0 +1,36 @@ +use alloy_primitives::{Address, U256}; +use gem_evm::across::{deployment::AcrossDeployment, fees}; +use primitives::{AssetId, Chain}; +use solana_primitives::types::Pubkey as SolanaPubkey; + +use crate::config::ReferralFee; + +pub struct QuoteContext<'a> { + pub from_amount: U256, + pub wallet_address: Address, + pub from_chain: Chain, + pub to_chain: Chain, + pub input_is_native: bool, + pub input_asset: AssetId, + pub output_asset: AssetId, + pub original_output_asset: AssetId, + pub mainnet_token: Address, + pub capital_cost: fees::CapitalCostConfig, + pub referral_fee: ReferralFee, + pub destination_deployment: AcrossDeployment, + pub destination_address: Option<&'a str>, + pub output_token_decimals: u8, +} + +#[derive(Clone, Debug)] +pub struct DestinationMessage { + pub bytes: Vec, + pub referral_fee: U256, + pub recipient: RelayRecipient, +} + +#[derive(Clone, Debug)] +pub enum RelayRecipient { + Evm(Address), + Solana(SolanaPubkey), +} diff --git a/crates/swapper/src/across/provider.rs b/crates/swapper/src/across/provider.rs index 3ba164529..009809a51 100644 --- a/crates/swapper/src/across/provider.rs +++ b/crates/swapper/src/across/provider.rs @@ -3,6 +3,8 @@ use super::{ api::AcrossApi, config_store::{ConfigStoreClient, TokenConfig}, hubpool::HubPoolClient, + models::{DestinationMessage, QuoteContext, RelayRecipient}, + solana::{AcrossPlusMessage, CompiledIx, MULTICALL_HANDLER}, }; use crate::{ SwapResult, Swapper, SwapperError, SwapperProvider, SwapperQuoteData, @@ -11,17 +13,17 @@ use crate::{ approval::check_approval_erc20, asset::*, chainlink::ChainlinkPriceFeed, - client_factory::create_eth_client, - config::ReferralFee, + client_factory::{create_client_with_chain, create_eth_client}, eth_address, models::*, }; use alloy_primitives::{ - Address, Bytes, U256, hex::{decode as HexDecode, encode_prefixed as HexEncode}, + Address, Bytes, FixedBytes, U256, }; use alloy_sol_types::{SolCall, SolValue}; use async_trait::async_trait; +use bs58; use gem_evm::{ across::{ contracts::{ @@ -36,10 +38,28 @@ use gem_evm::{ multicall3::IMulticall3, weth::WETH9, }; +use gem_solana::{jsonrpc::SolanaRpc, models::prioritization_fee::SolanaPrioritizationFee}; use num_bigint::{BigInt, Sign}; -use primitives::{AssetId, Chain, EVMChain, swap::ApprovalData, swap::SwapStatus}; +use primitives::{swap::ApprovalData, swap::SwapStatus, AssetId, Chain, ChainType, EVMChain}; use serde_serializers::biguint_from_hex_str; -use std::{fmt::Debug, str::FromStr, sync::Arc}; +use solana_primitives::{ + instructions::{associated_token::get_associated_token_address, program_ids, token::transfer_checked}, + types::{find_program_address, Instruction as SolInstruction, Pubkey as SolanaPubkey}, +}; +use std::{collections::HashMap, fmt::Debug, str::FromStr, sync::Arc}; + +const DEFAULT_SOLANA_COMPUTE_LIMIT: u64 = 200_000; +const SOL_NATIVE_DECIMALS: u32 = 9; +const SOL_RELAYER_FEE_LAMPORTS: u64 = 5_000; + +struct PoolState { + token_config: TokenConfig, + utilization_before: BigInt, + utilization_after: BigInt, + timestamp: u32, + eth_price: Option, + sol_price: Option, +} #[derive(Debug)] pub struct Across { @@ -69,11 +89,30 @@ impl Across { } pub fn is_supported_pair(from_asset: &AssetId, to_asset: &AssetId) -> bool { - let Some(from) = eth_address::convert_native_to_weth(from_asset) else { + if from_asset.chain == Chain::Solana { return false; + } + + if to_asset.chain == Chain::Solana { + if to_asset != &SOLANA_USDC.id { + return false; + } + let from_normalized = match eth_address::convert_native_to_weth(from_asset) { + Some(asset) => asset, + None => return false, + }; + return AcrossDeployment::asset_mappings() + .into_iter() + .any(|mapping| mapping.set.contains(&from_normalized) && mapping.set.contains(&SOLANA_USDC.id)); + } + + let from = match eth_address::convert_native_to_weth(from_asset) { + Some(asset) => asset, + None => return false, }; - let Some(to) = eth_address::convert_native_to_weth(to_asset) else { - return false; + let to = match eth_address::convert_native_to_weth(to_asset) { + Some(asset) => asset, + None => return false, }; AcrossDeployment::asset_mappings() @@ -81,64 +120,435 @@ impl Across { .any(|x| x.set.contains(&from) && x.set.contains(&to)) } - pub fn get_rate_model(from_asset: &AssetId, to_asset: &AssetId, token_config: &TokenConfig) -> RateModel { - let key = format!("{}-{}", from_asset.chain.network_id(), to_asset.chain.network_id()); - let rate_model = token_config.route_rate_model.get(&key).unwrap_or(&token_config.rate_model); - rate_model.clone().into() + fn decode_address_bytes32(addr: &Address) -> FixedBytes<32> { + let mut bytes = [0u8; 32]; + bytes[12..32].copy_from_slice(addr.as_slice()); + FixedBytes::from(bytes) } - async fn gas_price(&self, chain: Chain) -> Result { - let gas_price = create_eth_client(self.rpc_provider.clone(), chain)?.gas_price().await?; - Self::bigint_to_u256(&gas_price) + fn decode_bs58_bytes32(addr: &str) -> Result, SwapperError> { + let decoded = bs58::decode(addr).into_vec().map_err(|_| SwapperError::InvalidAddress(addr.to_string()))?; + if decoded.len() != 32 { + return Err(SwapperError::InvalidAddress(addr.to_string())); + } + let bytes: [u8; 32] = decoded.try_into().map_err(|_| SwapperError::InvalidAddress(addr.to_string()))?; + Ok(FixedBytes::from(bytes)) } - async fn multicall3(&self, chain: Chain, calls: Vec) -> Result, SwapperError> { - create_eth_client(self.rpc_provider.clone(), chain)? - .multicall3(calls) - .await - .map_err(|e| SwapperError::NetworkError(e.to_string())) + fn recipient_to_fixed_bytes(recipient: &RelayRecipient) -> Result, SwapperError> { + match recipient { + RelayRecipient::Evm(address) => Ok(Self::decode_address_bytes32(address)), + RelayRecipient::Solana(pubkey) => Ok(FixedBytes::from(*pubkey.as_bytes())), + } } - async fn estimate_gas_transaction(&self, chain: Chain, tx: TransactionObject) -> Result { - let client = create_eth_client(self.rpc_provider.clone(), chain)?; - let gas_hex = client - .estimate_gas(tx.from.as_deref(), &tx.to, tx.value.as_deref(), Some(tx.data.as_str())) - .await - .map_err(SwapperError::from)?; + fn recipient_evm_address(recipient: &RelayRecipient) -> Option<&Address> { + match recipient { + RelayRecipient::Evm(address) => Some(address), + RelayRecipient::Solana(_) => None, + } + } - let gas_biguint = biguint_from_hex_str(&gas_hex).map_err(|e| SwapperError::NetworkError(format!("Failed to parse gas estimate: {e}")))?; - let gas_bigint = BigInt::from_biguint(Sign::Plus, gas_biguint); - Self::bigint_to_u256(&gas_bigint) + fn token_bytes32_for_asset(asset: &AssetId) -> Result, SwapperError> { + match asset.chain.chain_type() { + ChainType::Solana => { + let id = asset + .token_id + .as_deref() + .ok_or_else(|| SwapperError::InvalidAddress("missing token_id for Solana".into()))?; + Self::decode_bs58_bytes32(id) + } + ChainType::Ethereum => { + let evm_chain = EVMChain::from_chain(asset.chain).ok_or(SwapperError::NotSupportedChain)?; + let default_weth = evm_chain.weth_contract().ok_or(SwapperError::NotSupportedChain)?; + let id = if asset.is_native() { default_weth } else { asset.token_id.as_deref().unwrap() }; + Ok(Self::decode_address_bytes32(ð_address::parse_str(id)?)) + } + _ => Err(SwapperError::NotImplemented), + } + } + + fn is_solana_destination(request: &QuoteRequest) -> bool { + request.to_asset.chain() == Chain::Solana } - /// Return (message, referral_fee) - pub fn message_for_multicall_handler( + fn get_output_asset(request: &QuoteRequest) -> Result { + if Self::is_solana_destination(request) { + Ok(request.to_asset.asset_id()) + } else { + eth_address::convert_native_to_weth(&request.to_asset.asset_id()).ok_or(SwapperError::NotSupportedPair) + } + } + + fn get_destination_chain_id(chain: &Chain) -> Result { + let deployment = AcrossDeployment::deployment_by_chain(chain).ok_or(SwapperError::NotSupportedChain)?; + Ok(deployment.chain_id) + } + + fn build_context<'a>(&self, request: &'a QuoteRequest) -> Result, SwapperError> { + if request.from_asset.chain() == request.to_asset.chain() { + return Err(SwapperError::NotSupportedPair); + } + + if request.from_asset.chain() == Chain::Solana { + return Err(SwapperError::NotSupportedPair); + } + + let from_amount: U256 = request.value.parse().map_err(SwapperError::from)?; + let wallet_address = eth_address::parse_str(&request.wallet_address)?; + let from_chain = request.from_asset.chain(); + let to_chain = request.to_asset.chain(); + let from_chain_evm = EVMChain::from_chain(from_chain).ok_or(SwapperError::NotSupportedChain)?; + + let _origin_deployment = AcrossDeployment::deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; + let destination_deployment = AcrossDeployment::deployment_by_chain(&to_chain).ok_or(SwapperError::NotSupportedChain)?; + + if !Self::is_supported_pair(&request.from_asset.asset_id(), &request.to_asset.asset_id()) { + return Err(SwapperError::NotSupportedPair); + } + + let input_asset = eth_address::convert_native_to_weth(&request.from_asset.asset_id()).ok_or(SwapperError::NotSupportedPair)?; + let output_asset = Self::get_output_asset(request)?; + let original_output_asset = request.to_asset.asset_id(); + + let asset_mapping = AcrossDeployment::asset_mappings() + .into_iter() + .find(|mapping| mapping.set.contains(&input_asset)) + .ok_or(SwapperError::NotSupportedPair)?; + let mainnet_asset = asset_mapping + .set + .iter() + .find(|asset| asset.chain == Chain::Ethereum) + .cloned() + .ok_or(SwapperError::NotSupportedPair)?; + let mainnet_token = eth_address::parse_or_weth_address(&mainnet_asset, from_chain_evm)?; + + let referral_fees = request.options.fee.clone().unwrap_or_default(); + let referral_fee = if to_chain == Chain::Solana { + if referral_fees.solana.address.is_empty() { + referral_fees.evm_bridge + } else { + referral_fees.solana + } + } else { + referral_fees.evm_bridge + }; + + let output_token_decimals = + u8::try_from(asset_mapping.capital_cost.decimals).map_err(|_| SwapperError::ComputeQuoteError("Unsupported token decimals".into()))?; + + Ok(QuoteContext { + from_amount, + wallet_address, + from_chain, + to_chain, + input_is_native: request.from_asset.is_native(), + input_asset, + output_asset, + original_output_asset, + mainnet_token, + capital_cost: asset_mapping.capital_cost, + referral_fee, + destination_deployment, + destination_address: if to_chain == Chain::Solana { + Some(request.destination_address.as_str()) + } else { + None + }, + output_token_decimals, + }) + } + + async fn fetch_pool_state(&self, ctx: &QuoteContext<'_>) -> Result { + let hubpool_client = HubPoolClient::new(self.rpc_provider.clone(), Chain::Ethereum); + let config_client = ConfigStoreClient::new(self.rpc_provider.clone(), Chain::Ethereum); + + let preflight_calls = vec![ + hubpool_client.paused_call3(), + hubpool_client.sync_call3(&ctx.mainnet_token), + hubpool_client.pooled_token_call3(&ctx.mainnet_token), + ]; + let preflight_results = self.multicall3(hubpool_client.chain, preflight_calls).await?; + + if hubpool_client.decoded_paused_call3(&preflight_results[0])? { + return Err(SwapperError::ComputeQuoteError("Across protocol is paused".into())); + } + + let reserves = hubpool_client.decoded_pooled_token_call3(&preflight_results[2])?.liquidReserves; + if ctx.from_amount > reserves { + return Err(SwapperError::ComputeQuoteError("Bridge amount is too large".into())); + } + + let token_config_future = config_client.fetch_config(&ctx.mainnet_token); + + let mut call_requests = vec![ + hubpool_client.utilization_call3(&ctx.mainnet_token, U256::from(0)), + hubpool_client.utilization_call3(&ctx.mainnet_token, ctx.from_amount), + hubpool_client.get_current_time(), + ]; + + let mut index_tracker: HashMap<&'static str, usize> = HashMap::new(); + let mut next_index = 3usize; + + if !ctx.input_is_native && ctx.to_chain != Chain::Monad { + let feed = ChainlinkPriceFeed::new_usd_feed_for_chain(ctx.to_chain).unwrap_or_else(ChainlinkPriceFeed::new_eth_usd_feed); + call_requests.push(feed.latest_round_call3()); + index_tracker.insert("eth_price", next_index); + next_index += 1; + } + + if ctx.to_chain == Chain::Solana { + call_requests.push(ChainlinkPriceFeed::new_sol_usd_feed().latest_round_call3()); + index_tracker.insert("sol_price", next_index); + } + + let multicall_future = self.multicall3(hubpool_client.chain, call_requests); + let (token_config, multicall_results) = futures::join!(token_config_future, multicall_future); + + let token_config = token_config?; + let multicall_results = multicall_results?; + + let utilization_before = hubpool_client.decoded_utilization_call3(&multicall_results[0])?; + let utilization_after = hubpool_client.decoded_utilization_call3(&multicall_results[1])?; + let timestamp = hubpool_client.decoded_current_time(&multicall_results[2])?; + + let mut eth_price = None; + if !ctx.input_is_native { + if ctx.to_chain == Chain::Monad { + let feed = ChainlinkPriceFeed::new_usd_feed_for_chain(ctx.to_chain).unwrap_or_else(ChainlinkPriceFeed::new_eth_usd_feed); + let results = create_eth_client(self.rpc_provider.clone(), Chain::Monad)? + .multicall3(vec![feed.latest_round_call3()]) + .await + .map_err(|e| SwapperError::NetworkError(e.to_string()))?; + eth_price = Some(ChainlinkPriceFeed::decoded_answer(&results[0])?); + } else if let Some(index) = index_tracker.get("eth_price") { + eth_price = Some(ChainlinkPriceFeed::decoded_answer(&multicall_results[*index])?); + } + } + + let sol_price = index_tracker + .get("sol_price") + .map(|index| ChainlinkPriceFeed::decoded_answer(&multicall_results[*index])) + .transpose()?; + + Ok(PoolState { + token_config, + utilization_before, + utilization_after, + timestamp, + eth_price, + sol_price, + }) + } + + fn build_v3_relay_data( + &self, + ctx: &QuoteContext<'_>, + recipient: FixedBytes<32>, + output_token: FixedBytes<32>, + message: &[u8], + ) -> Result { + let chain_id = Self::get_destination_chain_id(&ctx.to_chain)?; + + Ok(V3RelayData { + depositor: Self::decode_address_bytes32(&ctx.wallet_address), + recipient, + exclusiveRelayer: FixedBytes::from([0u8; 32]), + inputToken: Self::token_bytes32_for_asset(&ctx.input_asset)?, + outputToken: output_token, + inputAmount: ctx.from_amount, + outputAmount: U256::from(100), + originChainId: U256::from(chain_id), + depositId: U256::from(u32::MAX), + fillDeadline: u32::MAX, + exclusivityDeadline: 0, + message: Bytes::from(message.to_vec()), + }) + } + + fn calculate_relayer_fee_for_destination( + request: &QuoteRequest, + from_amount: U256, + cost_config: &fees::CapitalCostConfig, + sol_price: Option<&BigInt>, + ) -> U256 { + if Self::is_solana_destination(request) { + if let Some(sol_usd_price) = sol_price { + let sol_fee_lamports = U256::from(SOL_RELAYER_FEE_LAMPORTS); + Self::calculate_fee_in_token_with_native_decimals(&sol_fee_lamports, sol_usd_price, cost_config.decimals, SOL_NATIVE_DECIMALS) + } else { + U256::ZERO + } + } else { + let relayer_calc = RelayerFeeCalculator::default(); + let from_amount_bigint = BigInt::from_bytes_le(Sign::Plus, &from_amount.to_le_bytes::<32>()); + let relayer_fee_percent = relayer_calc.capital_fee_percent(&from_amount_bigint, cost_config); + fees::multiply(from_amount, relayer_fee_percent, cost_config.decimals) + } + } + + pub fn get_rate_model(from_asset: &AssetId, to_asset: &AssetId, token_config: &TokenConfig) -> RateModel { + let key = format!("{}-{}", from_asset.chain.network_id(), to_asset.chain.network_id()); + let rate_model = token_config.route_rate_model.get(&key).unwrap_or(&token_config.rate_model); + rate_model.clone().into() + } + + fn build_destination_message(&self, ctx: &QuoteContext<'_>, amount: &U256, output_token_evm: Option<&Address>) -> Result { + match ctx.to_chain.chain_type() { + ChainType::Ethereum => self.build_evm_destination_message(ctx, amount, output_token_evm), + ChainType::Solana => self.build_solana_destination_message(ctx, amount), + _ => Err(SwapperError::NotSupportedPair), + } + } + + fn build_evm_destination_message( &self, + ctx: &QuoteContext<'_>, amount: &U256, - original_output_asset: &AssetId, - output_token: &Address, - user_address: &Address, - referral_fee: &ReferralFee, - ) -> (Vec, U256) { - if referral_fee.bps == 0 { - return (vec![], U256::from(0)); + output_token_evm: Option<&Address>, + ) -> Result { + let referral_fee = &ctx.referral_fee; + if referral_fee.bps == 0 || referral_fee.address.is_empty() { + return Ok(DestinationMessage { + bytes: vec![], + referral_fee: U256::from(0), + recipient: RelayRecipient::Evm(ctx.wallet_address), + }); } - let fee_address = Address::from_str(&referral_fee.address).unwrap(); + + let token = output_token_evm.ok_or(SwapperError::NotSupportedPair)?; + let fee_address = Address::from_str(&referral_fee.address).map_err(|_| SwapperError::InvalidAddress(referral_fee.address.clone()))?; let fee_amount = amount * U256::from(referral_fee.bps) / U256::from(10000); let user_amount = amount - fee_amount; - let calls = if original_output_asset.is_native() { - // output_token is WETH and we need to unwrap it - Self::unwrap_weth_calls(output_token, amount, user_address, &user_amount, &fee_address, &fee_amount) + let calls = if ctx.original_output_asset.is_native() { + Self::unwrap_weth_calls(token, amount, &ctx.wallet_address, &user_amount, &fee_address, &fee_amount) } else { - Self::erc20_transfer_calls(output_token, user_address, &user_amount, &fee_address, &fee_amount) + Self::erc20_transfer_calls(token, &ctx.wallet_address, &user_amount, &fee_address, &fee_amount) }; + let instructions = multicall_handler::Instructions { calls, - fallbackRecipient: *user_address, + fallbackRecipient: ctx.wallet_address, }; let message = instructions.abi_encode(); - (message, fee_amount) + let multicall_address = eth_address::parse_str(ctx.destination_deployment.multicall_handler().as_str())?; + + Ok(DestinationMessage { + bytes: message, + referral_fee: fee_amount, + recipient: RelayRecipient::Evm(multicall_address), + }) + } + + fn build_solana_destination_message(&self, ctx: &QuoteContext<'_>, amount: &U256) -> Result { + let destination_address = ctx + .destination_address + .ok_or_else(|| SwapperError::InvalidAddress("Missing Solana destination address".into()))?; + let user_account = SolanaPubkey::from_str(destination_address).map_err(|_| SwapperError::InvalidAddress(destination_address.into()))?; + + let referral_fee = &ctx.referral_fee; + if referral_fee.bps == 0 || referral_fee.address.is_empty() { + return Ok(DestinationMessage { + bytes: vec![], + referral_fee: U256::from(0), + recipient: RelayRecipient::Solana(user_account), + }); + } + + let referral_account = SolanaPubkey::from_str(&referral_fee.address).map_err(|_| SwapperError::InvalidAddress(referral_fee.address.clone()))?; + let handler_program = SolanaPubkey::from_str(MULTICALL_HANDLER).map_err(|_| SwapperError::InvalidAddress(MULTICALL_HANDLER.into()))?; + let (handler_signer, _) = find_program_address(&handler_program, &[b"handler_signer"]) + .map_err(|_| SwapperError::ComputeQuoteError("Failed to derive handler signer".into()))?; + + let mint_id = ctx + .original_output_asset + .token_id + .as_deref() + .ok_or_else(|| SwapperError::InvalidAddress("Missing Solana mint".into()))?; + let mint = SolanaPubkey::from_str(mint_id).map_err(|_| SwapperError::InvalidAddress(mint_id.into()))?; + + let token_program = + SolanaPubkey::from_str(program_ids::TOKEN_PROGRAM_ID).map_err(|_| SwapperError::InvalidAddress(program_ids::TOKEN_PROGRAM_ID.into()))?; + + let handler_token_account = get_associated_token_address(&handler_signer, &mint); + + let fee_amount = amount * U256::from(referral_fee.bps) / U256::from(10000); + let user_amount = amount - fee_amount; + + let fee_amount_u64: u64 = fee_amount.try_into().map_err(|_| SwapperError::InvalidAmount("Referral fee overflow".into()))?; + let user_amount_u64: u64 = user_amount.try_into().map_err(|_| SwapperError::InvalidAmount("User amount overflow".into()))?; + + let transfer_fee_ix = transfer_checked( + &handler_token_account, + &referral_account, + &mint, + &handler_signer, + fee_amount_u64, + ctx.output_token_decimals, + ); + let transfer_user_ix = transfer_checked( + &handler_token_account, + &user_account, + &mint, + &handler_signer, + user_amount_u64, + ctx.output_token_decimals, + ); + + let accounts = vec![handler_token_account, referral_account, user_account, handler_signer, mint, token_program]; + + let compiled_ixs = self.compile_solana_instructions(&[transfer_fee_ix, transfer_user_ix], &accounts)?; + let handler_message = borsh::to_vec(&compiled_ixs).map_err(|_| SwapperError::ComputeQuoteError("Failed to encode handler message".into()))?; + + let across_message = AcrossPlusMessage { + handler: handler_program, + read_only_len: 3, + value_amount: 0, + accounts, + handler_message, + }; + let message_bytes = borsh::to_vec(&across_message).map_err(|_| SwapperError::ComputeQuoteError("Failed to encode Across message".into()))?; + + Ok(DestinationMessage { + bytes: message_bytes, + referral_fee: fee_amount, + recipient: RelayRecipient::Solana(handler_signer), + }) + } + + fn compile_solana_instructions(&self, instructions: &[SolInstruction], accounts: &[SolanaPubkey]) -> Result, SwapperError> { + let mut account_index_map: HashMap = HashMap::new(); + for (idx, account) in accounts.iter().enumerate() { + account_index_map.insert(account.to_base58(), (idx + 1) as u8); + } + + let mut compiled = Vec::with_capacity(instructions.len()); + for instruction in instructions { + let program_key = instruction.program_id.to_base58(); + let program_index = account_index_map + .get(&program_key) + .copied() + .ok_or_else(|| SwapperError::ComputeQuoteError("Program account missing from message".into()))?; + + let mut account_key_indexes = Vec::with_capacity(instruction.accounts.len()); + for account in &instruction.accounts { + let key = account.pubkey.to_base58(); + let index = account_index_map + .get(&key) + .copied() + .ok_or_else(|| SwapperError::ComputeQuoteError("Account missing from message".into()))?; + account_key_indexes.push(index); + } + + compiled.push(CompiledIx { + program_id_index: program_index, + account_key_indexes, + data: instruction.data.clone(), + }); + } + + Ok(compiled) } fn unwrap_weth_calls( @@ -200,50 +610,63 @@ impl Across { ] } - pub async fn estimate_gas_limit( + async fn gas_price(&self, chain: Chain) -> Result { + let gas_price = create_eth_client(self.rpc_provider.clone(), chain)?.gas_price().await?; + Self::bigint_to_u256(&gas_price) + } + + async fn multicall3(&self, chain: Chain, calls: Vec) -> Result, SwapperError> { + create_eth_client(self.rpc_provider.clone(), chain)? + .multicall3(calls) + .await + .map_err(|e| SwapperError::NetworkError(e.to_string())) + } + + async fn estimate_gas_transaction(&self, chain: Chain, tx: TransactionObject) -> Result { + let client = create_eth_client(self.rpc_provider.clone(), chain)?; + let gas_hex = client + .estimate_gas(tx.from.as_deref(), &tx.to, tx.value.as_deref(), Some(tx.data.as_str())) + .await + .map_err(SwapperError::from)?; + + let gas_biguint = biguint_from_hex_str(&gas_hex).map_err(|e| SwapperError::NetworkError(format!("Failed to parse gas estimate: {e}")))?; + let gas_bigint = BigInt::from_biguint(Sign::Plus, gas_biguint); + Self::bigint_to_u256(&gas_bigint) + } + + async fn estimate_gas_limit( &self, - amount: &U256, - is_native: bool, - input_asset: &AssetId, - output_token: &Address, - wallet_address: &Address, - message: &[u8], - deployment: &AcrossDeployment, - chain: Chain, + ctx: &QuoteContext<'_>, + destination_message: &DestinationMessage, + output_token: FixedBytes<32>, ) -> Result<(U256, V3RelayData), SwapperError> { - let chain_id: u32 = chain.network_id().parse().unwrap(); + let chain = ctx.to_chain; + if chain.chain_type() != ChainType::Ethereum { + return Err(SwapperError::NotImplemented); + } - let recipient = if message.is_empty() { - *wallet_address - } else { - Address::from_str(deployment.multicall_handler().as_str()).unwrap() - }; + let recipient_address = Self::recipient_evm_address(&destination_message.recipient).ok_or(SwapperError::NotImplemented)?; + let recipient = Self::decode_address_bytes32(recipient_address); + let v3_relay_data = self.build_v3_relay_data(ctx, recipient, output_token, &destination_message.bytes)?; - let v3_relay_data = V3RelayData { - depositor: *wallet_address, - recipient, - exclusiveRelayer: Address::ZERO, - inputToken: Address::from_str(input_asset.token_id.clone().unwrap().as_ref()).unwrap(), - outputToken: *output_token, - inputAmount: *amount, - outputAmount: U256::from(100), // safe amount - originChainId: U256::from(chain_id), - depositId: u32::MAX, - fillDeadline: u32::MAX, - exclusivityDeadline: 0, - message: Bytes::from(message.to_vec()), + let value = if ctx.input_is_native { + format!("{:#x}", ctx.from_amount) + } else { + String::from("0x0") }; - let value = if is_native { format!("{amount:#x}") } else { String::from("0x0") }; - let data = V3SpokePoolInterface::fillV3RelayCall { + let chain_id = Self::get_destination_chain_id(&chain)?; + let data = V3SpokePoolInterface::fillRelayCall { relayData: v3_relay_data.clone(), repaymentChainId: U256::from(chain_id), + repaymentAddress: Self::decode_address_bytes32(&ctx.wallet_address), } .abi_encode(); - let tx = TransactionObject::new_call_to_value(deployment.spoke_pool, &value, data); + + let tx = TransactionObject::new_call_to_value(ctx.destination_deployment.spoke_pool, &value, data); let gas_limit = self .estimate_gas_transaction(chain, tx) .await - .unwrap_or(U256::from(Self::get_default_fill_limit(chain))); + .unwrap_or_else(|_| U256::from(Self::get_default_fill_limit(chain))); Ok((gas_limit, v3_relay_data)) } @@ -254,44 +677,78 @@ impl Across { } } - async fn usd_price_for_chain(&self, chain: Chain, existing_results: &[IMulticall3::Result]) -> Result { - let feed = ChainlinkPriceFeed::new_usd_feed_for_chain(chain).ok_or(SwapperError::NotSupportedChain)?; - if chain == Chain::Monad { - let results = create_eth_client(self.rpc_provider.clone(), Chain::Monad)? - .multicall3(vec![feed.latest_round_call3()]) - .await - .map_err(|e| SwapperError::NetworkError(e.to_string()))?; - ChainlinkPriceFeed::decoded_answer(&results[0]) - } else { - ChainlinkPriceFeed::decoded_answer(&existing_results[3]) - } - } - - pub fn update_v3_relay_data( - &self, - v3_relay_data: &mut V3RelayData, - user_address: &Address, - output_amount: &U256, - original_output_asset: &AssetId, - output_token: &Address, - timestamp: u32, - referral_fee: &ReferralFee, - ) -> Result<(), SwapperError> { - let (message, _) = self.message_for_multicall_handler(output_amount, original_output_asset, output_token, user_address, referral_fee); - + fn update_v3_relay_data(&self, v3_relay_data: &mut V3RelayData, output_amount: &U256, timestamp: u32, destination_message: DestinationMessage) -> U256 { v3_relay_data.outputAmount = *output_amount; v3_relay_data.fillDeadline = timestamp + DEFAULT_FILL_TIMEOUT; - v3_relay_data.message = message.into(); + v3_relay_data.message = destination_message.bytes.into(); - Ok(()) + destination_message.referral_fee } pub fn calculate_fee_in_token(fee_in_wei: &U256, token_price: &BigInt, token_decimals: u32) -> U256 { - let fee = BigInt::from_bytes_le(Sign::Plus, &fee_in_wei.to_le_bytes::<32>()); - let fee_in_token = fee * token_price * BigInt::from(10_u64.pow(token_decimals)) / BigInt::from(10_u64.pow(8)) / BigInt::from(10_u64.pow(18)); + Self::calculate_fee_in_token_with_native_decimals(fee_in_wei, token_price, token_decimals, 18) + } + + fn calculate_fee_in_token_with_native_decimals( + fee_in_native: &U256, + token_price: &BigInt, + token_decimals: u32, + native_decimals: u32, + ) -> U256 { + let fee = BigInt::from_bytes_le(Sign::Plus, &fee_in_native.to_le_bytes::<32>()); + let fee_in_token = fee * token_price * BigInt::from(10_u64.pow(token_decimals)) + / BigInt::from(10_u64.pow(8)) + / BigInt::from(10_u64.pow(native_decimals)); U256::from_le_slice(&fee_in_token.to_bytes_le().1) } + async fn fetch_solana_unit_price(provider: Arc) -> Result { + let client = create_client_with_chain(provider, Chain::Solana); + let rpc_call = SolanaRpc::GetRecentPrioritizationFees; + let fees: Vec = client.request(rpc_call).await?; + + if fees.is_empty() { + return Err(SwapperError::NetworkError("Failed to fetch recent prioritization fees".to_string())); + } + + let total_fee: u64 = fees.iter().map(|f| f.prioritization_fee as u64).sum(); + let average_fee = total_fee / fees.len() as u64; + + Ok(std::cmp::max(1, average_fee)) + } + + async fn calculate_gas_price_and_fee( + &self, + ctx: &QuoteContext<'_>, + destination_message: &DestinationMessage, + output_token: FixedBytes<32>, + eth_price: Option<&BigInt>, + ) -> Result<(U256, V3RelayData), SwapperError> { + if ctx.to_chain == Chain::Solana { + let unit_price = Self::fetch_solana_unit_price(self.rpc_provider.clone()).await?; + let gas_fee = DEFAULT_SOLANA_COMPUTE_LIMIT * unit_price; + + let recipient = Self::recipient_to_fixed_bytes(&destination_message.recipient)?; + let v3_relay_data = self.build_v3_relay_data(ctx, recipient, output_token, &destination_message.bytes)?; + + Ok((U256::from(gas_fee), v3_relay_data)) + } else { + let gas_chain = ctx.to_chain; + let gas_price_req = self.gas_price(gas_chain); + let gas_limit_req = self.estimate_gas_limit(ctx, destination_message, output_token); + + let (tuple, gas_price) = futures::join!(gas_limit_req, gas_price_req); + let (gas_limit, v3_relay_data) = tuple?; + let mut gas_fee = gas_limit * gas_price?; + + if let Some(price) = eth_price { + gas_fee = Self::calculate_fee_in_token(&gas_fee, price, 6); + } + + Ok((gas_fee, v3_relay_data)) + } + } + pub fn get_eta_in_seconds(&self, from_chain: &Chain, to_chain: &Chain) -> Option { let from_chain = EVMChain::from_chain(*from_chain)?; let to_chain = EVMChain::from_chain(*to_chain)?; @@ -337,136 +794,55 @@ impl Swapper for Across { SwapperChainAsset::Assets(Chain::SmartChain, vec![SMARTCHAIN_ETH.id.clone()]), SwapperChainAsset::Assets(Chain::Hyperliquid, vec![HYPEREVM_USDC.id.clone(), HYPEREVM_USDT.id.clone()]), SwapperChainAsset::Assets(Chain::Plasma, vec![PLASMA_USDT.id.clone()]), + SwapperChainAsset::Assets(Chain::Solana, vec![SOLANA_USDC.id.clone()]), ] } async fn fetch_quote(&self, request: &QuoteRequest) -> Result { - // does not support same chain swap - if request.from_asset.chain() == request.to_asset.chain() { - return Err(SwapperError::NotSupportedPair); - } - - let input_is_native = request.from_asset.is_native(); - let from_chain = EVMChain::from_chain(request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; - let from_amount: U256 = request.value.parse().map_err(SwapperError::from)?; - let wallet_address = eth_address::parse_str(&request.wallet_address)?; + let ctx = self.build_context(request)?; + let pool_state = self.fetch_pool_state(&ctx).await?; - let _ = AcrossDeployment::deployment_by_chain(&request.from_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; - let destination_deployment = AcrossDeployment::deployment_by_chain(&request.to_asset.chain()).ok_or(SwapperError::NotSupportedChain)?; - if !Self::is_supported_pair(&request.from_asset.asset_id(), &request.to_asset.asset_id()) { - return Err(SwapperError::NotSupportedPair); - } - - let input_asset = eth_address::convert_native_to_weth(&request.from_asset.asset_id()).ok_or(SwapperError::NotSupportedPair)?; - let output_asset = eth_address::convert_native_to_weth(&request.to_asset.asset_id()).ok_or(SwapperError::NotSupportedPair)?; - let original_output_asset = request.to_asset.asset_id(); - let output_token = eth_address::parse_asset_id(&output_asset)?; - - // Get L1 token address - let mappings = AcrossDeployment::asset_mappings(); - let asset_mapping = mappings.iter().find(|x| x.set.contains(&input_asset)).unwrap(); - let asset_mainnet = asset_mapping.set.iter().find(|x| x.chain == Chain::Ethereum).unwrap(); - let mainnet_token = eth_address::parse_or_weth_address(asset_mainnet, from_chain)?; - - let hubpool_client = HubPoolClient::new(self.rpc_provider.clone(), Chain::Ethereum); - let config_client = ConfigStoreClient::new(self.rpc_provider.clone(), Chain::Ethereum); - - let calls = vec![ - hubpool_client.paused_call3(), - hubpool_client.sync_call3(&mainnet_token), - hubpool_client.pooled_token_call3(&mainnet_token), - ]; - let results = self.multicall3(hubpool_client.chain, calls).await?; + let rate_model = Self::get_rate_model(&ctx.input_asset, &ctx.output_asset, &pool_state.token_config); + let lpfee_calc = LpFeeCalculator::new(rate_model); + let lpfee_percent = lpfee_calc.realized_lp_fee_pct(&pool_state.utilization_before, &pool_state.utilization_after, false); + let lpfee = fees::multiply(ctx.from_amount, lpfee_percent, ctx.capital_cost.decimals); + let relayer_fee = Self::calculate_relayer_fee_for_destination(request, ctx.from_amount, &ctx.capital_cost, pool_state.sol_price.as_ref()); - // Check if protocol is paused - let is_paused = hubpool_client.decoded_paused_call3(&results[0])?; - if is_paused { - return Err(SwapperError::ComputeQuoteError("Across protocol is paused".into())); + if lpfee + relayer_fee >= ctx.from_amount { + return Err(SwapperError::InputAmountTooSmall); } + let remain_amount = ctx.from_amount - lpfee - relayer_fee; - // Check bridge amount is too large (Across API has some limit in USD amount but we don't have that info) - if from_amount > hubpool_client.decoded_pooled_token_call3(&results[2])?.liquidReserves { - return Err(SwapperError::ComputeQuoteError("Bridge amount is too large".into())); - } + let output_token_evm = if ctx.to_chain.chain_type() == ChainType::Ethereum { + Some(eth_address::parse_asset_id(&ctx.output_asset)?) + } else { + None + }; - // Prepare data for lp fee calculation (token config, utilization, current time) - let token_config_req = config_client.fetch_config(&mainnet_token); // cache is used inside config_client - let mut calls = vec![ - hubpool_client.utilization_call3(&mainnet_token, U256::from(0)), - hubpool_client.utilization_call3(&mainnet_token, from_amount), - hubpool_client.get_current_time(), - ]; + let initial_destination_message = self.build_destination_message(&ctx, &remain_amount, output_token_evm.as_ref())?; + let output_token_bytes = Self::token_bytes32_for_asset(&ctx.output_asset)?; + let (gas_fee, mut v3_relay_data) = self + .calculate_gas_price_and_fee(&ctx, &initial_destination_message, output_token_bytes, pool_state.eth_price.as_ref()) + .await?; - let gas_price_feed = ChainlinkPriceFeed::new_usd_feed_for_chain(request.to_asset.chain()).unwrap_or_else(ChainlinkPriceFeed::new_eth_usd_feed); - if !input_is_native { - calls.push(gas_price_feed.latest_round_call3()); + if remain_amount <= gas_fee { + return Err(SwapperError::InputAmountTooSmall); } + let output_amount = remain_amount - gas_fee; - let multicall_results = self.multicall3(hubpool_client.chain, calls).await?; - let token_config = token_config_req.await?; - - let util_before = hubpool_client.decoded_utilization_call3(&multicall_results[0])?; - let util_after = hubpool_client.decoded_utilization_call3(&multicall_results[1])?; - let timestamp = hubpool_client.decoded_current_time(&multicall_results[2])?; - - let rate_model = Self::get_rate_model(&input_asset, &output_asset, &token_config); - let cost_config = &asset_mapping.capital_cost; - - // Calculate lp fee - let lpfee_calc = LpFeeCalculator::new(rate_model); - let lpfee_percent = lpfee_calc.realized_lp_fee_pct(&util_before, &util_after, false); - let lpfee = fees::multiply(from_amount, lpfee_percent, cost_config.decimals); - - // Calculate relayer fee - let relayer_calc = RelayerFeeCalculator::default(); - let relayer_fee_percent = relayer_calc.capital_fee_percent(&BigInt::from_str(&request.value).unwrap(), cost_config); - let relayer_fee = fees::multiply(from_amount, relayer_fee_percent, cost_config.decimals); - - let referral_config = request.options.fee.clone().unwrap_or_default().evm_bridge; - - // Calculate gas limit / price for relayer - let remain_amount = from_amount - lpfee - relayer_fee; - let (message, referral_fee) = - self.message_for_multicall_handler(&remain_amount, &original_output_asset, &wallet_address, &output_token, &referral_config); - - let gas_price = self.gas_price(request.to_asset.chain()).await?; - let (gas_limit, mut v3_relay_data) = self - .estimate_gas_limit( - &from_amount, - input_is_native, - &input_asset, - &output_token, - &wallet_address, - &message, - &destination_deployment, - request.to_asset.chain(), - ) - .await?; - let mut gas_fee = gas_limit * gas_price; - if !input_is_native { - let price = self.usd_price_for_chain(request.to_asset.chain(), &multicall_results).await?; - gas_fee = Self::calculate_fee_in_token(&gas_fee, &price, 6); + let final_destination_message = self.build_destination_message(&ctx, &output_amount, output_token_evm.as_ref())?; + let recipient_bytes = Self::recipient_to_fixed_bytes(&final_destination_message.recipient)?; + if v3_relay_data.recipient != recipient_bytes { + v3_relay_data.recipient = recipient_bytes; } - - // Check if bridge amount is too small - if remain_amount < gas_fee { + let final_referral_fee = self.update_v3_relay_data(&mut v3_relay_data, &output_amount, pool_state.timestamp, final_destination_message); + if final_referral_fee > output_amount { return Err(SwapperError::InputAmountTooSmall); } + let to_value = output_amount - final_referral_fee; - let output_amount = remain_amount - gas_fee; - let to_value = output_amount - referral_fee; - - // Update v3 relay data (was used to estimate gas limit) with final output amount, quote timestamp and referral fee. - self.update_v3_relay_data( - &mut v3_relay_data, - &wallet_address, - &output_amount, - &original_output_asset, - &output_token, - timestamp, - &referral_config, - )?; - let route_data = HexEncode(v3_relay_data.abi_encode()); + let encoded_data = v3_relay_data.abi_encode(); + let route_data = HexEncode(encoded_data); Ok(Quote { from_value: request.value.clone(), @@ -475,34 +851,43 @@ impl Swapper for Across { provider: self.provider().clone(), slippage_bps: request.options.slippage.bps, routes: vec![Route { - input: input_asset.clone(), - output: output_asset.clone(), + input: ctx.input_asset.clone(), + output: ctx.output_asset.clone(), route_data, gas_limit: Some(DEFAULT_DEPOSIT_GAS_LIMIT.to_string()), }], }, request: request.clone(), - eta_in_seconds: self.get_eta_in_seconds(&request.from_asset.chain(), &request.to_asset.chain()), + eta_in_seconds: self.get_eta_in_seconds(&ctx.from_chain, &ctx.to_chain), }) } async fn fetch_quote_data(&self, quote: &Quote, data: FetchQuoteData) -> Result { let from_chain = quote.request.from_asset.chain(); let deployment = AcrossDeployment::deployment_by_chain(&from_chain).ok_or(SwapperError::NotSupportedChain)?; - let dst_chain_id: u32 = quote.request.to_asset.chain().network_id().parse().unwrap(); + let dst_chain_id = Self::get_destination_chain_id("e.request.to_asset.chain())?; let route = "e.data.routes[0]; let route_data = HexDecode(&route.route_data).map_err(|_| SwapperError::InvalidRoute)?; let v3_relay_data = V3RelayData::abi_decode(&route_data).map_err(|_| SwapperError::InvalidRoute)?; - let deposit_v3_call = V3SpokePoolInterface::depositV3Call { - depositor: v3_relay_data.depositor, - recipient: v3_relay_data.recipient, - inputToken: v3_relay_data.inputToken, - outputToken: v3_relay_data.outputToken, + let depositor = Self::decode_address_bytes32(ð_address::parse_str("e.request.wallet_address)?); + let recipient = v3_relay_data.recipient; + + let input_asset_id = quote.request.from_asset.asset_id(); + let input_token = Self::token_bytes32_for_asset(&input_asset_id)?; + + let to_asset_id = quote.request.to_asset.asset_id(); + let output_token = Self::token_bytes32_for_asset(&to_asset_id)?; + + let deposit_call = V3SpokePoolInterface::depositCall { + depositor, + recipient, + inputToken: input_token, + outputToken: output_token, inputAmount: v3_relay_data.inputAmount, outputAmount: v3_relay_data.outputAmount, destinationChainId: U256::from(dst_chain_id), - exclusiveRelayer: Address::ZERO, + exclusiveRelayer: FixedBytes::from([0u8; 32]), quoteTimestamp: v3_relay_data.fillDeadline - DEFAULT_FILL_TIMEOUT, fillDeadline: v3_relay_data.fillDeadline, exclusivityDeadline: 0, @@ -519,7 +904,7 @@ impl Swapper for Across { } else { check_approval_erc20( quote.request.wallet_address.clone(), - v3_relay_data.inputToken.to_string(), + eth_address::parse_asset_id("e.request.from_asset.asset_id())?.to_string(), deployment.spoke_pool.into(), v3_relay_data.inputAmount, self.rpc_provider.clone(), @@ -535,7 +920,7 @@ impl Swapper for Across { if matches!(data, FetchQuoteData::EstimateGas) { let hex_value = format!("{:#x}", U256::from_str(value).unwrap()); - let tx = TransactionObject::new_call_to_value(&to, &hex_value, deposit_v3_call.clone()); + let tx = TransactionObject::new_call_to_value(&to, &hex_value, deposit_call.clone()); let _gas_limit = self.estimate_gas_transaction(from_chain, tx).await?; gas_limit = Some(_gas_limit.to_string()); } @@ -543,11 +928,12 @@ impl Swapper for Across { Ok(SwapperQuoteData::new_contract( deployment.spoke_pool.into(), value.to_string(), - HexEncode(deposit_v3_call.clone()), + HexEncode(deposit_call.clone()), approval, gas_limit, )) } + async fn get_swap_result(&self, chain: Chain, transaction_hash: &str) -> Result { let api = AcrossApi::new(self.rpc_provider.clone()); let status = api.deposit_status(chain, transaction_hash).await?; @@ -555,7 +941,6 @@ impl Swapper for Across { let swap_status = status.swap_status(); let destination_chain = Chain::from_chain_id(status.destination_chain_id); - // Determine the transaction hash to show based on status let (to_chain, to_tx_hash) = match swap_status { SwapStatus::Completed => (destination_chain, status.fill_tx.clone()), SwapStatus::Failed | SwapStatus::Refunded => (Some(chain), None), @@ -575,14 +960,88 @@ impl Swapper for Across { #[cfg(test)] mod tests { use super::*; - use gem_evm::multicall3::IMulticall3; + use crate::{SwapperMode, SwapperQuoteAsset}; + use crate::alien::mock::{MockFn, ProviderMock}; + use crate::config::ReferralFee; + use gem_evm::{ + across::contracts::{multicall_handler, spoke_pool::V3SpokePoolInterface::depositCall}, + multicall3::IMulticall3, + weth::WETH9, + }; use primitives::asset_constants::*; + use std::time::Duration; + + fn make_quote_asset(asset_id: &AssetId, decimals: u32) -> SwapperQuoteAsset { + SwapperQuoteAsset { + id: asset_id.to_string(), + symbol: String::new(), + decimals, + } + } + + fn make_request(from_asset: AssetId, to_asset: AssetId, wallet: &str, destination: &str, value: &str) -> QuoteRequest { + QuoteRequest { + from_asset: make_quote_asset(&from_asset, 18), + to_asset: make_quote_asset(&to_asset, 18), + wallet_address: wallet.into(), + destination_address: destination.into(), + value: value.into(), + mode: SwapperMode::ExactIn, + options: Options::default(), + } + } + + #[allow(clippy::too_many_arguments)] + fn make_quote_context<'a>( + _request: &'a QuoteRequest, + from_amount: U256, + wallet_address: &str, + from_chain: Chain, + to_chain: Chain, + input_asset: AssetId, + output_asset: AssetId, + original_output_asset: AssetId, + referral_fee: ReferralFee, + destination_address: Option<&'a str>, + input_is_native: bool, + output_token_decimals: u8, + ) -> QuoteContext<'a> { + QuoteContext { + from_amount, + wallet_address: Address::from_str(wallet_address).unwrap(), + from_chain, + to_chain, + input_is_native, + input_asset, + output_asset, + original_output_asset, + mainnet_token: Address::from_str("0x0000000000000000000000000000000000000001").unwrap(), + capital_cost: fees::CapitalCostConfig { + lower_bound: BigInt::from(0), + upper_bound: BigInt::from(0), + cutoff: BigInt::from(1), + decimals: output_token_decimals as u32, + }, + referral_fee, + destination_deployment: AcrossDeployment::deployment_by_chain(&to_chain).unwrap(), + destination_address, + output_token_decimals, + } + } + + fn mock_provider(response: &str) -> Arc { + let response = response.to_string(); + Arc::new(ProviderMock { + response: MockFn(Box::new(move |_| response.clone())), + timeout: Duration::from_millis(50), + }) + } #[test] fn test_is_supported_pair() { - let weth_eth = AssetId::from_token(Chain::Ethereum, WETH_ETH_CONTRACT); - let weth_op = AssetId::from_token(Chain::Optimism, WETH_OP_CONTRACT); - let weth_arb = AssetId::from_token(Chain::Arbitrum, WETH_ARB_CONTRACT); + let weth_eth: AssetId = AssetId::from_token(Chain::Ethereum, WETH_ETH_CONTRACT); + let weth_op: AssetId = AssetId::from_token(Chain::Optimism, WETH_OP_CONTRACT); + let weth_arb: AssetId = AssetId::from_token(Chain::Arbitrum, WETH_ARB_CONTRACT); let weth_bsc: AssetId = ETH_SMARTCHAIN_ASSET_ID.into(); let usdc_eth: AssetId = USDC_ETH_ASSET_ID.into(); @@ -600,7 +1059,6 @@ mod tests { assert!(!Across::is_supported_pair(&weth_eth, &usdc_eth)); - // native asset let eth = AssetId::from(Chain::Ethereum, None); let op = AssetId::from(Chain::Optimism, None); let arb = AssetId::from(Chain::Arbitrum, None); @@ -610,6 +1068,59 @@ mod tests { assert!(Across::is_supported_pair(&op, ð)); assert!(Across::is_supported_pair(&arb, ð)); assert!(Across::is_supported_pair(&op, &arb)); + + let solana_usdc = SOLANA_USDC.id.clone(); + + assert!(Across::is_supported_pair(&usdc_eth, &solana_usdc)); + assert!(Across::is_supported_pair(&usdc_arb, &solana_usdc)); + + let solana_usdt = AssetId::from_token(Chain::Solana, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); + assert!(!Across::is_supported_pair(&usdc_eth, &solana_usdt)); + + assert!(!Across::is_supported_pair(&solana_usdc, &usdc_eth)); + assert!(!Across::is_supported_pair(&solana_usdc, &usdc_arb)); + assert!(!Across::is_supported_pair(&weth_eth, &solana_usdc)); + } + + #[test] + fn test_solana_address_to_bytes32() { + let bytes = Across::decode_bs58_bytes32("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); + let expected = "0xc6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61"; + + assert_eq!(HexEncode(bytes), expected); + + let bytes = Across::decode_bs58_bytes32("G7B17AigRCGvwnxFc5U8zY5T3NBGduLzT7KYApNU2VdR").unwrap(); + let expected = "0xe074190d46821cf0b318d4503f63178e25d76cc7d9d2498d54781fb95bb68868"; + + assert_eq!(HexEncode(bytes), expected); + } + + #[test] + fn test_v3_relay_data_solana_encoding() { + let depositor_addr = Address::from_str("0x514bcb1f9aabb904e6106bd1052b66d2706dbbb7").unwrap(); + let input_token_addr = Address::from_str("0xaf88d065e77c8cc2239327c5edb3a432268e5831").unwrap(); + let depositor = Across::decode_address_bytes32(&depositor_addr); + let recipient = Across::decode_bs58_bytes32("G7B17AigRCGvwnxFc5U8zY5T3NBGduLzT7KYApNU2VdR").unwrap(); + let input_token = Across::decode_address_bytes32(&input_token_addr); + let output_token = Across::decode_bs58_bytes32("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap(); + let call = depositCall { + depositor, + recipient, + inputToken: input_token, + outputToken: output_token, + inputAmount: U256::from(7000000_u64), + outputAmount: U256::from(6997408_u64), + destinationChainId: U256::from(34268394551451_u64), + exclusiveRelayer: FixedBytes::from([0u8; 32]), + quoteTimestamp: 1756299179, + fillDeadline: 1756311051, + exclusivityDeadline: 0, + message: Bytes::new(), + }; + let encoded_call = call.abi_encode(); + let call_data = "0xad5425c6000000000000000000000000514bcb1f9aabb904e6106bd1052b66d2706dbbb7e074190d46821cf0b318d4503f63178e25d76cc7d9d2498d54781fb95bb68868000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d6100000000000000000000000000000000000000000000000000000000006acfc000000000000000000000000000000000000000000000000000000000006ac5a000000000000000000000000000000000000000000000000000001f2abb7bf89b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068aeffab0000000000000000000000000000000000000000000000000000000068af2e0b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000"; + + assert_eq!(HexEncode(encoded_call), call_data); } #[test] @@ -629,6 +1140,237 @@ mod tests { assert_eq!(fee_in_token.to_string(), "6243790"); } + #[test] + fn test_build_destination_message_eth_to_base() { + let across = Across::new(mock_provider("{}")); + let amount = U256::from_str("1000000000000000000").unwrap(); + let request = make_request( + AssetId::from_chain(Chain::Ethereum), + AssetId::from_chain(Chain::Base), + "0x1111111111111111111111111111111111111111", + "11111111111111111111111111111111", + amount.to_string().as_str(), + ); + let referral_fee = ReferralFee { + address: "0x2222222222222222222222222222222222222222".into(), + bps: 100, + }; + let ctx = make_quote_context( + &request, + amount, + &request.wallet_address, + Chain::Ethereum, + Chain::Base, + AssetId::from_chain(Chain::Ethereum), + AssetId::from_token(Chain::Base, "0x4200000000000000000000000000000000000006"), + AssetId::from_chain(Chain::Base), + referral_fee, + None, + true, + 18, + ); + + let output_token = Address::from_str("0x4200000000000000000000000000000000000006").unwrap(); + let destination_message = across.build_destination_message(&ctx, &amount, Some(&output_token)).unwrap(); + + let expected_fee = amount * U256::from(100u64) / U256::from(10000u64); + assert_eq!(destination_message.referral_fee, expected_fee); + + let instructions = multicall_handler::Instructions::abi_decode(&destination_message.bytes).unwrap(); + assert_eq!(instructions.fallbackRecipient, ctx.wallet_address); + assert_eq!(instructions.calls.len(), 3); + + let expected_withdraw = WETH9::withdrawCall { wad: amount }.abi_encode(); + assert_eq!(instructions.calls[0].target, output_token); + assert_eq!(instructions.calls[0].callData, Bytes::from(expected_withdraw)); + assert_eq!(instructions.calls[1].value + instructions.calls[2].value, amount); + } + + #[test] + fn test_build_destination_message_usdc_to_optimism() { + let across = Across::new(mock_provider("{}")); + let amount = U256::from(1_000_000u64); + let request = make_request( + AssetId::from_token(Chain::Arbitrum, "0xaf88d065e77c8cc2239327c5edb3a432268e5831"), + USDC_OP_ASSET_ID.into(), + "0x1111111111111111111111111111111111111111", + "11111111111111111111111111111111", + amount.to_string().as_str(), + ); + let referral_fee = ReferralFee { + address: "0x2222222222222222222222222222222222222222".into(), + bps: 100, + }; + let ctx = make_quote_context( + &request, + amount, + &request.wallet_address, + Chain::Arbitrum, + Chain::Optimism, + AssetId::from_token(Chain::Arbitrum, "0xaf88d065e77c8cc2239327c5edb3a432268e5831"), + USDC_OP_ASSET_ID.into(), + USDC_OP_ASSET_ID.into(), + referral_fee, + None, + false, + 6, + ); + + let token_address = Address::from_str("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85").unwrap(); + let destination_message = across.build_destination_message(&ctx, &amount, Some(&token_address)).unwrap(); + + let expected_fee = amount * U256::from(100u64) / U256::from(10000u64); + assert_eq!(destination_message.referral_fee, expected_fee); + + let instructions = multicall_handler::Instructions::abi_decode(&destination_message.bytes).unwrap(); + assert_eq!(instructions.calls.len(), 2); + assert_eq!(instructions.calls[0].target, token_address); + assert_eq!(instructions.calls[1].target, token_address); + assert_eq!(instructions.calls[0].value, U256::from(0)); + assert_eq!(instructions.calls[1].value, U256::from(0)); + } + + #[test] + fn test_build_destination_message_solana_with_referral() { + let across = Across::new(mock_provider("{}")); + let amount = U256::from(2_000_000u64); + let destination = "7g2rVN8fAAQdPh1mkajpvELqYa3gWvFXJsBLnKfEQfqy"; + let referral_address = "5fmLrs2GuhfDP1B51ziV5Kd1xtAr9rw1jf3aQ4ihZ2gy"; + let request = make_request( + AssetId::from_token(Chain::Ethereum, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + SOLANA_USDC.id.clone(), + "0x1111111111111111111111111111111111111111", + destination, + amount.to_string().as_str(), + ); + let referral_fee = ReferralFee { + address: referral_address.into(), + bps: 100, + }; + let ctx = make_quote_context( + &request, + amount, + &request.wallet_address, + Chain::Ethereum, + Chain::Solana, + AssetId::from_token(Chain::Ethereum, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + SOLANA_USDC.id.clone(), + SOLANA_USDC.id.clone(), + referral_fee, + Some(destination), + false, + 6, + ); + + let destination_message = across.build_destination_message(&ctx, &amount, None).unwrap(); + let expected_fee = amount * U256::from(100u64) / U256::from(10000u64); + assert_eq!(destination_message.referral_fee, expected_fee); + + let across_message: AcrossPlusMessage = borsh::from_slice(&destination_message.bytes).unwrap(); + assert_eq!(across_message.read_only_len, 3); + assert!(across_message.accounts.iter().any(|acc| acc.to_string() == destination)); + assert!(across_message.accounts.iter().any(|acc| acc.to_string() == referral_address)); + + let compiled: Vec = borsh::from_slice(&across_message.handler_message).unwrap(); + assert_eq!(compiled.len(), 2); + assert_eq!(compiled[0].account_key_indexes.len(), 4); + } + + #[tokio::test] + async fn test_relay_data_recipient_destination() { + let across = Across::new(mock_provider("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x5208\"}")); + let amount = U256::from(12345u64); + let wallet = "0x1111111111111111111111111111111111111111"; + let request = make_request( + AssetId::from_token(Chain::Ethereum, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), + USDC_OP_ASSET_ID.into(), + wallet, + wallet, + amount.to_string().as_str(), + ); + let input_asset = AssetId::from_token(Chain::Ethereum, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + let output_token = Across::decode_address_bytes32(&Address::from_str("0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85").unwrap()); + let ctx = make_quote_context( + &request, + amount, + wallet, + Chain::Ethereum, + Chain::Optimism, + input_asset.clone(), + USDC_OP_ASSET_ID.into(), + USDC_OP_ASSET_ID.into(), + ReferralFee::default(), + None, + true, + 6, + ); + + let empty_message = DestinationMessage { + bytes: vec![], + referral_fee: U256::ZERO, + recipient: RelayRecipient::Evm(Address::from_str(wallet).unwrap()), + }; + let (gas_limit, v3_relay_data) = across.estimate_gas_limit(&ctx, &empty_message, output_token).await.unwrap(); + + assert_eq!(gas_limit, U256::from(21000u64)); + + let expected_recipient_user = Across::decode_address_bytes32(&Address::from_str(wallet).unwrap()); + + assert_eq!(v3_relay_data.recipient, expected_recipient_user); + + let expected_input_token = Across::decode_address_bytes32(&Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap()); + + assert_eq!(v3_relay_data.inputToken, expected_input_token); + assert_eq!(v3_relay_data.outputToken, output_token); + + let multicall_addr = Address::from_str(ctx.destination_deployment.multicall_handler().as_str()).unwrap(); + let message = DestinationMessage { + bytes: vec![0x01], + referral_fee: U256::ZERO, + recipient: RelayRecipient::Evm(multicall_addr), + }; + let (gas_limit2, v3_relay_data2) = across.estimate_gas_limit(&ctx, &message, output_token).await.unwrap(); + + assert_eq!(gas_limit2, U256::from(21000u64)); + + let expected_recipient_mc = Across::decode_address_bytes32(&multicall_addr); + + assert_eq!(v3_relay_data2.recipient, expected_recipient_mc); + assert_eq!(v3_relay_data2.inputToken, expected_input_token); + assert_eq!(v3_relay_data2.outputToken, output_token); + + let base_weth = "0x4200000000000000000000000000000000000006"; + let output_token_base = Across::decode_address_bytes32(&Address::from_str(base_weth).unwrap()); + let base_ctx = make_quote_context( + &request, + amount, + wallet, + Chain::Ethereum, + Chain::Base, + input_asset.clone(), + AssetId::from_token(Chain::Base, base_weth), + AssetId::from_chain(Chain::Base), + ReferralFee::default(), + None, + true, + 18, + ); + let base_message = DestinationMessage { + bytes: vec![], + referral_fee: U256::ZERO, + recipient: RelayRecipient::Evm(Address::from_str(wallet).unwrap()), + }; + let (gas_limit3, v3_relay_data3) = across.estimate_gas_limit(&base_ctx, &base_message, output_token_base).await.unwrap(); + + assert_eq!(gas_limit3, U256::from(21000u64)); + + let expected_input_token_eth_weth = Across::decode_address_bytes32(&Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap()); + let expected_output_token_base_weth = Across::decode_address_bytes32(&Address::from_str(base_weth).unwrap()); + + assert_eq!(v3_relay_data3.inputToken, expected_input_token_eth_weth); + assert_eq!(v3_relay_data3.outputToken, expected_output_token_base_weth); + } + #[cfg(all(test, feature = "swap_integration_tests", feature = "reqwest_provider"))] mod swap_integration_tests { use super::*; @@ -662,7 +1404,7 @@ mod tests { to_asset: AssetId::from_chain(Chain::Arbitrum).into(), wallet_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), destination_address: "0x514BCb1F9AAbb904e6106Bd1052B66d2706dBbb7".into(), - value: "20000000000000000".into(), // 0.02 ETH + value: "20000000000000000".into(), mode: SwapperMode::ExactIn, options, }; @@ -700,7 +1442,7 @@ mod tests { to_asset: to_asset.into(), wallet_address: wallet.into(), destination_address: wallet.into(), - value: "50000000".into(), // 50 USDC + value: "50000000".into(), mode: SwapperMode::ExactIn, options, }; @@ -719,16 +1461,50 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_across_quote_eth_usdc_to_solana_usdc() -> Result<(), SwapperError> { + let network_provider = Arc::new(NativeProvider::default()); + let swap_provider = Across::boxed(network_provider.clone()); + let options = Options { + slippage: 100.into(), + fee: None, + preferred_providers: vec![], + use_max_amount: false, + }; + + let wallet = "0x9b1fe00135e0ff09389bfaeff0c8f299ec818d4a"; + let destination = "7g2rVN8fAAQdPh1mkajpvELqYa3gWvFXJsBLnKfEQfqy"; + let from_asset: AssetId = USDC_ETH_ASSET_ID.into(); + let to_asset: AssetId = USDC_SOLANA_ASSET_ID.into(); + let request = QuoteRequest { + from_asset: from_asset.into(), + to_asset: to_asset.into(), + wallet_address: wallet.into(), + destination_address: destination.into(), + value: "1000000".into(), + mode: SwapperMode::ExactIn, + options, + }; + + let now = SystemTime::now(); + let quote = swap_provider.fetch_quote(&request).await?; + let elapsed = SystemTime::now().duration_since(now).unwrap(); + + println!("<== elapsed: {:?}", elapsed); + println!("<== quote: {:?}", quote); + assert!(quote.to_value.parse::().unwrap() > 0); + + let quote_data = swap_provider.fetch_quote_data("e, FetchQuoteData::None).await?; + println!("<== quote_data: {:?}", quote_data); + + Ok(()) + } + #[tokio::test] async fn test_get_swap_result() -> Result<(), Box> { let network_provider = Arc::new(NativeProvider::default()); let swap_provider = Across::new(network_provider.clone()); - // https://uniscan.xyz/tx/0x9827ca4bdd5dea3a310cff3485f87463987cdc52118077dba34f86ee79456952 - // IMPORTANT: This transaction may not be available on the default Unichain RPC endpoint - // (https://mainnet.unichain.org). It works on https://unichain-rpc.publicnode.com - // The transaction receipt contains: - // - Log 1, Topic 2: deposit ID (0x86f4 = 34548) let tx_hash = "0x9827ca4bdd5dea3a310cff3485f87463987cdc52118077dba34f86ee79456952"; let chain = Chain::Unichain; diff --git a/crates/swapper/src/across/solana.rs b/crates/swapper/src/across/solana.rs new file mode 100644 index 000000000..e3e635bb0 --- /dev/null +++ b/crates/swapper/src/across/solana.rs @@ -0,0 +1,36 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_primitives::types::Pubkey; + +pub const MULTICALL_HANDLER: &str = "HaQe51FWtnmaEcuYEfPA7MRCXKrtqptat4oJdJ8zV5Be"; + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct CompiledIx { + pub program_id_index: u8, + pub account_key_indexes: Vec, + pub data: Vec, +} + +#[derive(BorshDeserialize, BorshSerialize, Clone)] +pub struct RelayData { + pub depositor: Pubkey, + pub recipient: Pubkey, + pub exclusive_relayer: Pubkey, + pub input_token: Pubkey, + pub output_token: Pubkey, + pub input_amount: [u8; 32], + pub output_amount: u64, + pub origin_chain_id: u64, + pub deposit_id: [u8; 32], + pub fill_deadline: u32, + pub exclusivity_deadline: u32, + pub message: Vec, +} + +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct AcrossPlusMessage { + pub handler: Pubkey, + pub read_only_len: u8, + pub value_amount: u64, + pub accounts: Vec, + pub handler_message: Vec, +} diff --git a/crates/swapper/src/chainlink.rs b/crates/swapper/src/chainlink.rs index 02b6f3451..0e1bd8e6b 100644 --- a/crates/swapper/src/chainlink.rs +++ b/crates/swapper/src/chainlink.rs @@ -3,7 +3,7 @@ use num_traits::FromBytes; use crate::SwapperError; use gem_evm::{ - chainlink::contract::{AggregatorInterface, CHAINLINK_ETH_USD_FEED, CHAINLINK_MON_USD_FEED}, + chainlink::contract::{AggregatorInterface, CHAINLINK_ETH_USD_FEED, CHAINLINK_MON_USD_FEED, CHAINLINK_SOL_USD_FEED}, multicall3::{IMulticall3, create_call3, decode_call3_return}, }; @@ -31,6 +31,12 @@ impl ChainlinkPriceFeed { } } + pub fn new_sol_usd_feed() -> ChainlinkPriceFeed { + ChainlinkPriceFeed { + contract: CHAINLINK_SOL_USD_FEED.into(), + } + } + pub fn latest_round_call3(&self) -> IMulticall3::Call3 { create_call3(&self.contract, AggregatorInterface::latestRoundDataCall {}) }