From 724c735d97c8737348e385a4b2a9fbe580e575d7 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:25:09 +0200 Subject: [PATCH 01/23] Add yielder crate with Yo earn provider --- Cargo.lock | 19 ++++ Cargo.toml | 1 + crates/yielder/Cargo.toml | 31 ++++++ crates/yielder/src/client_factory.rs | 14 +++ crates/yielder/src/error.rs | 82 ++++++++++++++ crates/yielder/src/lib.rs | 9 ++ crates/yielder/src/provider.rs | 15 +++ crates/yielder/src/yielder.rs | 73 ++++++++++++ crates/yielder/src/yo/assets.rs | 31 ++++++ crates/yielder/src/yo/client.rs | 143 ++++++++++++++++++++++++ crates/yielder/src/yo/contract.rs | 20 ++++ crates/yielder/src/yo/mod.rs | 13 +++ crates/yielder/src/yo/provider.rs | 161 +++++++++++++++++++++++++++ 13 files changed, 612 insertions(+) create mode 100644 crates/yielder/Cargo.toml create mode 100644 crates/yielder/src/client_factory.rs create mode 100644 crates/yielder/src/error.rs create mode 100644 crates/yielder/src/lib.rs create mode 100644 crates/yielder/src/provider.rs create mode 100644 crates/yielder/src/yielder.rs create mode 100644 crates/yielder/src/yo/assets.rs create mode 100644 crates/yielder/src/yo/client.rs create mode 100644 crates/yielder/src/yo/contract.rs create mode 100644 crates/yielder/src/yo/mod.rs create mode 100644 crates/yielder/src/yo/provider.rs diff --git a/Cargo.lock b/Cargo.lock index 7f9a8687d..16b5c14d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3478,6 +3478,7 @@ dependencies = [ "tokio", "uniffi", "url", + "yielder", "zeroize", ] @@ -8962,6 +8963,24 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "yielder" +version = "1.0.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "async-trait", + "futures", + "gem_client", + "gem_evm", + "gem_jsonrpc", + "num-bigint", + "primitives", + "reqwest 0.13.2", + "strum", + "tokio", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 1ff226f71..e0ddf2426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ "crates/tracing", "crates/claude", "crates/support", + "crates/yielder", ] [workspace.dependencies] diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml new file mode 100644 index 000000000..00d51ce62 --- /dev/null +++ b/crates/yielder/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "yielder" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +default = [] +yield_integration_tests = [ + "gem_jsonrpc/reqwest", + "gem_client/reqwest", + "tokio/rt-multi-thread", +] + +[dependencies] +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +num-bigint = { workspace = true } +gem_client = { path = "../gem_client" } +gem_evm = { path = "../gem_evm", features = ["rpc"] } +gem_jsonrpc = { path = "../gem_jsonrpc" } +primitives = { path = "../primitives" } +async-trait = { workspace = true } +futures = { workspace = true } +strum = { workspace = true } + +[dev-dependencies] +gem_client = { path = "../gem_client", features = ["reqwest"] } +gem_jsonrpc = { path = "../gem_jsonrpc", features = ["reqwest"] } +reqwest = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/yielder/src/client_factory.rs b/crates/yielder/src/client_factory.rs new file mode 100644 index 000000000..0787baa93 --- /dev/null +++ b/crates/yielder/src/client_factory.rs @@ -0,0 +1,14 @@ +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::client::JsonRpcClient; +use gem_jsonrpc::{RpcClient, RpcClientError, RpcProvider}; +use primitives::{Chain, EVMChain}; +use std::sync::Arc; + +use crate::error::YielderError; + +pub fn create_eth_client(provider: Arc>, chain: Chain) -> Result>, YielderError> { + let evm_chain = EVMChain::from_chain(chain).ok_or_else(|| YielderError::unsupported_chain(&chain))?; + let endpoint = provider.get_endpoint(chain).map_err(|e| YielderError::NetworkError(e.to_string()))?; + let client = RpcClient::new(endpoint, provider); + Ok(EthereumClient::new(JsonRpcClient::new(client), evm_chain)) +} diff --git a/crates/yielder/src/error.rs b/crates/yielder/src/error.rs new file mode 100644 index 000000000..d63c8a212 --- /dev/null +++ b/crates/yielder/src/error.rs @@ -0,0 +1,82 @@ +use std::error::Error; +use std::fmt::{self, Display, Formatter}; + +use alloy_primitives::hex::FromHexError; +use alloy_primitives::ruint::ParseError; +use gem_client::ClientError; +use gem_jsonrpc::types::JsonRpcError; + +#[derive(Debug, Clone)] +pub enum YielderError { + InvalidAddress(String), + InvalidAmount(String), + UnsupportedAsset(String), + UnsupportedChain(String), + ProviderNotFound(String), + NetworkError(String), +} + +impl YielderError { + pub fn unsupported_asset(asset: &impl Display) -> Self { + Self::UnsupportedAsset(asset.to_string()) + } + + pub fn unsupported_chain(chain: &impl Display) -> Self { + Self::UnsupportedChain(chain.to_string()) + } + + pub fn provider_not_found(provider: &impl Display) -> Self { + Self::ProviderNotFound(provider.to_string()) + } +} + +impl fmt::Display for YielderError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidAddress(msg) => write!(f, "Invalid address: {msg}"), + Self::InvalidAmount(msg) => write!(f, "Invalid amount: {msg}"), + Self::UnsupportedAsset(msg) => write!(f, "Unsupported asset: {msg}"), + Self::UnsupportedChain(msg) => write!(f, "Unsupported chain: {msg}"), + Self::ProviderNotFound(msg) => write!(f, "Provider not found: {msg}"), + Self::NetworkError(msg) => write!(f, "Network error: {msg}"), + } + } +} + +impl Error for YielderError {} + +impl From for YielderError { + fn from(err: FromHexError) -> Self { + Self::InvalidAddress(err.to_string()) + } +} + +impl From for YielderError { + fn from(err: ParseError) -> Self { + Self::InvalidAmount(err.to_string()) + } +} + +impl From for YielderError { + fn from(err: ClientError) -> Self { + Self::NetworkError(err.to_string()) + } +} + +impl From for YielderError { + fn from(err: JsonRpcError) -> Self { + Self::NetworkError(err.to_string()) + } +} + +impl From> for YielderError { + fn from(err: Box) -> Self { + Self::NetworkError(err.to_string()) + } +} + +impl From for YielderError { + fn from(err: strum::ParseError) -> Self { + Self::ProviderNotFound(err.to_string()) + } +} diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs new file mode 100644 index 000000000..a495d9229 --- /dev/null +++ b/crates/yielder/src/lib.rs @@ -0,0 +1,9 @@ +mod client_factory; +mod error; +mod provider; +mod yielder; +mod yo; + +pub use error::YielderError; +pub use primitives::YieldProvider; +pub use yielder::Yielder; diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs new file mode 100644 index 000000000..a1864f7b4 --- /dev/null +++ b/crates/yielder/src/provider.rs @@ -0,0 +1,15 @@ +use async_trait::async_trait; +use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, YieldProvider}; + +use crate::error::YielderError; + +#[async_trait] +pub trait EarnProvider: Send + Sync { + fn id(&self) -> YieldProvider; + fn earn_providers(&self, asset_id: &AssetId) -> Vec; + fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec; + + async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError>; + async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result; + async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result; +} diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs new file mode 100644 index 000000000..af9c0752b --- /dev/null +++ b/crates/yielder/src/yielder.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use gem_jsonrpc::{RpcClientError, RpcProvider}; +use num_bigint::BigUint; +use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; + +use crate::client_factory::create_eth_client; +use crate::error::YielderError; +use crate::provider::EarnProvider; +use crate::yo::{YO_GATEWAY, YoClient, YoEarnProvider, YoGatewayClient, supported_assets}; + +pub struct Yielder { + providers: Vec>, +} + +impl Yielder { + pub fn new(rpc_provider: Arc>) -> Self { + let gateways: HashMap> = supported_assets() + .iter() + .filter_map(|asset| { + let chain = asset.chain; + let client = create_eth_client(rpc_provider.clone(), chain).ok()?; + Some((chain, Arc::new(YoGatewayClient::new(client, YO_GATEWAY)) as Arc)) + }) + .collect(); + + let yo_provider: Arc = Arc::new(YoEarnProvider::new(gateways)); + Self { providers: vec![yo_provider] } + } + + pub fn providers(&self, asset_id: &AssetId) -> Vec { + self.providers.iter().flat_map(|p| p.earn_providers(asset_id)).collect() + } + + pub async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Vec { + let futures: Vec<_> = self.providers.iter().map(|p| p.get_positions(chain, address, asset_ids)).collect(); + futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect() + } + + pub async fn balance(&self, chain: Chain, address: &str) -> Vec { + let asset_ids: Vec<_> = self.providers.iter().flat_map(|p| p.earn_asset_ids_for_chain(chain)).collect(); + let positions: HashMap<_, _> = self.positions(chain, address, &asset_ids).await.into_iter().map(|p| (p.asset_id, p.balance)).collect(); + + asset_ids + .into_iter() + .map(|id| { + let balance = positions.get(&id).cloned().unwrap_or(BigUint::ZERO); + AssetBalance::new_earn(id, balance) + }) + .collect() + } + + pub async fn get_earn_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { + let provider_id = earn_type.provider_id().parse::()?; + let provider = self.get_provider(provider_id)?; + match earn_type { + EarnType::Deposit(_) => provider.deposit(asset_id, address, value).await, + EarnType::Withdraw(delegation) => { + let shares = delegation.base.shares.to_string(); + provider.withdraw(asset_id, address, value, &shares).await + } + } + } + + fn get_provider(&self, provider: YieldProvider) -> Result, YielderError> { + self.providers + .iter() + .find(|p| p.id() == provider) + .cloned() + .ok_or_else(|| YielderError::provider_not_found(&provider)) + } +} diff --git a/crates/yielder/src/yo/assets.rs b/crates/yielder/src/yo/assets.rs new file mode 100644 index 000000000..56d168468 --- /dev/null +++ b/crates/yielder/src/yo/assets.rs @@ -0,0 +1,31 @@ +use alloy_primitives::{Address, address}; +use primitives::{AssetId, Chain}; + +#[derive(Debug, Clone, Copy)] +pub struct YoAsset { + pub chain: Chain, + pub asset_token: Address, + pub yo_token: Address, +} + +impl YoAsset { + pub fn asset_id(&self) -> AssetId { + AssetId::from_token(self.chain, &self.asset_token.to_string()) + } +} + +pub const YO_USDC: YoAsset = YoAsset { + chain: Chain::Base, + asset_token: address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), + yo_token: address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), +}; + +pub const YO_USDT: YoAsset = YoAsset { + chain: Chain::Ethereum, + asset_token: address!("0xdac17f958d2ee523a2206206994597c13d831ec7"), + yo_token: address!("0xb9a7da9e90d3b428083bae04b860faa6325b721e"), +}; + +pub fn supported_assets() -> &'static [YoAsset] { + &[YO_USDC, YO_USDT] +} diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs new file mode 100644 index 000000000..ffed5f538 --- /dev/null +++ b/crates/yielder/src/yo/client.rs @@ -0,0 +1,143 @@ +use alloy_primitives::{Address, U256}; +use alloy_sol_types::SolCall; +use async_trait::async_trait; +use gem_client::Client; +use gem_evm::contracts::IERC20; +use gem_evm::jsonrpc::TransactionObject; +use gem_evm::multicall3::{create_call3, decode_call3_return}; +use gem_evm::rpc::EthereumClient; +use primitives::swap::ApprovalData; + +use super::assets::YoAsset; +use super::contract::{IYoGateway, IYoVaultToken}; +use crate::error::YielderError; + +#[derive(Debug, Clone)] +pub struct PositionData { + pub share_balance: U256, + pub asset_balance: U256, +} + +#[async_trait] +pub trait YoClient: Send + Sync { + fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; + fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; + async fn get_positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError>; + async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError>; + async fn convert_to_shares(&self, yo_token: Address, assets: U256) -> Result; +} + +pub struct YoGatewayClient { + ethereum_client: EthereumClient, + contract_address: Address, +} + +impl YoGatewayClient { + pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { + Self { + ethereum_client, + contract_address, + } + } + + fn deposit_call_data(yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { + IYoGateway::depositCall { + yoVault: yo_token, + assets, + minSharesOut: min_shares_out, + receiver, + partnerId: partner_id, + } + .abi_encode() + } + + fn redeem_call_data(yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { + IYoGateway::redeemCall { + yoVault: yo_token, + shares, + minAssetsOut: min_assets_out, + receiver, + partnerId: partner_id, + } + .abi_encode() + } +} + +#[async_trait] +impl YoClient for YoGatewayClient +where + C: Client + Clone + Send + Sync + 'static, +{ + fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { + let data = Self::deposit_call_data(yo_token, assets, min_shares_out, receiver, partner_id); + TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) + } + + fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { + let data = Self::redeem_call_data(yo_token, shares, min_assets_out, receiver, partner_id); + TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) + } + + async fn get_positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { + if assets.is_empty() { + return Ok(vec![]); + } + + let calls: Vec<_> = assets + .iter() + .flat_map(|a| { + let vault = a.yo_token.to_string(); + [ + create_call3(&vault, IYoVaultToken::balanceOfCall { account: owner }), + create_call3(&vault, IYoVaultToken::totalAssetsCall {}), + create_call3(&vault, IYoVaultToken::totalSupplyCall {}), + ] + }) + .collect(); + let results = self.ethereum_client.multicall3(calls).await?; + + results + .chunks(3) + .map(|chunk| { + let shares = decode_call3_return::(&chunk[0])?; + let total_assets = decode_call3_return::(&chunk[1])?; + let total_supply = decode_call3_return::(&chunk[2])?; + let asset_balance = convert_to_assets_ceil(shares, total_assets, total_supply); + Ok(PositionData { share_balance: shares, asset_balance }) + }) + .collect() + } + + async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError> { + let spender = self.contract_address; + + let calls = vec![create_call3(&token.to_string(), IERC20::allowanceCall { owner, spender })]; + let results = self.ethereum_client.multicall3(calls).await?; + let allowance = decode_call3_return::(&results[0])?; + + if allowance < amount { + Ok(Some(ApprovalData { + token: token.to_string(), + spender: spender.to_string(), + value: amount.to_string(), + })) + } else { + Ok(None) + } + } + + async fn convert_to_shares(&self, yo_token: Address, assets: U256) -> Result { + let gateway = self.contract_address.to_string(); + let calls = vec![create_call3(&gateway, IYoGateway::quoteConvertToSharesCall { yoVault: yo_token, assets })]; + let results = self.ethereum_client.multicall3(calls).await?; + Ok(decode_call3_return::(&results[0])?) + } +} + +/// ERC4626 ceiling division: rounds up instead of down so display matches deposited amount. +fn convert_to_assets_ceil(shares: U256, total_assets: U256, total_supply: U256) -> U256 { + if shares.is_zero() || total_supply.is_zero() { + return U256::ZERO; + } + (shares * total_assets + total_supply - U256::from(1)) / total_supply +} diff --git a/crates/yielder/src/yo/contract.rs b/crates/yielder/src/yo/contract.rs new file mode 100644 index 000000000..b8651df41 --- /dev/null +++ b/crates/yielder/src/yo/contract.rs @@ -0,0 +1,20 @@ +use alloy_sol_types::sol; + +sol! { + interface IYoVaultToken { + function balanceOf(address account) external view returns (uint256); + function totalAssets() external view returns (uint256); + function totalSupply() external view returns (uint256); + } + + interface IYoGateway { + function quoteConvertToShares(address yoVault, uint256 assets) external view returns (uint256 shares); + function quoteConvertToAssets(address yoVault, uint256 shares) external view returns (uint256 assets); + function quotePreviewDeposit(address yoVault, uint256 assets) external view returns (uint256 shares); + function quotePreviewRedeem(address yoVault, uint256 shares) external view returns (uint256 assets); + function getAssetAllowance(address yoVault, address owner) external view returns (uint256 allowance); + function getShareAllowance(address yoVault, address owner) external view returns (uint256 allowance); + function deposit(address yoVault, uint256 assets, uint256 minSharesOut, address receiver, uint32 partnerId) external returns (uint256 sharesOut); + function redeem(address yoVault, uint256 shares, uint256 minAssetsOut, address receiver, uint32 partnerId) external returns (uint256 assetsOrRequestId); + } +} diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs new file mode 100644 index 000000000..f146565ac --- /dev/null +++ b/crates/yielder/src/yo/mod.rs @@ -0,0 +1,13 @@ +mod assets; +mod client; +mod contract; +mod provider; + +pub use assets::{YoAsset, supported_assets}; +pub use client::{YoClient, YoGatewayClient}; +pub use provider::YoEarnProvider; + +use alloy_primitives::{Address, address}; + +pub const YO_GATEWAY: Address = address!("0xF1EeE0957267b1A474323Ff9CfF7719E964969FA"); +pub const YO_PARTNER_ID_GEM: u32 = 6548; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs new file mode 100644 index 000000000..2c66cd503 --- /dev/null +++ b/crates/yielder/src/yo/provider.rs @@ -0,0 +1,161 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use alloy_primitives::{Address, U256}; +use async_trait::async_trait; +use gem_evm::u256::u256_to_biguint; +use num_bigint::BigUint; +use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; + +use crate::error::YielderError; +use crate::provider::EarnProvider; + +use super::{YO_PARTNER_ID_GEM, YoAsset, client::YoClient, supported_assets}; + +const GAS_LIMIT: &str = "300000"; +const SLIPPAGE_BPS: u64 = 50; +const PROVIDER: YieldProvider = YieldProvider::Yo; + +pub struct YoEarnProvider { + assets: &'static [YoAsset], + gateways: HashMap>, +} + +impl YoEarnProvider { + pub fn new(gateways: HashMap>) -> Self { + Self { + assets: supported_assets(), + gateways, + } + } + + fn get_asset(&self, asset_id: &AssetId) -> Result { + self.assets + .iter() + .find(|a| a.asset_id() == *asset_id) + .copied() + .ok_or_else(|| YielderError::unsupported_asset(asset_id)) + } + + fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YielderError> { + self.gateways.get(&chain).ok_or_else(|| YielderError::unsupported_chain(&chain)) + } + + async fn fetch_positions(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { + if assets.is_empty() { + return Ok(vec![]); + } + let gateway = self.gateway_for_chain(chain)?; + let owner = Address::from_str(address)?; + let provider_id = PROVIDER.to_string(); + let positions = gateway.get_positions_batch(assets, owner).await?; + + Ok(assets + .iter() + .zip(positions) + .filter_map(|(a, data)| { + if data.share_balance == U256::ZERO { + return None; + } + let asset_id = a.asset_id(); + Some(DelegationBase { + asset_id: asset_id.clone(), + state: DelegationState::Active, + balance: u256_to_biguint(&data.asset_balance), + shares: u256_to_biguint(&data.share_balance), + rewards: BigUint::ZERO, + completion_date: None, + delegation_id: format!("{}-{}", provider_id, asset_id), + validator_id: provider_id.clone(), + }) + }) + .collect()) + } +} + +#[async_trait] +impl EarnProvider for YoEarnProvider { + fn id(&self) -> YieldProvider { + PROVIDER + } + + fn earn_providers(&self, asset_id: &AssetId) -> Vec { + self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| earn_provider(a.chain)).collect() + } + + fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec { + self.assets.iter().filter(|a| a.chain == chain).map(|a| a.asset_id()).collect() + } + + async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { + let assets: Vec<_> = self + .assets + .iter() + .filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())) + .copied() + .collect(); + self.fetch_positions(chain, address, &assets).await + } + + async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result { + let asset = self.get_asset(asset_id)?; + let gateway = self.gateway_for_chain(asset.chain)?; + let wallet = Address::from_str(address)?; + let amount = U256::from_str(value)?; + + let approval = gateway.check_token_allowance(asset.asset_token, wallet, amount).await?; + let expected_shares = gateway.convert_to_shares(asset.yo_token, amount).await?; + let min_shares_out = apply_slippage(expected_shares); + let tx = gateway.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); + + Ok(ContractCallData { + contract_address: tx.to, + call_data: tx.data, + approval, + gas_limit: Some(GAS_LIMIT.to_string()), + }) + } + + async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result { + let asset = self.get_asset(asset_id)?; + let gateway = self.gateway_for_chain(asset.chain)?; + let wallet = Address::from_str(address)?; + let amount = U256::from_str(value)?; + let total_shares = U256::from_str(shares)?; + + let computed_shares = gateway.convert_to_shares(asset.yo_token, amount).await?; + let redeem_shares = if total_shares > computed_shares && total_shares - computed_shares <= U256::from(1) { + total_shares + } else { + computed_shares.min(total_shares) + }; + + let approval = gateway.check_token_allowance(asset.yo_token, wallet, redeem_shares).await?; + let min_assets_out = apply_slippage(amount); + let tx = gateway.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); + + Ok(ContractCallData { + contract_address: tx.to, + call_data: tx.data, + approval, + gas_limit: Some(GAS_LIMIT.to_string()), + }) + } +} + +fn earn_provider(chain: Chain) -> DelegationValidator { + DelegationValidator { + chain, + id: PROVIDER.to_string(), + name: PROVIDER.to_string(), + is_active: true, + commission: 0.0, + apr: 0.0, + provider_type: StakeProviderType::Earn, + } +} + +fn apply_slippage(amount: U256) -> U256 { + amount * U256::from(10_000 - SLIPPAGE_BPS) / U256::from(10_000) +} From 72f68aa31c47af7ea7ae700a485d7d43011c256d Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:25:18 +0200 Subject: [PATCH 02/23] Integrate yielder into gateway for earn operations --- crates/primitives/src/earn_type.rs | 13 +++++++++++++ gemstone/Cargo.toml | 1 + gemstone/src/gateway/mod.rs | 25 ++++++++++++++----------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/crates/primitives/src/earn_type.rs b/crates/primitives/src/earn_type.rs index 538bc0952..999f34a8e 100644 --- a/crates/primitives/src/earn_type.rs +++ b/crates/primitives/src/earn_type.rs @@ -10,3 +10,16 @@ pub enum EarnType { Deposit(DelegationValidator), Withdraw(Delegation), } + +impl EarnType { + pub fn provider(&self) -> &DelegationValidator { + match self { + EarnType::Deposit(provider) => provider, + EarnType::Withdraw(delegation) => &delegation.validator, + } + } + + pub fn provider_id(&self) -> &str { + &self.provider().id + } +} diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 0f9cf21ad..968c29c58 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -44,6 +44,7 @@ gem_wallet_connect = { path = "../crates/gem_wallet_connect" } chain_traits = { path = "../crates/chain_traits" } signer = { path = "../crates/signer" } number_formatter = { path = "../crates/number_formatter" } +yielder = { path = "../crates/yielder" } reqwest = { workspace = true, optional = true } sui-types = { workspace = true } diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 09f05d6fa..10b89af26 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -8,7 +8,7 @@ pub use preferences::EmptyPreferences; pub use preferences::GemPreferences; use preferences::PreferencesWrapper; -use crate::alien::{AlienProvider, new_alien_client}; +use crate::alien::{AlienProvider, AlienProviderWrapper, new_alien_client}; use crate::api_client::GemApiClient; use crate::models::*; use crate::network::JsonRpcClient; @@ -31,6 +31,7 @@ use gem_tron::rpc::{client::TronClient, trongrid::client::TronGridClient}; use gem_xrp::rpc::client::XRPClient; use std::future::Future; use std::sync::Arc; +use yielder::Yielder; use primitives::{AssetId, BitcoinChain, Chain, ChartPeriod, EVMChain, ScanAddressTarget, ScanTransactionPayload, TransactionPreloadInput, chain_cosmos::CosmosChain}; @@ -47,6 +48,7 @@ pub struct GemGateway { pub preferences: Arc, pub secure_preferences: Arc, pub api_client: GemApiClient, + yielder: Yielder, } impl std::fmt::Debug for GemGateway { @@ -151,11 +153,14 @@ impl GemGateway { #[uniffi::constructor] pub fn new(provider: Arc, preferences: Arc, secure_preferences: Arc, api_url: String) -> Self { let api_client = GemApiClient::new(api_url, provider.clone()); + let wrapper = AlienProviderWrapper { provider: provider.clone() }; + let yielder = Yielder::new(Arc::new(wrapper)); Self { provider, preferences, secure_preferences, api_client, + yielder, } } @@ -299,22 +304,20 @@ impl GemGateway { Ok(self.provider(chain).await?.get_is_token_address(&token_id)) } - pub async fn get_balance_earn(&self, _chain: Chain, _address: String) -> Result, GatewayError> { - Ok(vec![]) + pub async fn get_balance_earn(&self, chain: Chain, address: String) -> Result, GatewayError> { + Ok(self.yielder.balance(chain, &address).await) } - pub async fn get_earn_data(&self, _asset_id: AssetId, _address: String, _value: String, _earn_type: GemEarnType) -> Result { - Err(GatewayError::NetworkError { - msg: "Earn provider not available".to_string(), - }) + pub async fn get_earn_data(&self, asset_id: AssetId, address: String, value: String, earn_type: GemEarnType) -> Result { + self.yielder.get_earn_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) } - pub fn get_earn_providers(&self, _asset_id: AssetId) -> Vec { - vec![] + pub fn get_earn_providers(&self, asset_id: AssetId) -> Vec { + self.yielder.providers(&asset_id) } - pub async fn get_earn_positions(&self, _chain: Chain, _address: String, _asset_ids: Vec) -> Result, GatewayError> { - Ok(vec![]) + pub async fn get_earn_positions(&self, chain: Chain, address: String, asset_ids: Vec) -> Result, GatewayError> { + Ok(self.yielder.positions(chain, &address, &asset_ids).await) } pub async fn get_node_status(&self, chain: Chain, url: &str) -> Result { From f5aeaeefbb72ffdc69e0fa326afc75c547983d5b Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:13:44 +0200 Subject: [PATCH 03/23] Refactor Yo client and provider Replace internal call-data helpers with YoClient impl that builds TransactionObject directly (using IYoGateway::...abi_encode). Simplify IYoGateway ABI by removing several unused view functions. Update provider logic: remove empty-assets early return in fetch_positions, inline asset filtering, adjust DelegationBase field ordering (set delegation_id and validator_id earlier), and rename local tx -> transaction while mapping .to/.data into ContractCallData. Miscellaneous formatting/cleanup. --- crates/yielder/src/yo/client.rs | 43 ++++++++++++------------------- crates/yielder/src/yo/contract.rs | 5 ---- crates/yielder/src/yo/provider.rs | 28 +++++++------------- 3 files changed, 27 insertions(+), 49 deletions(-) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index ffed5f538..839e3f2c5 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -39,50 +39,38 @@ impl YoGatewayClient { contract_address, } } +} - fn deposit_call_data(yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> Vec { - IYoGateway::depositCall { +#[async_trait] +impl YoClient for YoGatewayClient +where + C: Client + Clone + Send + Sync + 'static, +{ + fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { + let data = IYoGateway::depositCall { yoVault: yo_token, assets, minSharesOut: min_shares_out, receiver, partnerId: partner_id, } - .abi_encode() + .abi_encode(); + TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - fn redeem_call_data(yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> Vec { - IYoGateway::redeemCall { + fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { + let data = IYoGateway::redeemCall { yoVault: yo_token, shares, minAssetsOut: min_assets_out, receiver, partnerId: partner_id, } - .abi_encode() - } -} - -#[async_trait] -impl YoClient for YoGatewayClient -where - C: Client + Clone + Send + Sync + 'static, -{ - fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { - let data = Self::deposit_call_data(yo_token, assets, min_shares_out, receiver, partner_id); - TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) - } - - fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { - let data = Self::redeem_call_data(yo_token, shares, min_assets_out, receiver, partner_id); + .abi_encode(); TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } async fn get_positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { - if assets.is_empty() { - return Ok(vec![]); - } - let calls: Vec<_> = assets .iter() .flat_map(|a| { @@ -103,7 +91,10 @@ where let total_assets = decode_call3_return::(&chunk[1])?; let total_supply = decode_call3_return::(&chunk[2])?; let asset_balance = convert_to_assets_ceil(shares, total_assets, total_supply); - Ok(PositionData { share_balance: shares, asset_balance }) + Ok(PositionData { + share_balance: shares, + asset_balance, + }) }) .collect() } diff --git a/crates/yielder/src/yo/contract.rs b/crates/yielder/src/yo/contract.rs index b8651df41..2bb1e33ae 100644 --- a/crates/yielder/src/yo/contract.rs +++ b/crates/yielder/src/yo/contract.rs @@ -9,11 +9,6 @@ sol! { interface IYoGateway { function quoteConvertToShares(address yoVault, uint256 assets) external view returns (uint256 shares); - function quoteConvertToAssets(address yoVault, uint256 shares) external view returns (uint256 assets); - function quotePreviewDeposit(address yoVault, uint256 assets) external view returns (uint256 shares); - function quotePreviewRedeem(address yoVault, uint256 shares) external view returns (uint256 assets); - function getAssetAllowance(address yoVault, address owner) external view returns (uint256 allowance); - function getShareAllowance(address yoVault, address owner) external view returns (uint256 allowance); function deposit(address yoVault, uint256 assets, uint256 minSharesOut, address receiver, uint32 partnerId) external returns (uint256 sharesOut); function redeem(address yoVault, uint256 shares, uint256 minAssetsOut, address receiver, uint32 partnerId) external returns (uint256 assetsOrRequestId); } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 2c66cd503..8e725a0e2 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -43,9 +43,6 @@ impl YoEarnProvider { } async fn fetch_positions(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { - if assets.is_empty() { - return Ok(vec![]); - } let gateway = self.gateway_for_chain(chain)?; let owner = Address::from_str(address)?; let provider_id = PROVIDER.to_string(); @@ -60,14 +57,14 @@ impl YoEarnProvider { } let asset_id = a.asset_id(); Some(DelegationBase { - asset_id: asset_id.clone(), + delegation_id: format!("{}-{}", provider_id, asset_id), + validator_id: provider_id.clone(), + asset_id, state: DelegationState::Active, balance: u256_to_biguint(&data.asset_balance), shares: u256_to_biguint(&data.share_balance), rewards: BigUint::ZERO, completion_date: None, - delegation_id: format!("{}-{}", provider_id, asset_id), - validator_id: provider_id.clone(), }) }) .collect()) @@ -89,12 +86,7 @@ impl EarnProvider for YoEarnProvider { } async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { - let assets: Vec<_> = self - .assets - .iter() - .filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())) - .copied() - .collect(); + let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())).copied().collect(); self.fetch_positions(chain, address, &assets).await } @@ -107,11 +99,11 @@ impl EarnProvider for YoEarnProvider { let approval = gateway.check_token_allowance(asset.asset_token, wallet, amount).await?; let expected_shares = gateway.convert_to_shares(asset.yo_token, amount).await?; let min_shares_out = apply_slippage(expected_shares); - let tx = gateway.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); + let transaction = gateway.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); Ok(ContractCallData { - contract_address: tx.to, - call_data: tx.data, + contract_address: transaction.to, + call_data: transaction.data, approval, gas_limit: Some(GAS_LIMIT.to_string()), }) @@ -133,11 +125,11 @@ impl EarnProvider for YoEarnProvider { let approval = gateway.check_token_allowance(asset.yo_token, wallet, redeem_shares).await?; let min_assets_out = apply_slippage(amount); - let tx = gateway.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); + let transaction = gateway.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); Ok(ContractCallData { - contract_address: tx.to, - call_data: tx.data, + contract_address: transaction.to, + call_data: transaction.data, approval, gas_limit: Some(GAS_LIMIT.to_string()), }) From 48a0144c0d271986a7e794cd620e18a853ba2a85 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:33:45 +0200 Subject: [PATCH 04/23] Add unit tests for conversion and slippage Add #[cfg(test)] modules with unit tests for convert_to_assets_ceil (crates/yielder/src/yo/client.rs) and apply_slippage (crates/yielder/src/yo/provider.rs). Tests cover typical cases and edge cases (including zero values) to ensure correct rounding behavior and slippage calculation. --- crates/yielder/src/yo/client.rs | 17 +++++++++++++++++ crates/yielder/src/yo/provider.rs | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 839e3f2c5..1351be513 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -132,3 +132,20 @@ fn convert_to_assets_ceil(shares: U256, total_assets: U256, total_supply: U256) } (shares * total_assets + total_supply - U256::from(1)) / total_supply } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_to_assets_ceil() { + assert_eq!(convert_to_assets_ceil(U256::from(100), U256::from(1000), U256::from(500)), U256::from(200)); + assert_eq!(convert_to_assets_ceil(U256::from(1), U256::from(1000), U256::from(500)), U256::from(2)); + assert_eq!(convert_to_assets_ceil(U256::from(500), U256::from(1000), U256::from(500)), U256::from(1000)); + assert_eq!(convert_to_assets_ceil(U256::from(1), U256::from(10), U256::from(3)), U256::from(4)); + assert_eq!(convert_to_assets_ceil(U256::from(2), U256::from(10), U256::from(3)), U256::from(7)); + assert_eq!(convert_to_assets_ceil(U256::ZERO, U256::from(1000), U256::from(500)), U256::ZERO); + assert_eq!(convert_to_assets_ceil(U256::from(100), U256::from(1000), U256::ZERO), U256::ZERO); + assert_eq!(convert_to_assets_ceil(U256::ZERO, U256::ZERO, U256::ZERO), U256::ZERO); + } +} diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 8e725a0e2..07c89e176 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -151,3 +151,15 @@ fn earn_provider(chain: Chain) -> DelegationValidator { fn apply_slippage(amount: U256) -> U256 { amount * U256::from(10_000 - SLIPPAGE_BPS) / U256::from(10_000) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_apply_slippage() { + assert_eq!(apply_slippage(U256::from(10_000)), U256::from(9_950)); + assert_eq!(apply_slippage(U256::from(1_000_000)), U256::from(995_000)); + assert_eq!(apply_slippage(U256::ZERO), U256::ZERO); + } +} From 8b643f5645eee44cc05f48633d840c1a9ce492ae Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:55:40 +0200 Subject: [PATCH 05/23] Rename yielder/yo methods for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor method names across the yielder and yo modules for clearer, consistent naming. Changes include: - Provider/EarnProvider: get_positions -> positions - Yielder: get_positions -> positions, get_earn_data -> earn_data, get_provider -> provider_by_id - YoClient/YoGatewayClient: get_positions_batch -> positions_batch, convert_to_shares -> quote_shares - YoEarnProvider: get_asset -> asset, fetch_positions -> positions_for_chain, get_positions -> positions - Updated all call sites (crates/yielder and gemstone gateway) to use the new names. No behavior changes intended — purely a rename refactor to improve readability and naming consistency. --- crates/yielder/src/provider.rs | 2 +- crates/yielder/src/yielder.rs | 8 ++++---- crates/yielder/src/yo/client.rs | 8 ++++---- crates/yielder/src/yo/provider.rs | 18 +++++++++--------- gemstone/src/gateway/mod.rs | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index a1864f7b4..1a57df747 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -9,7 +9,7 @@ pub trait EarnProvider: Send + Sync { fn earn_providers(&self, asset_id: &AssetId) -> Vec; fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec; - async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError>; + async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError>; async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result; async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result; } diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index af9c0752b..780076348 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -34,7 +34,7 @@ impl Yielder { } pub async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Vec { - let futures: Vec<_> = self.providers.iter().map(|p| p.get_positions(chain, address, asset_ids)).collect(); + let futures: Vec<_> = self.providers.iter().map(|p| p.positions(chain, address, asset_ids)).collect(); futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect() } @@ -51,9 +51,9 @@ impl Yielder { .collect() } - pub async fn get_earn_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { + pub async fn earn_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { let provider_id = earn_type.provider_id().parse::()?; - let provider = self.get_provider(provider_id)?; + let provider = self.provider_by_id(provider_id)?; match earn_type { EarnType::Deposit(_) => provider.deposit(asset_id, address, value).await, EarnType::Withdraw(delegation) => { @@ -63,7 +63,7 @@ impl Yielder { } } - fn get_provider(&self, provider: YieldProvider) -> Result, YielderError> { + fn provider_by_id(&self, provider: YieldProvider) -> Result, YielderError> { self.providers .iter() .find(|p| p.id() == provider) diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 1351be513..910d0149a 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -22,9 +22,9 @@ pub struct PositionData { pub trait YoClient: Send + Sync { fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; - async fn get_positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError>; + async fn positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError>; async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError>; - async fn convert_to_shares(&self, yo_token: Address, assets: U256) -> Result; + async fn quote_shares(&self, yo_token: Address, assets: U256) -> Result; } pub struct YoGatewayClient { @@ -70,7 +70,7 @@ where TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - async fn get_positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { + async fn positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { let calls: Vec<_> = assets .iter() .flat_map(|a| { @@ -117,7 +117,7 @@ where } } - async fn convert_to_shares(&self, yo_token: Address, assets: U256) -> Result { + async fn quote_shares(&self, yo_token: Address, assets: U256) -> Result { let gateway = self.contract_address.to_string(); let calls = vec![create_call3(&gateway, IYoGateway::quoteConvertToSharesCall { yoVault: yo_token, assets })]; let results = self.ethereum_client.multicall3(calls).await?; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 07c89e176..30760c90c 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -30,7 +30,7 @@ impl YoEarnProvider { } } - fn get_asset(&self, asset_id: &AssetId) -> Result { + fn asset(&self, asset_id: &AssetId) -> Result { self.assets .iter() .find(|a| a.asset_id() == *asset_id) @@ -42,11 +42,11 @@ impl YoEarnProvider { self.gateways.get(&chain).ok_or_else(|| YielderError::unsupported_chain(&chain)) } - async fn fetch_positions(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { + async fn positions_for_chain(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { let gateway = self.gateway_for_chain(chain)?; let owner = Address::from_str(address)?; let provider_id = PROVIDER.to_string(); - let positions = gateway.get_positions_batch(assets, owner).await?; + let positions = gateway.positions_batch(assets, owner).await?; Ok(assets .iter() @@ -85,19 +85,19 @@ impl EarnProvider for YoEarnProvider { self.assets.iter().filter(|a| a.chain == chain).map(|a| a.asset_id()).collect() } - async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { + async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())).copied().collect(); - self.fetch_positions(chain, address, &assets).await + self.positions_for_chain(chain, address, &assets).await } async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result { - let asset = self.get_asset(asset_id)?; + let asset = self.asset(asset_id)?; let gateway = self.gateway_for_chain(asset.chain)?; let wallet = Address::from_str(address)?; let amount = U256::from_str(value)?; let approval = gateway.check_token_allowance(asset.asset_token, wallet, amount).await?; - let expected_shares = gateway.convert_to_shares(asset.yo_token, amount).await?; + let expected_shares = gateway.quote_shares(asset.yo_token, amount).await?; let min_shares_out = apply_slippage(expected_shares); let transaction = gateway.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); @@ -110,13 +110,13 @@ impl EarnProvider for YoEarnProvider { } async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result { - let asset = self.get_asset(asset_id)?; + let asset = self.asset(asset_id)?; let gateway = self.gateway_for_chain(asset.chain)?; let wallet = Address::from_str(address)?; let amount = U256::from_str(value)?; let total_shares = U256::from_str(shares)?; - let computed_shares = gateway.convert_to_shares(asset.yo_token, amount).await?; + let computed_shares = gateway.quote_shares(asset.yo_token, amount).await?; let redeem_shares = if total_shares > computed_shares && total_shares - computed_shares <= U256::from(1) { total_shares } else { diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 10b89af26..23982469f 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -309,7 +309,7 @@ impl GemGateway { } pub async fn get_earn_data(&self, asset_id: AssetId, address: String, value: String, earn_type: GemEarnType) -> Result { - self.yielder.get_earn_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) + self.yielder.earn_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) } pub fn get_earn_providers(&self, asset_id: AssetId) -> Vec { From dd4da0a490f8b5f0453b6cf94184691a593fb6a3 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:10:13 +0200 Subject: [PATCH 06/23] Update YoAsset to checksum --- crates/yielder/src/yo/assets.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/yielder/src/yo/assets.rs b/crates/yielder/src/yo/assets.rs index 56d168468..14c551ae9 100644 --- a/crates/yielder/src/yo/assets.rs +++ b/crates/yielder/src/yo/assets.rs @@ -16,14 +16,14 @@ impl YoAsset { pub const YO_USDC: YoAsset = YoAsset { chain: Chain::Base, - asset_token: address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"), - yo_token: address!("0x0000000f2eb9f69274678c76222b35eec7588a65"), + asset_token: address!("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"), + yo_token: address!("0x0000000f2eB9f69274678c76222B35eEc7588a65"), }; pub const YO_USDT: YoAsset = YoAsset { chain: Chain::Ethereum, - asset_token: address!("0xdac17f958d2ee523a2206206994597c13d831ec7"), - yo_token: address!("0xb9a7da9e90d3b428083bae04b860faa6325b721e"), + asset_token: address!("0xdAC17F958D2ee523a2206206994597C13D831ec7"), + yo_token: address!("0xb9a7da9e90D3B428083BAe04b860faA6325b721e"), }; pub fn supported_assets() -> &'static [YoAsset] { From d2bc6198b2278075474edf5726f4883bbf2467a8 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:25:15 +0200 Subject: [PATCH 07/23] Extracted mapping logic to mapper --- crates/yielder/src/yo/mapper.rs | 60 +++++++++++++++++++++++++++++++ crates/yielder/src/yo/mod.rs | 3 +- crates/yielder/src/yo/provider.rs | 38 +++----------------- 3 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 crates/yielder/src/yo/mapper.rs diff --git a/crates/yielder/src/yo/mapper.rs b/crates/yielder/src/yo/mapper.rs new file mode 100644 index 000000000..a2e1c4d8c --- /dev/null +++ b/crates/yielder/src/yo/mapper.rs @@ -0,0 +1,60 @@ +use gem_evm::u256::u256_to_biguint; +use num_bigint::BigUint; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; + +use super::PositionData; + +pub fn map_to_delegation(asset_id: AssetId, data: &PositionData, provider_id: &str) -> DelegationBase { + DelegationBase { + delegation_id: format!("{}-{}", provider_id, asset_id), + validator_id: provider_id.to_string(), + asset_id, + state: DelegationState::Active, + balance: u256_to_biguint(&data.asset_balance), + shares: u256_to_biguint(&data.share_balance), + rewards: BigUint::ZERO, + completion_date: None, + } +} + +pub fn map_to_earn_provider(chain: Chain, provider: YieldProvider) -> DelegationValidator { + DelegationValidator { + chain, + id: provider.to_string(), + name: provider.to_string(), + is_active: true, + commission: 0.0, + apr: 0.0, + provider_type: StakeProviderType::Earn, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::assets::YO_USDC; + use alloy_primitives::U256; + + #[test] + fn test_map_to_delegation() { + let data = PositionData { + share_balance: U256::from(1_000_000), + asset_balance: U256::from(1_050_000), + }; + + let result = map_to_delegation(YO_USDC.asset_id(), &data, "yo"); + + assert_eq!(result.delegation_id, format!("yo-{}", YO_USDC.asset_id())); + assert_eq!(result.balance, BigUint::from(1_050_000u64)); + assert_eq!(result.shares, BigUint::from(1_000_000u64)); + } + + #[test] + fn test_map_to_earn_provider() { + let result = map_to_earn_provider(Chain::Base, YieldProvider::Yo); + + assert_eq!(result.id, "yo"); + assert_eq!(result.chain, Chain::Base); + assert_eq!(result.provider_type, StakeProviderType::Earn); + } +} diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index f146565ac..a4a474a64 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -1,10 +1,11 @@ mod assets; mod client; mod contract; +mod mapper; mod provider; pub use assets::{YoAsset, supported_assets}; -pub use client::{YoClient, YoGatewayClient}; +pub use client::{PositionData, YoClient, YoGatewayClient}; pub use provider::YoEarnProvider; use alloy_primitives::{Address, address}; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 30760c90c..807dfa010 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -4,14 +4,12 @@ use std::sync::Arc; use alloy_primitives::{Address, U256}; use async_trait::async_trait; -use gem_evm::u256::u256_to_biguint; -use num_bigint::BigUint; -use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; +use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, YieldProvider}; use crate::error::YielderError; use crate::provider::EarnProvider; -use super::{YO_PARTNER_ID_GEM, YoAsset, client::YoClient, supported_assets}; +use super::{YO_PARTNER_ID_GEM, YoAsset, client::YoClient, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; const GAS_LIMIT: &str = "300000"; const SLIPPAGE_BPS: u64 = 50; @@ -51,22 +49,8 @@ impl YoEarnProvider { Ok(assets .iter() .zip(positions) - .filter_map(|(a, data)| { - if data.share_balance == U256::ZERO { - return None; - } - let asset_id = a.asset_id(); - Some(DelegationBase { - delegation_id: format!("{}-{}", provider_id, asset_id), - validator_id: provider_id.clone(), - asset_id, - state: DelegationState::Active, - balance: u256_to_biguint(&data.asset_balance), - shares: u256_to_biguint(&data.share_balance), - rewards: BigUint::ZERO, - completion_date: None, - }) - }) + .filter(|(_, data)| data.share_balance != U256::ZERO) + .map(|(asset, data)| map_to_delegation(asset.asset_id(), &data, &provider_id)) .collect()) } } @@ -78,7 +62,7 @@ impl EarnProvider for YoEarnProvider { } fn earn_providers(&self, asset_id: &AssetId) -> Vec { - self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| earn_provider(a.chain)).collect() + self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, PROVIDER)).collect() } fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec { @@ -136,18 +120,6 @@ impl EarnProvider for YoEarnProvider { } } -fn earn_provider(chain: Chain) -> DelegationValidator { - DelegationValidator { - chain, - id: PROVIDER.to_string(), - name: PROVIDER.to_string(), - is_active: true, - commission: 0.0, - apr: 0.0, - provider_type: StakeProviderType::Earn, - } -} - fn apply_slippage(amount: U256) -> U256 { amount * U256::from(10_000 - SLIPPAGE_BPS) / U256::from(10_000) } From 1d3a7d900b47cd9282ef272e58008b7764a8a88a Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:30:47 +0200 Subject: [PATCH 08/23] Removed PROVIDER const, improved visability for GAS_LIMIT --- crates/yielder/src/yo/provider.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 807dfa010..cafdc4642 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -11,9 +11,8 @@ use crate::provider::EarnProvider; use super::{YO_PARTNER_ID_GEM, YoAsset, client::YoClient, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; -const GAS_LIMIT: &str = "300000"; +const GAS_LIMIT: u64 = 300_000; const SLIPPAGE_BPS: u64 = 50; -const PROVIDER: YieldProvider = YieldProvider::Yo; pub struct YoEarnProvider { assets: &'static [YoAsset], @@ -43,7 +42,7 @@ impl YoEarnProvider { async fn positions_for_chain(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { let gateway = self.gateway_for_chain(chain)?; let owner = Address::from_str(address)?; - let provider_id = PROVIDER.to_string(); + let provider_id = YieldProvider::Yo.to_string(); let positions = gateway.positions_batch(assets, owner).await?; Ok(assets @@ -58,11 +57,11 @@ impl YoEarnProvider { #[async_trait] impl EarnProvider for YoEarnProvider { fn id(&self) -> YieldProvider { - PROVIDER + YieldProvider::Yo } fn earn_providers(&self, asset_id: &AssetId) -> Vec { - self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, PROVIDER)).collect() + self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)).collect() } fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec { From f77510676cff6ba5353c40af7f8070da0df2efa9 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:45:39 +0200 Subject: [PATCH 09/23] update naming consistence --- crates/yielder/src/provider.rs | 6 +++--- crates/yielder/src/yielder.rs | 16 ++++++++-------- crates/yielder/src/yo/provider.rs | 6 +++--- gemstone/src/gateway/mod.rs | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index 1a57df747..ebcffd630 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -6,10 +6,10 @@ use crate::error::YielderError; #[async_trait] pub trait EarnProvider: Send + Sync { fn id(&self) -> YieldProvider; - fn earn_providers(&self, asset_id: &AssetId) -> Vec; - fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec; + fn get_providers(&self, asset_id: &AssetId) -> Vec; + fn get_asset_ids_for_chain(&self, chain: Chain) -> Vec; - async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError>; + async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError>; async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result; async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result; } diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index 780076348..cbc0ea44f 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -29,18 +29,18 @@ impl Yielder { Self { providers: vec![yo_provider] } } - pub fn providers(&self, asset_id: &AssetId) -> Vec { - self.providers.iter().flat_map(|p| p.earn_providers(asset_id)).collect() + pub fn get_providers(&self, asset_id: &AssetId) -> Vec { + self.providers.iter().flat_map(|p| p.get_providers(asset_id)).collect() } - pub async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Vec { - let futures: Vec<_> = self.providers.iter().map(|p| p.positions(chain, address, asset_ids)).collect(); + pub async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Vec { + let futures: Vec<_> = self.providers.iter().map(|p| p.get_positions(chain, address, asset_ids)).collect(); futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect() } - pub async fn balance(&self, chain: Chain, address: &str) -> Vec { - let asset_ids: Vec<_> = self.providers.iter().flat_map(|p| p.earn_asset_ids_for_chain(chain)).collect(); - let positions: HashMap<_, _> = self.positions(chain, address, &asset_ids).await.into_iter().map(|p| (p.asset_id, p.balance)).collect(); + pub async fn get_balance(&self, chain: Chain, address: &str) -> Vec { + let asset_ids: Vec<_> = self.providers.iter().flat_map(|p| p.get_asset_ids_for_chain(chain)).collect(); + let positions: HashMap<_, _> = self.get_positions(chain, address, &asset_ids).await.into_iter().map(|p| (p.asset_id, p.balance)).collect(); asset_ids .into_iter() @@ -51,7 +51,7 @@ impl Yielder { .collect() } - pub async fn earn_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { + pub async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { let provider_id = earn_type.provider_id().parse::()?; let provider = self.provider_by_id(provider_id)?; match earn_type { diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index cafdc4642..b7430ffea 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -60,15 +60,15 @@ impl EarnProvider for YoEarnProvider { YieldProvider::Yo } - fn earn_providers(&self, asset_id: &AssetId) -> Vec { + fn get_providers(&self, asset_id: &AssetId) -> Vec { self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)).collect() } - fn earn_asset_ids_for_chain(&self, chain: Chain) -> Vec { + fn get_asset_ids_for_chain(&self, chain: Chain) -> Vec { self.assets.iter().filter(|a| a.chain == chain).map(|a| a.asset_id()).collect() } - async fn positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { + async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())).copied().collect(); self.positions_for_chain(chain, address, &assets).await } diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 23982469f..3f2eb139d 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -305,19 +305,19 @@ impl GemGateway { } pub async fn get_balance_earn(&self, chain: Chain, address: String) -> Result, GatewayError> { - Ok(self.yielder.balance(chain, &address).await) + Ok(self.yielder.get_balance(chain, &address).await) } pub async fn get_earn_data(&self, asset_id: AssetId, address: String, value: String, earn_type: GemEarnType) -> Result { - self.yielder.earn_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) + self.yielder.get_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) } pub fn get_earn_providers(&self, asset_id: AssetId) -> Vec { - self.yielder.providers(&asset_id) + self.yielder.get_providers(&asset_id) } pub async fn get_earn_positions(&self, chain: Chain, address: String, asset_ids: Vec) -> Result, GatewayError> { - Ok(self.yielder.positions(chain, &address, &asset_ids).await) + Ok(self.yielder.get_positions(chain, &address, &asset_ids).await) } pub async fn get_node_status(&self, chain: Chain, url: &str) -> Result { From 1cd5ef05e1ac68e392791fd96f01217c0534262e Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:53:09 +0200 Subject: [PATCH 10/23] Simplify errors --- crates/yielder/src/error.rs | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/crates/yielder/src/error.rs b/crates/yielder/src/error.rs index d63c8a212..4d9d71e52 100644 --- a/crates/yielder/src/error.rs +++ b/crates/yielder/src/error.rs @@ -3,42 +3,30 @@ use std::fmt::{self, Display, Formatter}; use alloy_primitives::hex::FromHexError; use alloy_primitives::ruint::ParseError; -use gem_client::ClientError; -use gem_jsonrpc::types::JsonRpcError; #[derive(Debug, Clone)] pub enum YielderError { - InvalidAddress(String), - InvalidAmount(String), - UnsupportedAsset(String), - UnsupportedChain(String), - ProviderNotFound(String), NetworkError(String), } impl YielderError { pub fn unsupported_asset(asset: &impl Display) -> Self { - Self::UnsupportedAsset(asset.to_string()) + Self::NetworkError(format!("Unsupported asset: {asset}")) } pub fn unsupported_chain(chain: &impl Display) -> Self { - Self::UnsupportedChain(chain.to_string()) + Self::NetworkError(format!("Unsupported chain: {chain}")) } pub fn provider_not_found(provider: &impl Display) -> Self { - Self::ProviderNotFound(provider.to_string()) + Self::NetworkError(format!("Provider not found: {provider}")) } } impl fmt::Display for YielderError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - Self::InvalidAddress(msg) => write!(f, "Invalid address: {msg}"), - Self::InvalidAmount(msg) => write!(f, "Invalid amount: {msg}"), - Self::UnsupportedAsset(msg) => write!(f, "Unsupported asset: {msg}"), - Self::UnsupportedChain(msg) => write!(f, "Unsupported chain: {msg}"), - Self::ProviderNotFound(msg) => write!(f, "Provider not found: {msg}"), - Self::NetworkError(msg) => write!(f, "Network error: {msg}"), + Self::NetworkError(msg) => write!(f, "{msg}"), } } } @@ -47,24 +35,12 @@ impl Error for YielderError {} impl From for YielderError { fn from(err: FromHexError) -> Self { - Self::InvalidAddress(err.to_string()) + Self::NetworkError(err.to_string()) } } impl From for YielderError { fn from(err: ParseError) -> Self { - Self::InvalidAmount(err.to_string()) - } -} - -impl From for YielderError { - fn from(err: ClientError) -> Self { - Self::NetworkError(err.to_string()) - } -} - -impl From for YielderError { - fn from(err: JsonRpcError) -> Self { Self::NetworkError(err.to_string()) } } @@ -77,6 +53,6 @@ impl From> for YielderError { impl From for YielderError { fn from(err: strum::ParseError) -> Self { - Self::ProviderNotFound(err.to_string()) + Self::NetworkError(err.to_string()) } } From bfd40ace1b7e743fdcd8007c958dafb3cfc9be34 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:45:04 +0200 Subject: [PATCH 11/23] Yo client: add ERC-4626 interface, typed eth_call, and multicall3 batching Add two methods to EthereumClient: - call_contract: typed eth_call for single contract calls (no multicall3 overhead) - multicall3_batch: batched multicall3 with const generic chunk size Simplify Yo client: - positions_batch uses multicall3_batch instead of manual call building + chunk decoding - check_token_allowance and quote_shares use direct call_contract instead of multicall3 - Remove duplicate IYoVaultToken (was just IERC4626 with a different name) --- crates/gem_evm/src/contracts/erc4626.rs | 9 ++++ crates/gem_evm/src/contracts/mod.rs | 4 +- crates/gem_evm/src/rpc/client.rs | 21 ++++++++ crates/yielder/src/yo/client.rs | 64 +++++++++++-------------- crates/yielder/src/yo/contract.rs | 6 --- 5 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 crates/gem_evm/src/contracts/erc4626.rs diff --git a/crates/gem_evm/src/contracts/erc4626.rs b/crates/gem_evm/src/contracts/erc4626.rs new file mode 100644 index 000000000..2326ac3dc --- /dev/null +++ b/crates/gem_evm/src/contracts/erc4626.rs @@ -0,0 +1,9 @@ +use alloy_sol_types::sol; + +sol! { + interface IERC4626 { + function balanceOf(address account) external view returns (uint256); + function totalAssets() external view returns (uint256); + function totalSupply() external view returns (uint256); + } +} diff --git a/crates/gem_evm/src/contracts/mod.rs b/crates/gem_evm/src/contracts/mod.rs index b51c6bc8a..1f78dc3cc 100644 --- a/crates/gem_evm/src/contracts/mod.rs +++ b/crates/gem_evm/src/contracts/mod.rs @@ -1,7 +1,9 @@ -pub mod erc1155; pub mod erc20; pub mod erc721; +pub mod erc1155; +pub mod erc4626; pub use erc20::IERC20; pub use erc721::IERC721; pub use erc1155::IERC1155; +pub use erc4626::IERC4626; diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index 7d392e2b1..e57e5be25 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -277,4 +277,25 @@ impl EthereumClient { Ok(multicall_results) } + + #[cfg(feature = "rpc")] + pub async fn call_contract(&self, target: Address, sol_call: T) -> Result> { + let call_data = hex::encode_prefixed(sol_call.abi_encode()); + let params = json!([{ "to": target.to_string(), "data": call_data }, Self::latest_block_parameter()]); + let result: String = self.client.call("eth_call", params).await?; + let result_data = hex::decode(&result)?; + Ok(T::abi_decode_returns(&result_data)?) + } + + #[cfg(feature = "rpc")] + pub async fn multicall3_batch( + &self, + items: &[T], + build: impl Fn(&T) -> [Call3; N], + decode: impl Fn(&[MulticallResult]) -> Result>, + ) -> Result, Box> { + let calls = items.iter().flat_map(&build).collect(); + let results = self.multicall3(calls).await?; + results.chunks(N).map(&decode).collect() + } } diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 910d0149a..8720af408 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -2,6 +2,7 @@ use alloy_primitives::{Address, U256}; use alloy_sol_types::SolCall; use async_trait::async_trait; use gem_client::Client; +use gem_evm::contracts::erc4626::IERC4626; use gem_evm::contracts::IERC20; use gem_evm::jsonrpc::TransactionObject; use gem_evm::multicall3::{create_call3, decode_call3_return}; @@ -9,7 +10,7 @@ use gem_evm::rpc::EthereumClient; use primitives::swap::ApprovalData; use super::assets::YoAsset; -use super::contract::{IYoGateway, IYoVaultToken}; +use super::contract::IYoGateway; use crate::error::YielderError; #[derive(Debug, Clone)] @@ -71,40 +72,34 @@ where } async fn positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { - let calls: Vec<_> = assets - .iter() - .flat_map(|a| { - let vault = a.yo_token.to_string(); - [ - create_call3(&vault, IYoVaultToken::balanceOfCall { account: owner }), - create_call3(&vault, IYoVaultToken::totalAssetsCall {}), - create_call3(&vault, IYoVaultToken::totalSupplyCall {}), - ] - }) - .collect(); - let results = self.ethereum_client.multicall3(calls).await?; - - results - .chunks(3) - .map(|chunk| { - let shares = decode_call3_return::(&chunk[0])?; - let total_assets = decode_call3_return::(&chunk[1])?; - let total_supply = decode_call3_return::(&chunk[2])?; - let asset_balance = convert_to_assets_ceil(shares, total_assets, total_supply); - Ok(PositionData { - share_balance: shares, - asset_balance, - }) - }) - .collect() + Ok(self + .ethereum_client + .multicall3_batch( + assets, + |a| { + let vault = a.yo_token.to_string(); + [ + create_call3(&vault, IERC4626::balanceOfCall { account: owner }), + create_call3(&vault, IERC4626::totalAssetsCall {}), + create_call3(&vault, IERC4626::totalSupplyCall {}), + ] + }, + |c| { + let shares = decode_call3_return::(&c[0])?; + let total_assets = decode_call3_return::(&c[1])?; + let total_supply = decode_call3_return::(&c[2])?; + Ok(PositionData { + share_balance: shares, + asset_balance: convert_to_assets_ceil(shares, total_assets, total_supply), + }) + }, + ) + .await?) } async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError> { let spender = self.contract_address; - - let calls = vec![create_call3(&token.to_string(), IERC20::allowanceCall { owner, spender })]; - let results = self.ethereum_client.multicall3(calls).await?; - let allowance = decode_call3_return::(&results[0])?; + let allowance = self.ethereum_client.call_contract(token, IERC20::allowanceCall { owner, spender }).await?; if allowance < amount { Ok(Some(ApprovalData { @@ -118,10 +113,8 @@ where } async fn quote_shares(&self, yo_token: Address, assets: U256) -> Result { - let gateway = self.contract_address.to_string(); - let calls = vec![create_call3(&gateway, IYoGateway::quoteConvertToSharesCall { yoVault: yo_token, assets })]; - let results = self.ethereum_client.multicall3(calls).await?; - Ok(decode_call3_return::(&results[0])?) + let call = IYoGateway::quoteConvertToSharesCall { yoVault: yo_token, assets }; + Ok(self.ethereum_client.call_contract(self.contract_address, call).await?) } } @@ -149,3 +142,4 @@ mod tests { assert_eq!(convert_to_assets_ceil(U256::ZERO, U256::ZERO, U256::ZERO), U256::ZERO); } } + diff --git a/crates/yielder/src/yo/contract.rs b/crates/yielder/src/yo/contract.rs index 2bb1e33ae..237df6ff8 100644 --- a/crates/yielder/src/yo/contract.rs +++ b/crates/yielder/src/yo/contract.rs @@ -1,12 +1,6 @@ use alloy_sol_types::sol; sol! { - interface IYoVaultToken { - function balanceOf(address account) external view returns (uint256); - function totalAssets() external view returns (uint256); - function totalSupply() external view returns (uint256); - } - interface IYoGateway { function quoteConvertToShares(address yoVault, uint256 assets) external view returns (uint256 shares); function deposit(address yoVault, uint256 assets, uint256 minSharesOut, address receiver, uint32 partnerId) external returns (uint256 sharesOut); From de4c4e7457d74783c235d40707a092fd0b4ac058 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:11:08 +0200 Subject: [PATCH 12/23] Refactor: move slippage to gem_evm, rename yielder methods for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move apply_slippage_in_bp from swapper to gem_evm::slippage so both swapper and yielder reuse the same implementation. Yielder naming cleanup: - gateways → clients, gateway_for_chain → get_client - positions_batch → get_positions, quote_shares → get_quote_shares - asset → get_asset, positions_for_chain → get_positions_for_chain - Consistent get_ prefix across all lookup methods Remove duplicate slippage logic from yielder provider. --- crates/gem_evm/src/lib.rs | 1 + crates/{swapper => gem_evm}/src/slippage.rs | 2 +- crates/swapper/src/chainflip/provider.rs | 6 +- crates/swapper/src/lib.rs | 1 - crates/swapper/src/uniswap/v3/commands.rs | 3 +- crates/swapper/src/uniswap/v3/provider.rs | 3 +- crates/swapper/src/uniswap/v4/commands.rs | 3 +- crates/swapper/src/uniswap/v4/provider.rs | 3 +- crates/yielder/src/yielder.rs | 4 +- crates/yielder/src/yo/client.rs | 8 +-- crates/yielder/src/yo/provider.rs | 62 +++++++++------------ 11 files changed, 45 insertions(+), 51 deletions(-) rename crates/{swapper => gem_evm}/src/slippage.rs (97%) diff --git a/crates/gem_evm/src/lib.rs b/crates/gem_evm/src/lib.rs index af872fd9b..e6fd09bf3 100644 --- a/crates/gem_evm/src/lib.rs +++ b/crates/gem_evm/src/lib.rs @@ -17,6 +17,7 @@ pub mod jsonrpc; pub mod message; pub mod monad; pub mod multicall3; +pub mod slippage; pub mod permit2; #[cfg(feature = "rpc")] pub mod registry; diff --git a/crates/swapper/src/slippage.rs b/crates/gem_evm/src/slippage.rs similarity index 97% rename from crates/swapper/src/slippage.rs rename to crates/gem_evm/src/slippage.rs index 03a4de4df..bab4a07c4 100644 --- a/crates/swapper/src/slippage.rs +++ b/crates/gem_evm/src/slippage.rs @@ -39,7 +39,7 @@ mod tests { use super::*; #[test] - fn test_apply_slippage() { + fn test_apply_slippage_in_bp() { assert_eq!(apply_slippage_in_bp(&U256::from(100), 300), U256::from(97)); assert_eq!(apply_slippage_in_bp(&100_u128, 300), 97_u128); assert_eq!(apply_slippage_in_bp(&1000_u64, 500), 950_u64); diff --git a/crates/swapper/src/chainflip/provider.rs b/crates/swapper/src/chainflip/provider.rs index 871371369..7567f132b 100644 --- a/crates/swapper/src/chainflip/provider.rs +++ b/crates/swapper/src/chainflip/provider.rs @@ -14,9 +14,11 @@ use super::{ seed::generate_random_seed, tx_builder, }; +use gem_evm::slippage::apply_slippage_in_bp; + use crate::{ FetchQuoteData, ProviderData, ProviderType, Quote, QuoteRequest, Route, SwapResult, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteData, - alien::RpcProvider, amount_to_value, approval::check_approval_erc20, config::DEFAULT_CHAINFLIP_FEE_BPS, cross_chain::VaultAddresses, slippage, + alien::RpcProvider, amount_to_value, approval::check_approval_erc20, config::DEFAULT_CHAINFLIP_FEE_BPS, cross_chain::VaultAddresses, }; use primitives::{ChainType, chain::Chain, swap::QuoteAsset}; @@ -218,7 +220,7 @@ where }) } else if from_asset.chain.chain_type() == ChainType::Bitcoin { let output_amount: U256 = quote.to_value.parse()?; - let min_output_amount = slippage::apply_slippage_in_bp(&output_amount, quote.data.slippage_bps); + let min_output_amount = apply_slippage_in_bp(&output_amount, quote.data.slippage_bps); VaultSwapExtras::Bitcoin(VaultSwapBtcExtras { chain, min_output_amount: BigUint::from_bytes_le(&min_output_amount.to_le_bytes::<32>()), diff --git a/crates/swapper/src/lib.rs b/crates/swapper/src/lib.rs index cbce4cb89..98113ebe5 100644 --- a/crates/swapper/src/lib.rs +++ b/crates/swapper/src/lib.rs @@ -23,7 +23,6 @@ pub mod permit2_data; pub mod proxy; pub mod referrer; pub mod relay; -pub mod slippage; pub mod swapper; pub mod thorchain; pub mod uniswap; diff --git a/crates/swapper/src/uniswap/v3/commands.rs b/crates/swapper/src/uniswap/v3/commands.rs index b3dbf7ee2..2bdcd36d8 100644 --- a/crates/swapper/src/uniswap/v3/commands.rs +++ b/crates/swapper/src/uniswap/v3/commands.rs @@ -1,4 +1,5 @@ -use crate::{SwapperError, SwapperMode, eth_address, models::*, slippage::apply_slippage_in_bp}; +use crate::{SwapperError, SwapperMode, eth_address, models::*}; +use gem_evm::slippage::apply_slippage_in_bp; use gem_evm::uniswap::command::{ADDRESS_THIS, PayPortion, Permit2Permit, Sweep, Transfer, UniversalRouterCommand, UnwrapWeth, V3SwapExactIn, WrapEth}; use alloy_primitives::{Address, Bytes, U256}; diff --git a/crates/swapper/src/uniswap/v3/provider.rs b/crates/swapper/src/uniswap/v3/provider.rs index e8abe3e83..9f017b248 100644 --- a/crates/swapper/src/uniswap/v3/provider.rs +++ b/crates/swapper/src/uniswap/v3/provider.rs @@ -1,10 +1,11 @@ +use gem_evm::slippage::apply_slippage_in_bp; + use crate::{ FetchQuoteData, Permit2ApprovalData, ProviderData, ProviderType, Quote, QuoteRequest, Swapper, SwapperError, SwapperQuoteData, alien::{RpcClient, RpcProvider}, approval::{check_approval_erc20_with_client, check_approval_permit2_with_client}, eth_address, models::*, - slippage::apply_slippage_in_bp, uniswap::{ deadline::get_sig_deadline, fee_token::get_fee_token, diff --git a/crates/swapper/src/uniswap/v4/commands.rs b/crates/swapper/src/uniswap/v4/commands.rs index fa0ecd51e..94edabb7b 100644 --- a/crates/swapper/src/uniswap/v4/commands.rs +++ b/crates/swapper/src/uniswap/v4/commands.rs @@ -1,6 +1,7 @@ use std::str::FromStr; -use crate::{QuoteRequest, Route, SwapperError, SwapperMode, eth_address, slippage::apply_slippage_in_bp}; +use crate::{QuoteRequest, Route, SwapperError, SwapperMode, eth_address}; +use gem_evm::slippage::apply_slippage_in_bp; use alloy_primitives::{Address, U256}; use gem_evm::uniswap::{ actions::V4Action::{SETTLE, SWAP_EXACT_IN, TAKE}, diff --git a/crates/swapper/src/uniswap/v4/provider.rs b/crates/swapper/src/uniswap/v4/provider.rs index 756d8c42d..fe3479d03 100644 --- a/crates/swapper/src/uniswap/v4/provider.rs +++ b/crates/swapper/src/uniswap/v4/provider.rs @@ -2,12 +2,13 @@ use alloy_primitives::{Address, U256, hex::encode_prefixed as HexEncode}; use async_trait::async_trait; use std::{collections::HashSet, fmt, str::FromStr, sync::Arc, vec}; +use gem_evm::slippage::apply_slippage_in_bp; + use crate::{ FetchQuoteData, Permit2ApprovalData, ProviderData, ProviderType, Quote, QuoteRequest, Swapper, SwapperChainAsset, SwapperError, SwapperProvider, SwapperQuoteData, alien::{RpcClient, RpcProvider}, approval::evm::{check_approval_erc20_with_client, check_approval_permit2_with_client}, eth_address, - slippage::apply_slippage_in_bp, uniswap::{ deadline::get_sig_deadline, fee_token::get_fee_token, diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index cbc0ea44f..5b9385ea3 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -16,7 +16,7 @@ pub struct Yielder { impl Yielder { pub fn new(rpc_provider: Arc>) -> Self { - let gateways: HashMap> = supported_assets() + let clients: HashMap> = supported_assets() .iter() .filter_map(|asset| { let chain = asset.chain; @@ -25,7 +25,7 @@ impl Yielder { }) .collect(); - let yo_provider: Arc = Arc::new(YoEarnProvider::new(gateways)); + let yo_provider: Arc = Arc::new(YoEarnProvider::new(clients)); Self { providers: vec![yo_provider] } } diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 8720af408..234bbdc57 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -23,9 +23,9 @@ pub struct PositionData { pub trait YoClient: Send + Sync { fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; - async fn positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError>; + async fn get_positions(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError>; async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError>; - async fn quote_shares(&self, yo_token: Address, assets: U256) -> Result; + async fn get_quote_shares(&self, yo_token: Address, assets: U256) -> Result; } pub struct YoGatewayClient { @@ -71,7 +71,7 @@ where TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - async fn positions_batch(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { + async fn get_positions(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { Ok(self .ethereum_client .multicall3_batch( @@ -112,7 +112,7 @@ where } } - async fn quote_shares(&self, yo_token: Address, assets: U256) -> Result { + async fn get_quote_shares(&self, yo_token: Address, assets: U256) -> Result { let call = IYoGateway::quoteConvertToSharesCall { yoVault: yo_token, assets }; Ok(self.ethereum_client.call_contract(self.contract_address, call).await?) } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index b7430ffea..b80127e45 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -6,28 +6,30 @@ use alloy_primitives::{Address, U256}; use async_trait::async_trait; use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, YieldProvider}; +use gem_evm::slippage::apply_slippage_in_bp; + use crate::error::YielderError; use crate::provider::EarnProvider; use super::{YO_PARTNER_ID_GEM, YoAsset, client::YoClient, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; const GAS_LIMIT: u64 = 300_000; -const SLIPPAGE_BPS: u64 = 50; +const SLIPPAGE_BPS: u32 = 50; pub struct YoEarnProvider { assets: &'static [YoAsset], - gateways: HashMap>, + clients: HashMap>, } impl YoEarnProvider { - pub fn new(gateways: HashMap>) -> Self { + pub fn new(clients: HashMap>) -> Self { Self { assets: supported_assets(), - gateways, + clients, } } - fn asset(&self, asset_id: &AssetId) -> Result { + fn get_asset(&self, asset_id: &AssetId) -> Result { self.assets .iter() .find(|a| a.asset_id() == *asset_id) @@ -35,15 +37,15 @@ impl YoEarnProvider { .ok_or_else(|| YielderError::unsupported_asset(asset_id)) } - fn gateway_for_chain(&self, chain: Chain) -> Result<&Arc, YielderError> { - self.gateways.get(&chain).ok_or_else(|| YielderError::unsupported_chain(&chain)) + fn get_client(&self, chain: Chain) -> Result<&Arc, YielderError> { + self.clients.get(&chain).ok_or_else(|| YielderError::unsupported_chain(&chain)) } - async fn positions_for_chain(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { - let gateway = self.gateway_for_chain(chain)?; + async fn get_positions_for_chain(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { + let client = self.get_client(chain)?; let owner = Address::from_str(address)?; let provider_id = YieldProvider::Yo.to_string(); - let positions = gateway.positions_batch(assets, owner).await?; + let positions = client.get_positions(assets, owner).await?; Ok(assets .iter() @@ -70,19 +72,19 @@ impl EarnProvider for YoEarnProvider { async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())).copied().collect(); - self.positions_for_chain(chain, address, &assets).await + self.get_positions_for_chain(chain, address, &assets).await } async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result { - let asset = self.asset(asset_id)?; - let gateway = self.gateway_for_chain(asset.chain)?; + let asset = self.get_asset(asset_id)?; + let client = self.get_client(asset.chain)?; let wallet = Address::from_str(address)?; let amount = U256::from_str(value)?; - let approval = gateway.check_token_allowance(asset.asset_token, wallet, amount).await?; - let expected_shares = gateway.quote_shares(asset.yo_token, amount).await?; - let min_shares_out = apply_slippage(expected_shares); - let transaction = gateway.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); + let approval = client.check_token_allowance(asset.asset_token, wallet, amount).await?; + let expected_shares = client.get_quote_shares(asset.yo_token, amount).await?; + let min_shares_out = apply_slippage_in_bp(&expected_shares, SLIPPAGE_BPS); + let transaction = client.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); Ok(ContractCallData { contract_address: transaction.to, @@ -93,22 +95,22 @@ impl EarnProvider for YoEarnProvider { } async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result { - let asset = self.asset(asset_id)?; - let gateway = self.gateway_for_chain(asset.chain)?; + let asset = self.get_asset(asset_id)?; + let client = self.get_client(asset.chain)?; let wallet = Address::from_str(address)?; let amount = U256::from_str(value)?; let total_shares = U256::from_str(shares)?; - let computed_shares = gateway.quote_shares(asset.yo_token, amount).await?; + let computed_shares = client.get_quote_shares(asset.yo_token, amount).await?; let redeem_shares = if total_shares > computed_shares && total_shares - computed_shares <= U256::from(1) { total_shares } else { computed_shares.min(total_shares) }; - let approval = gateway.check_token_allowance(asset.yo_token, wallet, redeem_shares).await?; - let min_assets_out = apply_slippage(amount); - let transaction = gateway.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); + let approval = client.check_token_allowance(asset.yo_token, wallet, redeem_shares).await?; + let min_assets_out = apply_slippage_in_bp(&amount, SLIPPAGE_BPS); + let transaction = client.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); Ok(ContractCallData { contract_address: transaction.to, @@ -119,18 +121,4 @@ impl EarnProvider for YoEarnProvider { } } -fn apply_slippage(amount: U256) -> U256 { - amount * U256::from(10_000 - SLIPPAGE_BPS) / U256::from(10_000) -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_apply_slippage() { - assert_eq!(apply_slippage(U256::from(10_000)), U256::from(9_950)); - assert_eq!(apply_slippage(U256::from(1_000_000)), U256::from(995_000)); - assert_eq!(apply_slippage(U256::ZERO), U256::ZERO); - } -} From 017f14206c742ca358995ec6030252fba792962d Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:30:31 +0200 Subject: [PATCH 13/23] Refactor yielder crate architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simplify EarnProvider trait: merge deposit/withdraw into single get_data, singularize get_providers/get_positions to return Option, remove id(), remove get_asset_ids_for_chain by moving get_balance into trait - Remove redundant chain param from get_position — derived from asset_id - Make Yielder a thin orchestrator: pure routing, no business logic, no provider_by_id - Move client creation from Yielder::new into YoEarnProvider::new - Hide yo/ internals — only YoEarnProvider is exported - Remove dead code: strum dependency, YieldProvider re-export, provider_not_found, strum::ParseError impl - Use biguint_to_u256 instead of BigUint → String → U256 roundtrip - Update gateway FFI: get_earn_provider (singular), get_earn_position (singular, no chain param) --- Cargo.lock | 1 - crates/yielder/Cargo.toml | 1 - crates/yielder/src/error.rs | 12 +-- crates/yielder/src/lib.rs | 1 - crates/yielder/src/provider.rs | 12 ++- crates/yielder/src/yielder.rs | 62 ++++----------- crates/yielder/src/yo/mapper.rs | 2 +- crates/yielder/src/yo/mod.rs | 7 +- crates/yielder/src/yo/provider.rs | 126 +++++++++++++++--------------- gemstone/src/gateway/mod.rs | 8 +- 10 files changed, 89 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16b5c14d6..b4ac443a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8977,7 +8977,6 @@ dependencies = [ "num-bigint", "primitives", "reqwest 0.13.2", - "strum", "tokio", ] diff --git a/crates/yielder/Cargo.toml b/crates/yielder/Cargo.toml index 00d51ce62..8e49f8033 100644 --- a/crates/yielder/Cargo.toml +++ b/crates/yielder/Cargo.toml @@ -22,7 +22,6 @@ gem_jsonrpc = { path = "../gem_jsonrpc" } primitives = { path = "../primitives" } async-trait = { workspace = true } futures = { workspace = true } -strum = { workspace = true } [dev-dependencies] gem_client = { path = "../gem_client", features = ["reqwest"] } diff --git a/crates/yielder/src/error.rs b/crates/yielder/src/error.rs index 4d9d71e52..094fd049f 100644 --- a/crates/yielder/src/error.rs +++ b/crates/yielder/src/error.rs @@ -17,10 +17,6 @@ impl YielderError { pub fn unsupported_chain(chain: &impl Display) -> Self { Self::NetworkError(format!("Unsupported chain: {chain}")) } - - pub fn provider_not_found(provider: &impl Display) -> Self { - Self::NetworkError(format!("Provider not found: {provider}")) - } } impl fmt::Display for YielderError { @@ -49,10 +45,4 @@ impl From> for YielderError { fn from(err: Box) -> Self { Self::NetworkError(err.to_string()) } -} - -impl From for YielderError { - fn from(err: strum::ParseError) -> Self { - Self::NetworkError(err.to_string()) - } -} +} \ No newline at end of file diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index a495d9229..eb5015a94 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,5 +5,4 @@ mod yielder; mod yo; pub use error::YielderError; -pub use primitives::YieldProvider; pub use yielder::Yielder; diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index ebcffd630..f47aff278 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -1,15 +1,13 @@ use async_trait::async_trait; -use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, YieldProvider}; +use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType}; use crate::error::YielderError; #[async_trait] pub trait EarnProvider: Send + Sync { - fn id(&self) -> YieldProvider; - fn get_providers(&self, asset_id: &AssetId) -> Vec; - fn get_asset_ids_for_chain(&self, chain: Chain) -> Vec; + fn get_provider(&self, asset_id: &AssetId) -> Option; - async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError>; - async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result; - async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result; + async fn get_position(&self, address: &str, asset_id: &AssetId) -> Result, YielderError>; + async fn get_balance(&self, chain: Chain, address: &str) -> Result, YielderError>; + async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result; } diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index 5b9385ea3..291bd637b 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -1,14 +1,11 @@ -use std::collections::HashMap; use std::sync::Arc; use gem_jsonrpc::{RpcClientError, RpcProvider}; -use num_bigint::BigUint; -use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; +use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType}; -use crate::client_factory::create_eth_client; use crate::error::YielderError; use crate::provider::EarnProvider; -use crate::yo::{YO_GATEWAY, YoClient, YoEarnProvider, YoGatewayClient, supported_assets}; +use crate::yo::YoEarnProvider; pub struct Yielder { providers: Vec>, @@ -16,58 +13,27 @@ pub struct Yielder { impl Yielder { pub fn new(rpc_provider: Arc>) -> Self { - let clients: HashMap> = supported_assets() - .iter() - .filter_map(|asset| { - let chain = asset.chain; - let client = create_eth_client(rpc_provider.clone(), chain).ok()?; - Some((chain, Arc::new(YoGatewayClient::new(client, YO_GATEWAY)) as Arc)) - }) - .collect(); - - let yo_provider: Arc = Arc::new(YoEarnProvider::new(clients)); - Self { providers: vec![yo_provider] } + Self { + providers: vec![Arc::new(YoEarnProvider::new(rpc_provider))], + } } - pub fn get_providers(&self, asset_id: &AssetId) -> Vec { - self.providers.iter().flat_map(|p| p.get_providers(asset_id)).collect() + pub fn get_provider(&self, asset_id: &AssetId) -> Option { + self.providers.iter().find_map(|p| p.get_provider(asset_id)) } - pub async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Vec { - let futures: Vec<_> = self.providers.iter().map(|p| p.get_positions(chain, address, asset_ids)).collect(); - futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect() + pub async fn get_position(&self, address: &str, asset_id: &AssetId) -> Option { + let futures: Vec<_> = self.providers.iter().map(|p| p.get_position(address, asset_id)).collect(); + futures::future::join_all(futures).await.into_iter().find_map(|r| r.ok().flatten()) } pub async fn get_balance(&self, chain: Chain, address: &str) -> Vec { - let asset_ids: Vec<_> = self.providers.iter().flat_map(|p| p.get_asset_ids_for_chain(chain)).collect(); - let positions: HashMap<_, _> = self.get_positions(chain, address, &asset_ids).await.into_iter().map(|p| (p.asset_id, p.balance)).collect(); - - asset_ids - .into_iter() - .map(|id| { - let balance = positions.get(&id).cloned().unwrap_or(BigUint::ZERO); - AssetBalance::new_earn(id, balance) - }) - .collect() + let futures: Vec<_> = self.providers.iter().map(|p| p.get_balance(chain, address)).collect(); + futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect() } pub async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { - let provider_id = earn_type.provider_id().parse::()?; - let provider = self.provider_by_id(provider_id)?; - match earn_type { - EarnType::Deposit(_) => provider.deposit(asset_id, address, value).await, - EarnType::Withdraw(delegation) => { - let shares = delegation.base.shares.to_string(); - provider.withdraw(asset_id, address, value, &shares).await - } - } - } - - fn provider_by_id(&self, provider: YieldProvider) -> Result, YielderError> { - self.providers - .iter() - .find(|p| p.id() == provider) - .cloned() - .ok_or_else(|| YielderError::provider_not_found(&provider)) + let provider = self.providers.iter().find(|p| p.get_provider(asset_id).is_some()).ok_or_else(|| YielderError::unsupported_asset(asset_id))?; + provider.get_data(asset_id, address, value, earn_type).await } } diff --git a/crates/yielder/src/yo/mapper.rs b/crates/yielder/src/yo/mapper.rs index a2e1c4d8c..3776af0f6 100644 --- a/crates/yielder/src/yo/mapper.rs +++ b/crates/yielder/src/yo/mapper.rs @@ -2,7 +2,7 @@ use gem_evm::u256::u256_to_biguint; use num_bigint::BigUint; use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; -use super::PositionData; +use super::client::PositionData; pub fn map_to_delegation(asset_id: AssetId, data: &PositionData, provider_id: &str) -> DelegationBase { DelegationBase { diff --git a/crates/yielder/src/yo/mod.rs b/crates/yielder/src/yo/mod.rs index a4a474a64..c06a7f7e8 100644 --- a/crates/yielder/src/yo/mod.rs +++ b/crates/yielder/src/yo/mod.rs @@ -4,11 +4,10 @@ mod contract; mod mapper; mod provider; -pub use assets::{YoAsset, supported_assets}; -pub use client::{PositionData, YoClient, YoGatewayClient}; +use assets::{YoAsset, supported_assets}; pub use provider::YoEarnProvider; use alloy_primitives::{Address, address}; -pub const YO_GATEWAY: Address = address!("0xF1EeE0957267b1A474323Ff9CfF7719E964969FA"); -pub const YO_PARTNER_ID_GEM: u32 = 6548; +const YO_GATEWAY: Address = address!("0xF1EeE0957267b1A474323Ff9CfF7719E964969FA"); +const YO_PARTNER_ID_GEM: u32 = 6548; diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index b80127e45..2e99960c1 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -1,17 +1,19 @@ use std::collections::HashMap; -use std::str::FromStr; use std::sync::Arc; use alloy_primitives::{Address, U256}; use async_trait::async_trait; -use primitives::{AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, YieldProvider}; +use num_bigint::BigUint; +use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; use gem_evm::slippage::apply_slippage_in_bp; +use gem_evm::u256::biguint_to_u256; +use crate::client_factory::create_eth_client; use crate::error::YielderError; use crate::provider::EarnProvider; -use super::{YO_PARTNER_ID_GEM, YoAsset, client::YoClient, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; +use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, client::{YoClient, YoGatewayClient}, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; const GAS_LIMIT: u64 = 300_000; const SLIPPAGE_BPS: u32 = 50; @@ -22,11 +24,16 @@ pub struct YoEarnProvider { } impl YoEarnProvider { - pub fn new(clients: HashMap>) -> Self { - Self { - assets: supported_assets(), - clients, - } + pub fn new(rpc_provider: Arc>) -> Self { + let assets = supported_assets(); + let clients = assets + .iter() + .filter_map(|asset| { + let client = create_eth_client(rpc_provider.clone(), asset.chain).ok()?; + Some((asset.chain, Arc::new(YoGatewayClient::new(client, YO_GATEWAY)) as Arc)) + }) + .collect(); + Self { assets, clients } } fn get_asset(&self, asset_id: &AssetId) -> Result { @@ -41,77 +48,68 @@ impl YoEarnProvider { self.clients.get(&chain).ok_or_else(|| YielderError::unsupported_chain(&chain)) } - async fn get_positions_for_chain(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { - let client = self.get_client(chain)?; - let owner = Address::from_str(address)?; - let provider_id = YieldProvider::Yo.to_string(); - let positions = client.get_positions(assets, owner).await?; + async fn get_position_for_asset(&self, address: &str, asset: &YoAsset) -> Result, YielderError> { + let client = self.get_client(asset.chain)?; + let owner: Address = address.parse()?; + let positions = client.get_positions(&[*asset], owner).await?; - Ok(assets - .iter() - .zip(positions) - .filter(|(_, data)| data.share_balance != U256::ZERO) - .map(|(asset, data)| map_to_delegation(asset.asset_id(), &data, &provider_id)) - .collect()) + Ok(positions.into_iter().find(|d| d.share_balance != U256::ZERO).map(|data| map_to_delegation(asset.asset_id(), &data, YieldProvider::Yo.as_ref()))) } } #[async_trait] impl EarnProvider for YoEarnProvider { - fn id(&self) -> YieldProvider { - YieldProvider::Yo - } - - fn get_providers(&self, asset_id: &AssetId) -> Vec { - self.assets.iter().filter(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)).collect() + fn get_provider(&self, asset_id: &AssetId) -> Option { + self.assets.iter().find(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)) } - fn get_asset_ids_for_chain(&self, chain: Chain) -> Vec { - self.assets.iter().filter(|a| a.chain == chain).map(|a| a.asset_id()).collect() - } - - async fn get_positions(&self, chain: Chain, address: &str, asset_ids: &[AssetId]) -> Result, YielderError> { - let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain && asset_ids.contains(&a.asset_id())).copied().collect(); - self.get_positions_for_chain(chain, address, &assets).await - } - - async fn deposit(&self, asset_id: &AssetId, address: &str, value: &str) -> Result { + async fn get_position(&self, address: &str, asset_id: &AssetId) -> Result, YielderError> { let asset = self.get_asset(asset_id)?; - let client = self.get_client(asset.chain)?; - let wallet = Address::from_str(address)?; - let amount = U256::from_str(value)?; - - let approval = client.check_token_allowance(asset.asset_token, wallet, amount).await?; - let expected_shares = client.get_quote_shares(asset.yo_token, amount).await?; - let min_shares_out = apply_slippage_in_bp(&expected_shares, SLIPPAGE_BPS); - let transaction = client.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); + self.get_position_for_asset(address, &asset).await + } - Ok(ContractCallData { - contract_address: transaction.to, - call_data: transaction.data, - approval, - gas_limit: Some(GAS_LIMIT.to_string()), - }) + async fn get_balance(&self, chain: Chain, address: &str) -> Result, YielderError> { + let chain_assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain).collect(); + let futures: Vec<_> = chain_assets.iter().map(|a| self.get_position_for_asset(address, a)).collect(); + Ok(chain_assets + .iter() + .zip(futures::future::join_all(futures).await) + .map(|(asset, position)| { + let balance = position.ok().flatten().map(|p| p.balance).unwrap_or(BigUint::ZERO); + AssetBalance::new_earn(asset.asset_id(), balance) + }) + .collect()) } - async fn withdraw(&self, asset_id: &AssetId, address: &str, value: &str, shares: &str) -> Result { + async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { let asset = self.get_asset(asset_id)?; let client = self.get_client(asset.chain)?; - let wallet = Address::from_str(address)?; - let amount = U256::from_str(value)?; - let total_shares = U256::from_str(shares)?; - - let computed_shares = client.get_quote_shares(asset.yo_token, amount).await?; - let redeem_shares = if total_shares > computed_shares && total_shares - computed_shares <= U256::from(1) { - total_shares - } else { - computed_shares.min(total_shares) + let wallet: Address = address.parse()?; + let amount: U256 = value.parse()?; + + let (approval, transaction) = match earn_type { + EarnType::Deposit(_) => { + let approval = client.check_token_allowance(asset.asset_token, wallet, amount).await?; + let expected_shares = client.get_quote_shares(asset.yo_token, amount).await?; + let min_shares_out = apply_slippage_in_bp(&expected_shares, SLIPPAGE_BPS); + let transaction = client.build_deposit_transaction(wallet, asset.yo_token, amount, min_shares_out, wallet, YO_PARTNER_ID_GEM); + (approval, transaction) + } + EarnType::Withdraw(delegation) => { + let total_shares = biguint_to_u256(&delegation.base.shares).ok_or_else(|| YielderError::NetworkError("Invalid shares".to_string()))?; + let computed_shares = client.get_quote_shares(asset.yo_token, amount).await?; + let redeem_shares = if total_shares > computed_shares && total_shares - computed_shares <= U256::from(1) { + total_shares + } else { + computed_shares.min(total_shares) + }; + let approval = client.check_token_allowance(asset.yo_token, wallet, redeem_shares).await?; + let min_assets_out = apply_slippage_in_bp(&amount, SLIPPAGE_BPS); + let transaction = client.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); + (approval, transaction) + } }; - let approval = client.check_token_allowance(asset.yo_token, wallet, redeem_shares).await?; - let min_assets_out = apply_slippage_in_bp(&amount, SLIPPAGE_BPS); - let transaction = client.build_redeem_transaction(wallet, asset.yo_token, redeem_shares, min_assets_out, wallet, YO_PARTNER_ID_GEM); - Ok(ContractCallData { contract_address: transaction.to, call_data: transaction.data, @@ -120,5 +118,3 @@ impl EarnProvider for YoEarnProvider { }) } } - - diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 3f2eb139d..0836b12ba 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -312,12 +312,12 @@ impl GemGateway { self.yielder.get_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) } - pub fn get_earn_providers(&self, asset_id: AssetId) -> Vec { - self.yielder.get_providers(&asset_id) + pub fn get_earn_provider(&self, asset_id: AssetId) -> Option { + self.yielder.get_provider(&asset_id) } - pub async fn get_earn_positions(&self, chain: Chain, address: String, asset_ids: Vec) -> Result, GatewayError> { - Ok(self.yielder.get_positions(chain, &address, &asset_ids).await) + pub async fn get_earn_position(&self, address: String, asset_id: AssetId) -> Result, GatewayError> { + Ok(self.yielder.get_position(&address, &asset_id).await) } pub async fn get_node_status(&self, chain: Chain, url: &str) -> Result { From d6b7f1c8680438f22bc6816e4cfbe55fa20a7230 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:51:23 +0200 Subject: [PATCH 14/23] Extract shared alien types and client factories to gem_jsonrpc and gem_evm Move AlienError, RpcClient, RpcProvider, and create_client to gem_jsonrpc::alien. Add create_eth_client to gem_evm::rpc for EVM-specific client creation. Both swapper and yielder can now use shared concrete types with no generics. --- crates/gem_evm/src/rpc/client_factory.rs | 11 ++++ crates/gem_evm/src/rpc/mod.rs | 3 ++ crates/gem_jsonrpc/src/alien.rs | 64 ++++++++++++++++++++++++ crates/gem_jsonrpc/src/lib.rs | 2 + 4 files changed, 80 insertions(+) create mode 100644 crates/gem_evm/src/rpc/client_factory.rs create mode 100644 crates/gem_jsonrpc/src/alien.rs diff --git a/crates/gem_evm/src/rpc/client_factory.rs b/crates/gem_evm/src/rpc/client_factory.rs new file mode 100644 index 000000000..e6a7cfb90 --- /dev/null +++ b/crates/gem_evm/src/rpc/client_factory.rs @@ -0,0 +1,11 @@ +use gem_jsonrpc::alien::{self, RpcClient, RpcProvider}; +use primitives::{Chain, EVMChain}; +use std::sync::Arc; + +use super::EthereumClient; + +pub fn create_eth_client(provider: Arc, chain: Chain) -> Option> { + let evm_chain = EVMChain::from_chain(chain)?; + let client = alien::create_client(provider, chain).ok()?; + Some(EthereumClient::new(client, evm_chain)) +} diff --git a/crates/gem_evm/src/rpc/mod.rs b/crates/gem_evm/src/rpc/mod.rs index a911bdad7..423e06953 100644 --- a/crates/gem_evm/src/rpc/mod.rs +++ b/crates/gem_evm/src/rpc/mod.rs @@ -6,6 +6,9 @@ pub mod model; pub mod staking_mapper; pub mod swap_mapper; +mod client_factory; + pub use client::EthereumClient; +pub use client_factory::create_eth_client; pub use mapper::EthereumMapper; pub use staking_mapper::StakingMapper; diff --git a/crates/gem_jsonrpc/src/alien.rs b/crates/gem_jsonrpc/src/alien.rs new file mode 100644 index 000000000..9cb2a012c --- /dev/null +++ b/crates/gem_jsonrpc/src/alien.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use gem_client::ClientError; +use primitives::Chain; + +use crate::client::JsonRpcClient; +use crate::rpc::{RpcClient as GenericRpcClient, RpcClientError, RpcProvider as GenericRpcProvider}; + +#[derive(Debug, Clone)] +pub enum AlienError { + RequestError { msg: String }, + ResponseError { msg: String }, + Http { status: u16, len: u32 }, +} + +impl AlienError { + pub fn request_error(msg: impl Into) -> Self { + Self::RequestError { msg: msg.into() } + } + + pub fn response_error(msg: impl Into) -> Self { + Self::ResponseError { msg: msg.into() } + } + + pub fn http_error(status: u16, len: usize) -> Self { + Self::Http { + status, + len: len.min(u32::MAX as usize) as u32, + } + } +} + +impl std::fmt::Display for AlienError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RequestError { msg } => write!(f, "Request error: {msg}"), + Self::ResponseError { msg } => write!(f, "Response error: {msg}"), + Self::Http { status, .. } => write!(f, "HTTP error: status {status}"), + } + } +} + +impl std::error::Error for AlienError {} + +impl RpcClientError for AlienError { + fn into_client_error(self) -> ClientError { + match self { + Self::RequestError { msg } | Self::ResponseError { msg } => ClientError::Network(msg), + Self::Http { status, .. } => ClientError::Http { status, body: Vec::new() }, + } + } +} + +pub type RpcClient = GenericRpcClient; + +pub trait RpcProvider: GenericRpcProvider {} + +impl RpcProvider for T where T: GenericRpcProvider {} + +pub fn create_client(provider: Arc, chain: Chain) -> Result, AlienError> { + let endpoint = provider.get_endpoint(chain)?; + let client = RpcClient::new(endpoint, provider); + Ok(JsonRpcClient::new(client)) +} diff --git a/crates/gem_jsonrpc/src/lib.rs b/crates/gem_jsonrpc/src/lib.rs index 4279a274d..7a749c781 100644 --- a/crates/gem_jsonrpc/src/lib.rs +++ b/crates/gem_jsonrpc/src/lib.rs @@ -1,5 +1,7 @@ pub mod types; +#[cfg(feature = "client")] +pub mod alien; #[cfg(feature = "client")] pub mod client; #[cfg(feature = "client")] From 1b77b406a35b0b0a773655ffccf4dbf87816eadf Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:51:41 +0200 Subject: [PATCH 15/23] Migrate swapper to shared alien types from gem_jsonrpc Remove swapper's local AlienError definition, use gem_jsonrpc::alien instead. Update client_factory to delegate to shared create_client and create_eth_client. --- crates/swapper/src/alien/error.rs | 48 -------------------- crates/swapper/src/alien/mock.rs | 3 +- crates/swapper/src/alien/mod.rs | 11 +---- crates/swapper/src/alien/reqwest_provider.rs | 3 +- crates/swapper/src/client_factory.rs | 18 +++----- gemstone/src/alien/provider.rs | 2 +- 6 files changed, 13 insertions(+), 72 deletions(-) delete mode 100644 crates/swapper/src/alien/error.rs diff --git a/crates/swapper/src/alien/error.rs b/crates/swapper/src/alien/error.rs deleted file mode 100644 index 9e8cfd4f1..000000000 --- a/crates/swapper/src/alien/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -use gem_client::ClientError; -use gem_jsonrpc::RpcClientError; - -#[derive(Debug, Clone)] -pub enum AlienError { - RequestError { msg: String }, - ResponseError { msg: String }, - Http { status: u16, len: u32 }, -} - -impl AlienError { - pub fn request_error(msg: impl Into) -> Self { - Self::RequestError { msg: msg.into() } - } - - pub fn response_error(msg: impl Into) -> Self { - Self::ResponseError { msg: msg.into() } - } - - pub fn http_error(status: u16, len: usize) -> Self { - Self::Http { - status, - len: len.min(u32::MAX as usize) as u32, - } - } -} - -impl std::fmt::Display for AlienError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::RequestError { msg } => write!(f, "Request error: {}", msg), - Self::ResponseError { msg } => write!(f, "Response error: {}", msg), - Self::Http { status, .. } => write!(f, "HTTP error: status {}", status), - } - } -} - -impl std::error::Error for AlienError {} - -impl RpcClientError for AlienError { - fn into_client_error(self) -> ClientError { - match self { - Self::RequestError { msg } => ClientError::Network(msg), - Self::ResponseError { msg } => ClientError::Network(msg), - Self::Http { status, .. } => ClientError::Http { status, body: Vec::new() }, - } - } -} diff --git a/crates/swapper/src/alien/mock.rs b/crates/swapper/src/alien/mock.rs index 0b9e060f5..87c2cff0c 100644 --- a/crates/swapper/src/alien/mock.rs +++ b/crates/swapper/src/alien/mock.rs @@ -5,7 +5,8 @@ use std::{ }; use super::{AlienError, Target}; -use gem_jsonrpc::{RpcProvider as GenericRpcProvider, RpcResponse}; +use gem_jsonrpc::RpcResponse; +use gem_jsonrpc::rpc::RpcProvider as GenericRpcProvider; use primitives::Chain; #[allow(unused)] diff --git a/crates/swapper/src/alien/mod.rs b/crates/swapper/src/alien/mod.rs index 9ceaab23b..9c56b0463 100644 --- a/crates/swapper/src/alien/mod.rs +++ b/crates/swapper/src/alien/mod.rs @@ -1,13 +1,6 @@ -pub mod error; pub mod mock; #[cfg(feature = "reqwest_provider")] pub mod reqwest_provider; -pub use error::AlienError; -pub use gem_jsonrpc::{HttpMethod, RpcClient as GenericRpcClient, RpcProvider as GenericRpcProvider, Target}; - -pub type RpcClient = GenericRpcClient; - -pub trait RpcProvider: GenericRpcProvider {} - -impl RpcProvider for T where T: GenericRpcProvider {} +pub use gem_jsonrpc::alien::{AlienError, RpcClient, RpcProvider}; +pub use gem_jsonrpc::{HttpMethod, Target}; diff --git a/crates/swapper/src/alien/reqwest_provider.rs b/crates/swapper/src/alien/reqwest_provider.rs index 1686d93f4..de5d510cc 100644 --- a/crates/swapper/src/alien/reqwest_provider.rs +++ b/crates/swapper/src/alien/reqwest_provider.rs @@ -5,7 +5,8 @@ use primitives::{Chain, node_config::get_nodes_for_chain}; use async_trait::async_trait; use futures::TryFutureExt; -use gem_jsonrpc::{RpcProvider as GenericRpcProvider, RpcResponse}; +use gem_jsonrpc::RpcResponse; +use gem_jsonrpc::rpc::RpcProvider as GenericRpcProvider; use reqwest::Client; #[derive(Debug)] diff --git a/crates/swapper/src/client_factory.rs b/crates/swapper/src/client_factory.rs index 735401500..2bb0da571 100644 --- a/crates/swapper/src/client_factory.rs +++ b/crates/swapper/src/client_factory.rs @@ -1,23 +1,17 @@ -use gem_evm::rpc::EthereumClient; +use gem_evm::rpc::{self, EthereumClient}; +use gem_jsonrpc::alien::{self, RpcClient, RpcProvider}; use gem_jsonrpc::client::JsonRpcClient; -use primitives::{Chain, EVMChain}; +use primitives::Chain; use std::sync::Arc; -use crate::{ - SwapperError, - alien::{RpcClient, RpcProvider}, -}; +use crate::SwapperError; pub fn create_client_with_chain(provider: Arc, chain: Chain) -> JsonRpcClient { - let endpoint = provider.get_endpoint(chain).expect("Failed to get endpoint for chain"); - let client = RpcClient::new(endpoint, provider); - JsonRpcClient::new(client) + alien::create_client(provider, chain).expect("failed to create client for chain") } pub fn create_eth_client(provider: Arc, chain: Chain) -> Result, SwapperError> { - let evm_chain = EVMChain::from_chain(chain).ok_or(SwapperError::NotSupportedChain)?; - let client = create_client_with_chain(provider, chain); - Ok(EthereumClient::new(client, evm_chain)) + rpc::create_eth_client(provider, chain).ok_or(SwapperError::NotSupportedChain) } #[cfg(all(test, feature = "reqwest_provider", feature = "swap_integration_tests"))] diff --git a/gemstone/src/alien/provider.rs b/gemstone/src/alien/provider.rs index 8c907871a..996c25af4 100644 --- a/gemstone/src/alien/provider.rs +++ b/gemstone/src/alien/provider.rs @@ -1,7 +1,7 @@ use super::{AlienError, AlienResponse, AlienTarget}; use async_trait::async_trait; -use gem_jsonrpc::RpcProvider as GenericRpcProvider; +use gem_jsonrpc::rpc::RpcProvider as GenericRpcProvider; use primitives::Chain; use std::{fmt::Debug, sync::Arc}; From 95606786640f29d5c01f6c7df52456bdf6c3aabb Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:51:58 +0200 Subject: [PATCH 16/23] Refactor yielder crate architecture EarnProvider trait: merge deposit/withdraw into get_data, singularize get_provider/get_position to Option, move get_balance into trait, remove id() and get_asset_ids_for_chain. Yielder orchestrator: pure routing with no business logic, aggregates providers/positions into Vec, deduplicates balances across providers, routes get_data by provider_id from EarnType. YoEarnProvider: drop YoClient trait and HashMap>, use concrete YoGatewayClient created on demand via shared create_eth_client. Batch all chain assets in single multicall3 for get_balance. Cleanup: remove strum dependency, remove client_factory (use gem_evm shared), use concrete alien types (no generics), hardcode Yo APR at 4.9%, use biguint_to_u256 instead of string roundtrip, use .parse() over FromStr. Gateway FFI: get_earn_providers/get_earn_positions take single asset_id instead of chain + Vec. --- crates/yielder/src/client_factory.rs | 14 --------- crates/yielder/src/lib.rs | 1 - crates/yielder/src/yielder.rs | 28 ++++++++++++----- crates/yielder/src/yo/client.rs | 37 +++++++---------------- crates/yielder/src/yo/mapper.rs | 14 +++++---- crates/yielder/src/yo/provider.rs | 45 ++++++++++++++-------------- gemstone/src/gateway/mod.rs | 8 ++--- 7 files changed, 64 insertions(+), 83 deletions(-) delete mode 100644 crates/yielder/src/client_factory.rs diff --git a/crates/yielder/src/client_factory.rs b/crates/yielder/src/client_factory.rs deleted file mode 100644 index 0787baa93..000000000 --- a/crates/yielder/src/client_factory.rs +++ /dev/null @@ -1,14 +0,0 @@ -use gem_evm::rpc::EthereumClient; -use gem_jsonrpc::client::JsonRpcClient; -use gem_jsonrpc::{RpcClient, RpcClientError, RpcProvider}; -use primitives::{Chain, EVMChain}; -use std::sync::Arc; - -use crate::error::YielderError; - -pub fn create_eth_client(provider: Arc>, chain: Chain) -> Result>, YielderError> { - let evm_chain = EVMChain::from_chain(chain).ok_or_else(|| YielderError::unsupported_chain(&chain))?; - let endpoint = provider.get_endpoint(chain).map_err(|e| YielderError::NetworkError(e.to_string()))?; - let client = RpcClient::new(endpoint, provider); - Ok(EthereumClient::new(JsonRpcClient::new(client), evm_chain)) -} diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index eb5015a94..62fc009ee 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -1,4 +1,3 @@ -mod client_factory; mod error; mod provider; mod yielder; diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index 291bd637b..0853c53cd 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; use std::sync::Arc; -use gem_jsonrpc::{RpcClientError, RpcProvider}; +use gem_jsonrpc::alien::RpcProvider; +use num_bigint::BigUint; use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType}; use crate::error::YielderError; @@ -12,28 +14,38 @@ pub struct Yielder { } impl Yielder { - pub fn new(rpc_provider: Arc>) -> Self { + pub fn new(rpc_provider: Arc) -> Self { Self { providers: vec![Arc::new(YoEarnProvider::new(rpc_provider))], } } - pub fn get_provider(&self, asset_id: &AssetId) -> Option { - self.providers.iter().find_map(|p| p.get_provider(asset_id)) + pub fn get_providers(&self, asset_id: &AssetId) -> Vec { + self.providers.iter().filter_map(|p| p.get_provider(asset_id)).collect() } - pub async fn get_position(&self, address: &str, asset_id: &AssetId) -> Option { + pub async fn get_positions(&self, address: &str, asset_id: &AssetId) -> Vec { let futures: Vec<_> = self.providers.iter().map(|p| p.get_position(address, asset_id)).collect(); - futures::future::join_all(futures).await.into_iter().find_map(|r| r.ok().flatten()) + futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok().flatten()).collect() } pub async fn get_balance(&self, chain: Chain, address: &str) -> Vec { let futures: Vec<_> = self.providers.iter().map(|p| p.get_balance(chain, address)).collect(); - futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect() + let balances: HashMap = futures::future::join_all(futures) + .await + .into_iter() + .filter_map(|r| r.ok()) + .flatten() + .fold(HashMap::new(), |mut acc, b| { + *acc.entry(b.asset_id).or_default() += b.balance.earn; + acc + }); + balances.into_iter().map(|(id, earn)| AssetBalance::new_earn(id, earn)).collect() } pub async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { - let provider = self.providers.iter().find(|p| p.get_provider(asset_id).is_some()).ok_or_else(|| YielderError::unsupported_asset(asset_id))?; + let provider_id = earn_type.provider_id(); + let provider = self.providers.iter().find(|p| p.get_provider(asset_id).is_some_and(|v| v.id == provider_id)).ok_or_else(|| YielderError::unsupported_asset(asset_id))?; provider.get_data(asset_id, address, value, earn_type).await } } diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index 234bbdc57..c34f6243a 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -1,12 +1,11 @@ use alloy_primitives::{Address, U256}; use alloy_sol_types::SolCall; -use async_trait::async_trait; -use gem_client::Client; use gem_evm::contracts::erc4626::IERC4626; use gem_evm::contracts::IERC20; use gem_evm::jsonrpc::TransactionObject; use gem_evm::multicall3::{create_call3, decode_call3_return}; use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::alien::RpcClient; use primitives::swap::ApprovalData; use super::assets::YoAsset; @@ -19,35 +18,20 @@ pub struct PositionData { pub asset_balance: U256, } -#[async_trait] -pub trait YoClient: Send + Sync { - fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; - fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject; - async fn get_positions(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError>; - async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError>; - async fn get_quote_shares(&self, yo_token: Address, assets: U256) -> Result; -} - -pub struct YoGatewayClient { - ethereum_client: EthereumClient, +pub struct YoGatewayClient { + ethereum_client: EthereumClient, contract_address: Address, } -impl YoGatewayClient { - pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { +impl YoGatewayClient { + pub fn new(ethereum_client: EthereumClient, contract_address: Address) -> Self { Self { ethereum_client, contract_address, } } -} -#[async_trait] -impl YoClient for YoGatewayClient -where - C: Client + Clone + Send + Sync + 'static, -{ - fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { + pub fn build_deposit_transaction(&self, from: Address, yo_token: Address, assets: U256, min_shares_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { let data = IYoGateway::depositCall { yoVault: yo_token, assets, @@ -59,7 +43,7 @@ where TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { + pub fn build_redeem_transaction(&self, from: Address, yo_token: Address, shares: U256, min_assets_out: U256, receiver: Address, partner_id: u32) -> TransactionObject { let data = IYoGateway::redeemCall { yoVault: yo_token, shares, @@ -71,7 +55,7 @@ where TransactionObject::new_call_with_from(&from.to_string(), &self.contract_address.to_string(), data) } - async fn get_positions(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { + pub async fn get_positions(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { Ok(self .ethereum_client .multicall3_batch( @@ -97,7 +81,7 @@ where .await?) } - async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError> { + pub async fn check_token_allowance(&self, token: Address, owner: Address, amount: U256) -> Result, YielderError> { let spender = self.contract_address; let allowance = self.ethereum_client.call_contract(token, IERC20::allowanceCall { owner, spender }).await?; @@ -112,7 +96,7 @@ where } } - async fn get_quote_shares(&self, yo_token: Address, assets: U256) -> Result { + pub async fn get_quote_shares(&self, yo_token: Address, assets: U256) -> Result { let call = IYoGateway::quoteConvertToSharesCall { yoVault: yo_token, assets }; Ok(self.ethereum_client.call_contract(self.contract_address, call).await?) } @@ -142,4 +126,3 @@ mod tests { assert_eq!(convert_to_assets_ceil(U256::ZERO, U256::ZERO, U256::ZERO), U256::ZERO); } } - diff --git a/crates/yielder/src/yo/mapper.rs b/crates/yielder/src/yo/mapper.rs index 3776af0f6..973cda48c 100644 --- a/crates/yielder/src/yo/mapper.rs +++ b/crates/yielder/src/yo/mapper.rs @@ -1,6 +1,6 @@ use gem_evm::u256::u256_to_biguint; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType}; use super::client::PositionData; @@ -17,14 +17,16 @@ pub fn map_to_delegation(asset_id: AssetId, data: &PositionData, provider_id: &s } } -pub fn map_to_earn_provider(chain: Chain, provider: YieldProvider) -> DelegationValidator { +const YO_APR: f64 = 4.9; + +pub fn map_to_earn_provider(chain: Chain, provider_id: &str) -> DelegationValidator { DelegationValidator { chain, - id: provider.to_string(), - name: provider.to_string(), + id: provider_id.to_string(), + name: provider_id.to_string(), is_active: true, commission: 0.0, - apr: 0.0, + apr: YO_APR, provider_type: StakeProviderType::Earn, } } @@ -51,7 +53,7 @@ mod tests { #[test] fn test_map_to_earn_provider() { - let result = map_to_earn_provider(Chain::Base, YieldProvider::Yo); + let result = map_to_earn_provider(Chain::Base, "yo"); assert_eq!(result.id, "yo"); assert_eq!(result.chain, Chain::Base); diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 2e99960c1..398cfab8f 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::sync::Arc; use alloy_primitives::{Address, U256}; @@ -6,34 +5,30 @@ use async_trait::async_trait; use num_bigint::BigUint; use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; +use gem_evm::rpc::create_eth_client; use gem_evm::slippage::apply_slippage_in_bp; -use gem_evm::u256::biguint_to_u256; +use gem_evm::u256::{biguint_to_u256, u256_to_biguint}; +use gem_jsonrpc::alien::RpcProvider; -use crate::client_factory::create_eth_client; use crate::error::YielderError; use crate::provider::EarnProvider; -use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, client::{YoClient, YoGatewayClient}, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; +use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, client::YoGatewayClient, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; const GAS_LIMIT: u64 = 300_000; const SLIPPAGE_BPS: u32 = 50; pub struct YoEarnProvider { assets: &'static [YoAsset], - clients: HashMap>, + rpc_provider: Arc, } impl YoEarnProvider { - pub fn new(rpc_provider: Arc>) -> Self { - let assets = supported_assets(); - let clients = assets - .iter() - .filter_map(|asset| { - let client = create_eth_client(rpc_provider.clone(), asset.chain).ok()?; - Some((asset.chain, Arc::new(YoGatewayClient::new(client, YO_GATEWAY)) as Arc)) - }) - .collect(); - Self { assets, clients } + pub fn new(rpc_provider: Arc) -> Self { + Self { + assets: supported_assets(), + rpc_provider, + } } fn get_asset(&self, asset_id: &AssetId) -> Result { @@ -44,8 +39,9 @@ impl YoEarnProvider { .ok_or_else(|| YielderError::unsupported_asset(asset_id)) } - fn get_client(&self, chain: Chain) -> Result<&Arc, YielderError> { - self.clients.get(&chain).ok_or_else(|| YielderError::unsupported_chain(&chain)) + fn get_client(&self, chain: Chain) -> Result { + let client = create_eth_client(self.rpc_provider.clone(), chain).ok_or_else(|| YielderError::unsupported_chain(&chain))?; + Ok(YoGatewayClient::new(client, YO_GATEWAY)) } async fn get_position_for_asset(&self, address: &str, asset: &YoAsset) -> Result, YielderError> { @@ -60,7 +56,7 @@ impl YoEarnProvider { #[async_trait] impl EarnProvider for YoEarnProvider { fn get_provider(&self, asset_id: &AssetId) -> Option { - self.assets.iter().find(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)) + self.assets.iter().find(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo.as_ref())) } async fn get_position(&self, address: &str, asset_id: &AssetId) -> Result, YielderError> { @@ -69,13 +65,16 @@ impl EarnProvider for YoEarnProvider { } async fn get_balance(&self, chain: Chain, address: &str) -> Result, YielderError> { - let chain_assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain).collect(); - let futures: Vec<_> = chain_assets.iter().map(|a| self.get_position_for_asset(address, a)).collect(); + let chain_assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain).copied().collect(); + let client = self.get_client(chain)?; + let owner: Address = address.parse()?; + let positions = client.get_positions(&chain_assets, owner).await?; + Ok(chain_assets .iter() - .zip(futures::future::join_all(futures).await) - .map(|(asset, position)| { - let balance = position.ok().flatten().map(|p| p.balance).unwrap_or(BigUint::ZERO); + .zip(positions) + .map(|(asset, data)| { + let balance = if data.share_balance != U256::ZERO { u256_to_biguint(&data.asset_balance) } else { BigUint::ZERO }; AssetBalance::new_earn(asset.asset_id(), balance) }) .collect()) diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 0836b12ba..a7e74ebf2 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -312,12 +312,12 @@ impl GemGateway { self.yielder.get_data(&asset_id, &address, &value, &earn_type).await.map_err(|e| GatewayError::NetworkError { msg: e.to_string() }) } - pub fn get_earn_provider(&self, asset_id: AssetId) -> Option { - self.yielder.get_provider(&asset_id) + pub fn get_earn_providers(&self, asset_id: AssetId) -> Vec { + self.yielder.get_providers(&asset_id) } - pub async fn get_earn_position(&self, address: String, asset_id: AssetId) -> Result, GatewayError> { - Ok(self.yielder.get_position(&address, &asset_id).await) + pub async fn get_earn_positions(&self, address: String, asset_id: AssetId) -> Vec { + self.yielder.get_positions(&address, &asset_id).await } pub async fn get_node_status(&self, chain: Chain, url: &str) -> Result { From 128f77d776b20de49361882e83d65a4992c94d2b Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:36:36 +0200 Subject: [PATCH 17/23] Added name to earn provider --- crates/primitives/src/yield_provider.rs | 8 ++++++++ crates/yielder/src/yo/mapper.rs | 11 ++++++----- crates/yielder/src/yo/provider.rs | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/primitives/src/yield_provider.rs b/crates/primitives/src/yield_provider.rs index b9dab7b30..a414dd524 100644 --- a/crates/primitives/src/yield_provider.rs +++ b/crates/primitives/src/yield_provider.rs @@ -9,3 +9,11 @@ use typeshare::typeshare; pub enum YieldProvider { Yo, } + +impl YieldProvider { + pub fn name(&self) -> &str { + match self { + Self::Yo => "Yo", + } + } +} diff --git a/crates/yielder/src/yo/mapper.rs b/crates/yielder/src/yo/mapper.rs index 973cda48c..606d482dc 100644 --- a/crates/yielder/src/yo/mapper.rs +++ b/crates/yielder/src/yo/mapper.rs @@ -1,6 +1,6 @@ use gem_evm::u256::u256_to_biguint; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType}; +use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; use super::client::PositionData; @@ -19,11 +19,11 @@ pub fn map_to_delegation(asset_id: AssetId, data: &PositionData, provider_id: &s const YO_APR: f64 = 4.9; -pub fn map_to_earn_provider(chain: Chain, provider_id: &str) -> DelegationValidator { +pub fn map_to_earn_provider(chain: Chain, provider: YieldProvider) -> DelegationValidator { DelegationValidator { chain, - id: provider_id.to_string(), - name: provider_id.to_string(), + id: provider.as_ref().to_string(), + name: provider.name().to_string(), is_active: true, commission: 0.0, apr: YO_APR, @@ -53,9 +53,10 @@ mod tests { #[test] fn test_map_to_earn_provider() { - let result = map_to_earn_provider(Chain::Base, "yo"); + let result = map_to_earn_provider(Chain::Base, YieldProvider::Yo); assert_eq!(result.id, "yo"); + assert_eq!(result.name, "Yo"); assert_eq!(result.chain, Chain::Base); assert_eq!(result.provider_type, StakeProviderType::Earn); } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 398cfab8f..582ad49fa 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -56,7 +56,7 @@ impl YoEarnProvider { #[async_trait] impl EarnProvider for YoEarnProvider { fn get_provider(&self, asset_id: &AssetId) -> Option { - self.assets.iter().find(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo.as_ref())) + self.assets.iter().find(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)) } async fn get_position(&self, address: &str, asset_id: &AssetId) -> Result, YielderError> { From dfc31975cfed6846d9192d0cbd0ae5c0c663c16a Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:08:54 +0200 Subject: [PATCH 18/23] Pass token IDs to earn balance fetch to skip unnecessary RPC calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change get_balance(chain, address) to get_balance(chain, address, token_ids). Provider matches user's token IDs against supported earn assets with case-insensitive comparison. No matching tokens → early return, zero RPC calls. --- crates/yielder/src/provider.rs | 2 +- crates/yielder/src/yielder.rs | 4 ++-- crates/yielder/src/yo/provider.rs | 12 ++++++++---- gemstone/src/gateway/mod.rs | 4 ++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/yielder/src/provider.rs b/crates/yielder/src/provider.rs index f47aff278..18c76e725 100644 --- a/crates/yielder/src/provider.rs +++ b/crates/yielder/src/provider.rs @@ -8,6 +8,6 @@ pub trait EarnProvider: Send + Sync { fn get_provider(&self, asset_id: &AssetId) -> Option; async fn get_position(&self, address: &str, asset_id: &AssetId) -> Result, YielderError>; - async fn get_balance(&self, chain: Chain, address: &str) -> Result, YielderError>; + async fn get_balance(&self, chain: Chain, address: &str, token_ids: &[String]) -> Result, YielderError>; async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result; } diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index 0853c53cd..32bf12609 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -29,8 +29,8 @@ impl Yielder { futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok().flatten()).collect() } - pub async fn get_balance(&self, chain: Chain, address: &str) -> Vec { - let futures: Vec<_> = self.providers.iter().map(|p| p.get_balance(chain, address)).collect(); + pub async fn get_balance(&self, chain: Chain, address: &str, token_ids: &[String]) -> Vec { + let futures: Vec<_> = self.providers.iter().map(|p| p.get_balance(chain, address, token_ids)).collect(); let balances: HashMap = futures::future::join_all(futures) .await .into_iter() diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 582ad49fa..c41c1c390 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -64,13 +64,17 @@ impl EarnProvider for YoEarnProvider { self.get_position_for_asset(address, &asset).await } - async fn get_balance(&self, chain: Chain, address: &str) -> Result, YielderError> { - let chain_assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain).copied().collect(); + async fn get_balance(&self, chain: Chain, address: &str, token_ids: &[String]) -> Result, YielderError> { + let token_match = |a: &&YoAsset| token_ids.iter().any(|t| a.asset_token.to_string().eq_ignore_ascii_case(t)); + let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain).filter(token_match).copied().collect(); + if assets.is_empty() { + return Ok(vec![]); + } let client = self.get_client(chain)?; let owner: Address = address.parse()?; - let positions = client.get_positions(&chain_assets, owner).await?; + let positions = client.get_positions(&assets, owner).await?; - Ok(chain_assets + Ok(assets .iter() .zip(positions) .map(|(asset, data)| { diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index a7e74ebf2..3dc35ca95 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -304,8 +304,8 @@ impl GemGateway { Ok(self.provider(chain).await?.get_is_token_address(&token_id)) } - pub async fn get_balance_earn(&self, chain: Chain, address: String) -> Result, GatewayError> { - Ok(self.yielder.get_balance(chain, &address).await) + pub async fn get_balance_earn(&self, chain: Chain, address: String, token_ids: Vec) -> Result, GatewayError> { + Ok(self.yielder.get_balance(chain, &address, &token_ids).await) } pub async fn get_earn_data(&self, asset_id: AssetId, address: String, value: String, earn_type: GemEarnType) -> Result { From 5341ab4032443753d0a80b314db201ca689448fb Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:12:50 +0200 Subject: [PATCH 19/23] Refactor yielder crate for clarity and testability - Extract mapping functions to mapper.rs (map_to_asset_balance, map_to_contract_call_data) - Add get_positions helper to reduce duplication in YoEarnProvider - Reuse get_asset in get_provider to avoid duplicate find logic - Remove hardcoded YO_APR, use 0.0 (iOS reads APR from DB) - Fix token comparison to use direct equality instead of case-insensitive - Add tests for map_to_asset_balance and map_to_contract_call_data --- crates/yielder/src/yielder.rs | 18 ++++++---- crates/yielder/src/yo/mapper.rs | 42 +++++++++++++++++++++-- crates/yielder/src/yo/provider.rs | 56 ++++++++++++++----------------- 3 files changed, 76 insertions(+), 40 deletions(-) diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index 32bf12609..b62ae2b7f 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -31,16 +31,20 @@ impl Yielder { pub async fn get_balance(&self, chain: Chain, address: &str, token_ids: &[String]) -> Vec { let futures: Vec<_> = self.providers.iter().map(|p| p.get_balance(chain, address, token_ids)).collect(); - let balances: HashMap = futures::future::join_all(futures) - .await + let balances = futures::future::join_all(futures).await.into_iter().filter_map(|r| r.ok()).flatten().collect(); + Self::map_earn_balances(balances) + } + + fn map_earn_balances(balances: Vec) -> Vec { + balances .into_iter() - .filter_map(|r| r.ok()) - .flatten() - .fold(HashMap::new(), |mut acc, b| { + .fold(HashMap::::new(), |mut acc, b| { *acc.entry(b.asset_id).or_default() += b.balance.earn; acc - }); - balances.into_iter().map(|(id, earn)| AssetBalance::new_earn(id, earn)).collect() + }) + .into_iter() + .map(|(id, earn)| AssetBalance::new_earn(id, earn)) + .collect() } pub async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { diff --git a/crates/yielder/src/yo/mapper.rs b/crates/yielder/src/yo/mapper.rs index 606d482dc..bbb6fb49a 100644 --- a/crates/yielder/src/yo/mapper.rs +++ b/crates/yielder/src/yo/mapper.rs @@ -1,7 +1,11 @@ +use alloy_primitives::U256; +use gem_evm::jsonrpc::TransactionObject; use gem_evm::u256::u256_to_biguint; use num_bigint::BigUint; -use primitives::{AssetId, Chain, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; +use primitives::swap::ApprovalData; +use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationState, DelegationValidator, StakeProviderType, YieldProvider}; +use super::assets::YoAsset; use super::client::PositionData; pub fn map_to_delegation(asset_id: AssetId, data: &PositionData, provider_id: &str) -> DelegationBase { @@ -17,7 +21,23 @@ pub fn map_to_delegation(asset_id: AssetId, data: &PositionData, provider_id: &s } } -const YO_APR: f64 = 4.9; +pub fn map_to_asset_balance(asset: &YoAsset, data: &PositionData) -> AssetBalance { + let balance = if data.share_balance != U256::ZERO { + u256_to_biguint(&data.asset_balance) + } else { + BigUint::ZERO + }; + AssetBalance::new_earn(asset.asset_id(), balance) +} + +pub fn map_to_contract_call_data(transaction: TransactionObject, approval: Option, gas_limit: u64) -> ContractCallData { + ContractCallData { + contract_address: transaction.to, + call_data: transaction.data, + approval, + gas_limit: Some(gas_limit.to_string()), + } +} pub fn map_to_earn_provider(chain: Chain, provider: YieldProvider) -> DelegationValidator { DelegationValidator { @@ -26,7 +46,7 @@ pub fn map_to_earn_provider(chain: Chain, provider: YieldProvider) -> Delegation name: provider.name().to_string(), is_active: true, commission: 0.0, - apr: YO_APR, + apr: 0.0, provider_type: StakeProviderType::Earn, } } @@ -36,6 +56,7 @@ mod tests { use super::*; use super::super::assets::YO_USDC; use alloy_primitives::U256; + use primitives::AssetId; #[test] fn test_map_to_delegation() { @@ -58,6 +79,21 @@ mod tests { assert_eq!(result.id, "yo"); assert_eq!(result.name, "Yo"); assert_eq!(result.chain, Chain::Base); + assert_eq!(result.apr, 0.0); assert_eq!(result.provider_type, StakeProviderType::Earn); } + + #[test] + fn test_map_to_asset_balance() { + assert_eq!(map_to_asset_balance(&YO_USDC, &PositionData { share_balance: U256::from(1_000_000), asset_balance: U256::from(1_050_000) }).balance.earn, BigUint::from(1_050_000u64)); + assert_eq!(map_to_asset_balance(&YO_USDC, &PositionData { share_balance: U256::ZERO, asset_balance: U256::from(1_050_000) }).balance.earn, BigUint::ZERO); + } + + #[test] + fn test_map_to_contract_call_data() { + let result = map_to_contract_call_data(TransactionObject { to: "0xcontract".to_string(), data: "0xcalldata".to_string() }, None, 300_000); + assert_eq!(result.contract_address, "0xcontract"); + assert_eq!(result.call_data, "0xcalldata"); + assert_eq!(result.gas_limit, Some("300000".to_string())); + } } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index c41c1c390..56d06ba4a 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -1,19 +1,19 @@ +use std::slice::from_ref; use std::sync::Arc; use alloy_primitives::{Address, U256}; use async_trait::async_trait; -use num_bigint::BigUint; use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; use gem_evm::rpc::create_eth_client; use gem_evm::slippage::apply_slippage_in_bp; -use gem_evm::u256::{biguint_to_u256, u256_to_biguint}; +use gem_evm::u256::biguint_to_u256; use gem_jsonrpc::alien::RpcProvider; use crate::error::YielderError; use crate::provider::EarnProvider; -use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, client::YoGatewayClient, mapper::{map_to_delegation, map_to_earn_provider}, supported_assets}; +use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, client::YoGatewayClient, mapper::{map_to_asset_balance, map_to_contract_call_data, map_to_delegation, map_to_earn_provider}, supported_assets}; const GAS_LIMIT: u64 = 300_000; const SLIPPAGE_BPS: u32 = 50; @@ -31,6 +31,14 @@ impl YoEarnProvider { } } + fn get_assets(&self, chain: Chain, token_ids: &[String]) -> Vec { + self.assets + .iter() + .filter(|a| a.chain == chain && token_ids.contains(&a.asset_token.to_string())) + .copied() + .collect() + } + fn get_asset(&self, asset_id: &AssetId) -> Result { self.assets .iter() @@ -44,44 +52,37 @@ impl YoEarnProvider { Ok(YoGatewayClient::new(client, YO_GATEWAY)) } - async fn get_position_for_asset(&self, address: &str, asset: &YoAsset) -> Result, YielderError> { - let client = self.get_client(asset.chain)?; + async fn get_positions(&self, chain: Chain, address: &str, assets: &[YoAsset]) -> Result, YielderError> { + let client = self.get_client(chain)?; let owner: Address = address.parse()?; - let positions = client.get_positions(&[*asset], owner).await?; - - Ok(positions.into_iter().find(|d| d.share_balance != U256::ZERO).map(|data| map_to_delegation(asset.asset_id(), &data, YieldProvider::Yo.as_ref()))) + client.get_positions(assets, owner).await } } #[async_trait] impl EarnProvider for YoEarnProvider { fn get_provider(&self, asset_id: &AssetId) -> Option { - self.assets.iter().find(|a| a.asset_id() == *asset_id).map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)) + self.get_asset(asset_id).ok().map(|a| map_to_earn_provider(a.chain, YieldProvider::Yo)) } async fn get_position(&self, address: &str, asset_id: &AssetId) -> Result, YielderError> { let asset = self.get_asset(asset_id)?; - self.get_position_for_asset(address, &asset).await + let positions = self.get_positions(asset.chain, address, from_ref(&asset)).await?; + let delegation = positions + .into_iter() + .find(|d| d.share_balance != U256::ZERO) + .map(|data| map_to_delegation(asset.asset_id(), &data, YieldProvider::Yo.as_ref())); + Ok(delegation) } async fn get_balance(&self, chain: Chain, address: &str, token_ids: &[String]) -> Result, YielderError> { - let token_match = |a: &&YoAsset| token_ids.iter().any(|t| a.asset_token.to_string().eq_ignore_ascii_case(t)); - let assets: Vec<_> = self.assets.iter().filter(|a| a.chain == chain).filter(token_match).copied().collect(); + let assets = self.get_assets(chain, token_ids); if assets.is_empty() { return Ok(vec![]); } - let client = self.get_client(chain)?; - let owner: Address = address.parse()?; - let positions = client.get_positions(&assets, owner).await?; - - Ok(assets - .iter() - .zip(positions) - .map(|(asset, data)| { - let balance = if data.share_balance != U256::ZERO { u256_to_biguint(&data.asset_balance) } else { BigUint::ZERO }; - AssetBalance::new_earn(asset.asset_id(), balance) - }) - .collect()) + let positions = self.get_positions(chain, address, &assets).await?; + let balances = assets.iter().zip(positions).map(|(asset, data)| map_to_asset_balance(asset, &data)).collect(); + Ok(balances) } async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { @@ -113,11 +114,6 @@ impl EarnProvider for YoEarnProvider { } }; - Ok(ContractCallData { - contract_address: transaction.to, - call_data: transaction.data, - approval, - gas_limit: Some(GAS_LIMIT.to_string()), - }) + Ok(map_to_contract_call_data(transaction, approval, GAS_LIMIT)) } } From e697c6ddb36b2257d87d14113d1a158fd9c4d055 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:44:35 +0200 Subject: [PATCH 20/23] Refactor EVM client factory: remove Option, add yielder client_factory - Delete gem_evm client_factory (returned Option causing double conversion) - Inline EVM client creation in swapper with proper Result types - Add yielder/src/client_factory.rs mirroring swapper pattern - Add YielderError::NotSupportedChain unit variant - Fix import grouping in yo/provider.rs and yo/mapper.rs --- crates/gem_evm/src/rpc/client_factory.rs | 11 ----------- crates/gem_evm/src/rpc/mod.rs | 3 --- crates/swapper/src/client_factory.rs | 8 +++++--- crates/yielder/src/client_factory.rs | 17 +++++++++++++++++ crates/yielder/src/error.rs | 6 ++---- crates/yielder/src/lib.rs | 1 + crates/yielder/src/yo/mapper.rs | 7 ++----- crates/yielder/src/yo/provider.rs | 11 ++++++----- 8 files changed, 33 insertions(+), 31 deletions(-) delete mode 100644 crates/gem_evm/src/rpc/client_factory.rs create mode 100644 crates/yielder/src/client_factory.rs diff --git a/crates/gem_evm/src/rpc/client_factory.rs b/crates/gem_evm/src/rpc/client_factory.rs deleted file mode 100644 index e6a7cfb90..000000000 --- a/crates/gem_evm/src/rpc/client_factory.rs +++ /dev/null @@ -1,11 +0,0 @@ -use gem_jsonrpc::alien::{self, RpcClient, RpcProvider}; -use primitives::{Chain, EVMChain}; -use std::sync::Arc; - -use super::EthereumClient; - -pub fn create_eth_client(provider: Arc, chain: Chain) -> Option> { - let evm_chain = EVMChain::from_chain(chain)?; - let client = alien::create_client(provider, chain).ok()?; - Some(EthereumClient::new(client, evm_chain)) -} diff --git a/crates/gem_evm/src/rpc/mod.rs b/crates/gem_evm/src/rpc/mod.rs index 423e06953..a911bdad7 100644 --- a/crates/gem_evm/src/rpc/mod.rs +++ b/crates/gem_evm/src/rpc/mod.rs @@ -6,9 +6,6 @@ pub mod model; pub mod staking_mapper; pub mod swap_mapper; -mod client_factory; - pub use client::EthereumClient; -pub use client_factory::create_eth_client; pub use mapper::EthereumMapper; pub use staking_mapper::StakingMapper; diff --git a/crates/swapper/src/client_factory.rs b/crates/swapper/src/client_factory.rs index 2bb0da571..ff7555f2b 100644 --- a/crates/swapper/src/client_factory.rs +++ b/crates/swapper/src/client_factory.rs @@ -1,7 +1,7 @@ -use gem_evm::rpc::{self, EthereumClient}; +use gem_evm::rpc::EthereumClient; use gem_jsonrpc::alien::{self, RpcClient, RpcProvider}; use gem_jsonrpc::client::JsonRpcClient; -use primitives::Chain; +use primitives::{Chain, EVMChain}; use std::sync::Arc; use crate::SwapperError; @@ -11,7 +11,9 @@ pub fn create_client_with_chain(provider: Arc, chain: Chain) -> } pub fn create_eth_client(provider: Arc, chain: Chain) -> Result, SwapperError> { - rpc::create_eth_client(provider, chain).ok_or(SwapperError::NotSupportedChain) + let evm_chain = EVMChain::from_chain(chain).ok_or(SwapperError::NotSupportedChain)?; + let client = alien::create_client(provider, chain).map_err(|_| SwapperError::NotSupportedChain)?; + Ok(EthereumClient::new(client, evm_chain)) } #[cfg(all(test, feature = "reqwest_provider", feature = "swap_integration_tests"))] diff --git a/crates/yielder/src/client_factory.rs b/crates/yielder/src/client_factory.rs new file mode 100644 index 000000000..a3da2fa71 --- /dev/null +++ b/crates/yielder/src/client_factory.rs @@ -0,0 +1,17 @@ +use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::alien::{self, RpcClient, RpcProvider}; +use gem_jsonrpc::client::JsonRpcClient; +use primitives::{Chain, EVMChain}; +use std::sync::Arc; + +use crate::YielderError; + +pub fn create_client(provider: Arc, chain: Chain) -> Result, YielderError> { + alien::create_client(provider, chain).map_err(|_| YielderError::NotSupportedChain) +} + +pub fn create_eth_client(provider: Arc, chain: Chain) -> Result, YielderError> { + let evm_chain = EVMChain::from_chain(chain).ok_or(YielderError::NotSupportedChain)?; + let client = create_client(provider, chain)?; + Ok(EthereumClient::new(client, evm_chain)) +} diff --git a/crates/yielder/src/error.rs b/crates/yielder/src/error.rs index 094fd049f..f5a8f0018 100644 --- a/crates/yielder/src/error.rs +++ b/crates/yielder/src/error.rs @@ -7,22 +7,20 @@ use alloy_primitives::ruint::ParseError; #[derive(Debug, Clone)] pub enum YielderError { NetworkError(String), + NotSupportedChain, } impl YielderError { pub fn unsupported_asset(asset: &impl Display) -> Self { Self::NetworkError(format!("Unsupported asset: {asset}")) } - - pub fn unsupported_chain(chain: &impl Display) -> Self { - Self::NetworkError(format!("Unsupported chain: {chain}")) - } } impl fmt::Display for YielderError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Self::NetworkError(msg) => write!(f, "{msg}"), + Self::NotSupportedChain => write!(f, "Not supported chain"), } } } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index 62fc009ee..eb5015a94 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -1,3 +1,4 @@ +mod client_factory; mod error; mod provider; mod yielder; diff --git a/crates/yielder/src/yo/mapper.rs b/crates/yielder/src/yo/mapper.rs index bbb6fb49a..1981b65b0 100644 --- a/crates/yielder/src/yo/mapper.rs +++ b/crates/yielder/src/yo/mapper.rs @@ -54,9 +54,7 @@ pub fn map_to_earn_provider(chain: Chain, provider: YieldProvider) -> Delegation #[cfg(test)] mod tests { use super::*; - use super::super::assets::YO_USDC; - use alloy_primitives::U256; - use primitives::AssetId; + use crate::yo::assets::YO_USDC; #[test] fn test_map_to_delegation() { @@ -91,9 +89,8 @@ mod tests { #[test] fn test_map_to_contract_call_data() { - let result = map_to_contract_call_data(TransactionObject { to: "0xcontract".to_string(), data: "0xcalldata".to_string() }, None, 300_000); + let result = map_to_contract_call_data(TransactionObject::new_call("0xcontract", vec![]), None, 300_000); assert_eq!(result.contract_address, "0xcontract"); - assert_eq!(result.call_data, "0xcalldata"); assert_eq!(result.gas_limit, Some("300000".to_string())); } } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 56d06ba4a..385145356 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -3,17 +3,18 @@ use std::sync::Arc; use alloy_primitives::{Address, U256}; use async_trait::async_trait; -use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; - -use gem_evm::rpc::create_eth_client; use gem_evm::slippage::apply_slippage_in_bp; use gem_evm::u256::biguint_to_u256; use gem_jsonrpc::alien::RpcProvider; +use primitives::{AssetBalance, AssetId, Chain, ContractCallData, DelegationBase, DelegationValidator, EarnType, YieldProvider}; +use crate::client_factory::create_eth_client; use crate::error::YielderError; use crate::provider::EarnProvider; -use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, client::YoGatewayClient, mapper::{map_to_asset_balance, map_to_contract_call_data, map_to_delegation, map_to_earn_provider}, supported_assets}; +use super::client::YoGatewayClient; +use super::mapper::{map_to_asset_balance, map_to_contract_call_data, map_to_delegation, map_to_earn_provider}; +use super::{YO_GATEWAY, YO_PARTNER_ID_GEM, YoAsset, supported_assets}; const GAS_LIMIT: u64 = 300_000; const SLIPPAGE_BPS: u32 = 50; @@ -48,7 +49,7 @@ impl YoEarnProvider { } fn get_client(&self, chain: Chain) -> Result { - let client = create_eth_client(self.rpc_provider.clone(), chain).ok_or_else(|| YielderError::unsupported_chain(&chain))?; + let client = create_eth_client(self.rpc_provider.clone(), chain)?; Ok(YoGatewayClient::new(client, YO_GATEWAY)) } From 37d30ef9bbd470bca4f80ed06889a1a652a7509a Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:06:29 +0200 Subject: [PATCH 21/23] Improve Yielder and AlienProviderWrapper design - Add Yielder::with_providers for testability without real RPC - Extract get_provider(asset_id, provider_id) routing in Yielder::get_data - Add YielderError::NotSupportedAsset unit variant - Export EarnProvider trait from yielder crate - Add AlienProviderWrapper::new constructor, make field private --- crates/yielder/src/error.rs | 8 ++------ crates/yielder/src/lib.rs | 1 + crates/yielder/src/yielder.rs | 22 ++++++++++++++-------- crates/yielder/src/yo/provider.rs | 2 +- gemstone/src/alien/client.rs | 3 +-- gemstone/src/alien/provider.rs | 8 +++++++- gemstone/src/gateway/mod.rs | 3 +-- gemstone/src/gem_swapper/mod.rs | 3 +-- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/crates/yielder/src/error.rs b/crates/yielder/src/error.rs index f5a8f0018..70c5e1a89 100644 --- a/crates/yielder/src/error.rs +++ b/crates/yielder/src/error.rs @@ -8,12 +8,7 @@ use alloy_primitives::ruint::ParseError; pub enum YielderError { NetworkError(String), NotSupportedChain, -} - -impl YielderError { - pub fn unsupported_asset(asset: &impl Display) -> Self { - Self::NetworkError(format!("Unsupported asset: {asset}")) - } + NotSupportedAsset, } impl fmt::Display for YielderError { @@ -21,6 +16,7 @@ impl fmt::Display for YielderError { match self { Self::NetworkError(msg) => write!(f, "{msg}"), Self::NotSupportedChain => write!(f, "Not supported chain"), + Self::NotSupportedAsset => write!(f, "Not supported asset"), } } } diff --git a/crates/yielder/src/lib.rs b/crates/yielder/src/lib.rs index eb5015a94..1b93dee11 100644 --- a/crates/yielder/src/lib.rs +++ b/crates/yielder/src/lib.rs @@ -5,4 +5,5 @@ mod yielder; mod yo; pub use error::YielderError; +pub use provider::EarnProvider; pub use yielder::Yielder; diff --git a/crates/yielder/src/yielder.rs b/crates/yielder/src/yielder.rs index b62ae2b7f..363506ddf 100644 --- a/crates/yielder/src/yielder.rs +++ b/crates/yielder/src/yielder.rs @@ -15,9 +15,11 @@ pub struct Yielder { impl Yielder { pub fn new(rpc_provider: Arc) -> Self { - Self { - providers: vec![Arc::new(YoEarnProvider::new(rpc_provider))], - } + Self::with_providers(vec![Arc::new(YoEarnProvider::new(rpc_provider))]) + } + + pub fn with_providers(providers: Vec>) -> Self { + Self { providers } } pub fn get_providers(&self, asset_id: &AssetId) -> Vec { @@ -35,6 +37,15 @@ impl Yielder { Self::map_earn_balances(balances) } + pub async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { + self.providers + .iter() + .find(|p| p.get_provider(asset_id).is_some_and(|v| v.id == earn_type.provider_id())) + .ok_or(YielderError::NotSupportedAsset)? + .get_data(asset_id, address, value, earn_type) + .await + } + fn map_earn_balances(balances: Vec) -> Vec { balances .into_iter() @@ -47,9 +58,4 @@ impl Yielder { .collect() } - pub async fn get_data(&self, asset_id: &AssetId, address: &str, value: &str, earn_type: &EarnType) -> Result { - let provider_id = earn_type.provider_id(); - let provider = self.providers.iter().find(|p| p.get_provider(asset_id).is_some_and(|v| v.id == provider_id)).ok_or_else(|| YielderError::unsupported_asset(asset_id))?; - provider.get_data(asset_id, address, value, earn_type).await - } } diff --git a/crates/yielder/src/yo/provider.rs b/crates/yielder/src/yo/provider.rs index 385145356..831792348 100644 --- a/crates/yielder/src/yo/provider.rs +++ b/crates/yielder/src/yo/provider.rs @@ -45,7 +45,7 @@ impl YoEarnProvider { .iter() .find(|a| a.asset_id() == *asset_id) .copied() - .ok_or_else(|| YielderError::unsupported_asset(asset_id)) + .ok_or(YielderError::NotSupportedAsset) } fn get_client(&self, chain: Chain) -> Result { diff --git a/gemstone/src/alien/client.rs b/gemstone/src/alien/client.rs index 9e435af66..bd6f972d5 100644 --- a/gemstone/src/alien/client.rs +++ b/gemstone/src/alien/client.rs @@ -6,6 +6,5 @@ use swapper::{RpcClient, RpcProvider}; pub type AlienClient = RpcClient; pub fn new_alien_client(base_url: String, provider: Arc) -> AlienClient { - let wrapper: Arc = Arc::new(AlienProviderWrapper { provider }); - RpcClient::new(base_url, wrapper) + RpcClient::new(base_url, Arc::new(AlienProviderWrapper::new(provider))) } diff --git a/gemstone/src/alien/provider.rs b/gemstone/src/alien/provider.rs index 996c25af4..eb872b6fb 100644 --- a/gemstone/src/alien/provider.rs +++ b/gemstone/src/alien/provider.rs @@ -14,7 +14,13 @@ pub trait AlienProvider: Send + Sync + Debug { #[derive(Debug)] pub struct AlienProviderWrapper { - pub provider: Arc, + provider: Arc, +} + +impl AlienProviderWrapper { + pub fn new(provider: Arc) -> Self { + Self { provider } + } } #[async_trait] diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index 3dc35ca95..2a58df5eb 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -153,8 +153,7 @@ impl GemGateway { #[uniffi::constructor] pub fn new(provider: Arc, preferences: Arc, secure_preferences: Arc, api_url: String) -> Self { let api_client = GemApiClient::new(api_url, provider.clone()); - let wrapper = AlienProviderWrapper { provider: provider.clone() }; - let yielder = Yielder::new(Arc::new(wrapper)); + let yielder = Yielder::new(Arc::new(AlienProviderWrapper::new(provider.clone()))); Self { provider, preferences, diff --git a/gemstone/src/gem_swapper/mod.rs b/gemstone/src/gem_swapper/mod.rs index 4988c353f..1cab80ce1 100644 --- a/gemstone/src/gem_swapper/mod.rs +++ b/gemstone/src/gem_swapper/mod.rs @@ -19,9 +19,8 @@ pub struct GemSwapper { impl GemSwapper { #[uniffi::constructor] pub fn new(rpc_provider: Arc) -> Self { - let wrapper = AlienProviderWrapper { provider: rpc_provider }; Self { - inner: Swapper::new(Arc::new(wrapper)), + inner: Swapper::new(Arc::new(AlienProviderWrapper::new(rpc_provider))), } } From 4c5eec2304b7ce6bfb2d714058a1fc80b666f818 Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:16:10 +0200 Subject: [PATCH 22/23] Removed dead code --- crates/yielder/src/error.rs | 2 +- gemstone/src/alien/client.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/yielder/src/error.rs b/crates/yielder/src/error.rs index 70c5e1a89..159bb10d3 100644 --- a/crates/yielder/src/error.rs +++ b/crates/yielder/src/error.rs @@ -1,5 +1,5 @@ use std::error::Error; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Formatter}; use alloy_primitives::hex::FromHexError; use alloy_primitives::ruint::ParseError; diff --git a/gemstone/src/alien/client.rs b/gemstone/src/alien/client.rs index bd6f972d5..bdf8194ce 100644 --- a/gemstone/src/alien/client.rs +++ b/gemstone/src/alien/client.rs @@ -1,7 +1,7 @@ use super::AlienProvider; use super::provider::AlienProviderWrapper; use std::sync::Arc; -use swapper::{RpcClient, RpcProvider}; +use swapper::RpcClient; pub type AlienClient = RpcClient; From a3662e3dcc214d3b1c425ad3c8a24029117426fe Mon Sep 17 00:00:00 2001 From: gemdev111 <171273137+gemdev111@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:47:42 +0200 Subject: [PATCH 23/23] Improve naming --- crates/gem_evm/src/rpc/client.rs | 2 +- crates/yielder/src/yo/client.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index e57e5be25..c6d3874ca 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -288,7 +288,7 @@ impl EthereumClient { } #[cfg(feature = "rpc")] - pub async fn multicall3_batch( + pub async fn multicall3_map( &self, items: &[T], build: impl Fn(&T) -> [Call3; N], diff --git a/crates/yielder/src/yo/client.rs b/crates/yielder/src/yo/client.rs index c34f6243a..0a68ea135 100644 --- a/crates/yielder/src/yo/client.rs +++ b/crates/yielder/src/yo/client.rs @@ -58,7 +58,7 @@ impl YoGatewayClient { pub async fn get_positions(&self, assets: &[YoAsset], owner: Address) -> Result, YielderError> { Ok(self .ethereum_client - .multicall3_batch( + .multicall3_map( assets, |a| { let vault = a.yo_token.to_string();